docs/plans/2026-04-28-slate-v2-absolute-architecture-review-plan.md
Date: 2026-04-28 Status: done; slate-review rerun closed Score: 0.923 (rerun closure score; previous closure score was 0.924)
The architecture direction is still the right one, and this rerun is closed. The prior execution completed the accepted plan; this review cycle rechecked that plan against live source and found one public-DX guard to add.
The previous multi-pass review completed and drove the implementation lane. This rerun has its own pass schedule. Pass 1 and Pass 2 are recorded below; Pass 3 hardens the public write policy, Pass 4 answers maintainer and ecosystem objections, Pass 5 folds that policy into the execution plan, and Pass 6 closes the review gates.
Keep the Slate model and operations. Hard cut the remaining public API clutter toward:
editor.read((state) => {
state.selection.get();
});
editor.update((tx) => {
tx.nodes.set(props, { at: target });
});
The implementation lane closed the exact issues this plan targeted.
Closed in /Users/zbeyens/git/slate-v2:
focused / selected / actionsonKeyCommandonSnapshotChangeonValueChange / onSelectionChangeNew review finding:
editor.update((tx) => tx.nodes.*), but live examples and tests still teach
editor.update(() => editor.insertNodes(...)) and similar primitive editor
methods. Pass 3 resolves the policy: tx.* is the only normal public write
path; primitive editor write methods may remain only as advanced/internal
bridge APIs and must leave first-party authoring docs/examples/tests.2026-04-28 correction: the ecosystem lane was over-scoped. Slate v2 does not
need to support current Plate or slate-yjs APIs directly, and it must not
require current-version adapter fixtures. The migration requirement is a raw
architecture backbone only: stable operations, commits, snapshots, state /
tx extension namespaces, schema/spec policy, and local-only render targets so
Plate, slate-yjs, and similar libraries have a credible migration path. Any
older "Plate adapter fixture" wording in this plan is superseded by this
correction.
This is the previous closure score from the completed review lane. It does not
close the current rerun; the active rerun closure score is 0.923 in section
2.6.
| Dimension | Weight | Score | Evidence |
|---|---|---|---|
| React 19.2 runtime performance | 0.20 | 0.92 | Pass 3 rechecked node/text selector dirty-runtime-id filtering in /Users/zbeyens/git/slate-v2/packages/slate-react/src/hooks/use-node-selector.tsx:31, root source filters in /Users/zbeyens/git/slate-v2/packages/slate-react/src/editable/root-selector-sources.ts:23, and render profiler budgets in /Users/zbeyens/git/slate-v2/playwright/stress/generated-editing.test.ts:267; the revision pass moved eager void subscriptions, stale-target handling, and plugin browser contracts into implementation phases and final gates instead of leaving them as review-only notes. |
| Slate-close unopinionated DX | 0.20 | 0.93 | Pass 4 rechecked legacy Slate docs for onKeyDown and onChange, current v2 onKeyCommand, onSnapshotChange, RenderVoidProps, hook exports, and the accepted state / tx decision. Pass 5 rechecked legacy Slate command/transform docs, Tiptap command/chain source, current v2 command registry, and the current extension-method mutation surface. Pass 9 cut raw Slate filtered change callbacks; the final raw callback surface is onChange plus advanced onCommit. Pass 10 challenged every hard cut as a skeptical Slate maintainer and kept the Slate mental model: document value, paths, operations, Editable, renderElement, renderLeaf, onKeyDown, onChange, and plain React renderers. |
| Plate and slate-yjs migration shape | 0.15 | 0.93 | Pass 6 rechecked Plate table typed API/transform groups in /Users/zbeyens/git/plate-2/packages/table/src/lib/BaseTablePlugin.ts:119, link element config in /Users/zbeyens/git/plate-2/packages/link/src/lib/BaseLinkPlugin.ts:13, mark transform sugar in /Users/zbeyens/git/plate-2/packages/basic-nodes/src/lib/BaseBoldPlugin.ts:27, image void/media transforms in /Users/zbeyens/git/plate-2/packages/media/src/lib/image/BaseImagePlugin.ts:33, Plate Yjs adapter APIs in /Users/zbeyens/git/plate-2/packages/yjs/src/lib/BaseYjsPlugin.ts:30, and v2 operation replay/commit contracts in /Users/zbeyens/git/slate-v2/packages/slate/test/apply-onchange-hard-cut-contract.ts:38 and /Users/zbeyens/git/slate-v2/packages/slate/test/collab-history-runtime-contract.ts:14. The 2026-04-28 correction cuts current-version adapter fixture requirements; the remaining requirement is migration-backbone proof only. |
| Regression-proof testing strategy | 0.20 | 0.92 | Pass 7 rechecked the operation-family contract list in /Users/zbeyens/git/slate-v2/playwright/stress/generated-editing.test.ts:43, inline void navigation at generated-editing.test.ts:254, markable inline void shell proof at generated-editing.test.ts:348, block void navigation and no-layout-gap proof at generated-editing.test.ts:408, editable island focus proof at generated-editing.test.ts:556, table boundary navigation at generated-editing.test.ts:625, search focus/decorations at generated-editing.test.ts:665, mouse toolbar selection at generated-editing.test.ts:698, replay proof in /Users/zbeyens/git/slate-v2/playwright/stress/replay.test.ts:19, and release/stress scripts in /Users/zbeyens/git/slate-v2/package.json:60. The revision pass moved final callback, hook, namespace, migration-backbone proof, stale-target, plugin browser contract, and collab replay proof into the proof matrix, phases, and final gates. |
| Research evidence completeness | 0.15 | 0.92 | Pass 8 rechecked the compiled research entrypoints in docs/research/index.md, the accepted state / tx decision in docs/research/decisions/slate-v2-state-tx-public-api-and-extension-namespaces.md:27, the cross-corpus steal/reject decision in docs/research/decisions/slate-v2-perfect-plan-should-steal-read-update-transaction-discipline-and-extension-dx.md:21, runtime-owned shell DX in docs/research/decisions/editor-node-dx-should-use-runtime-owned-shells-and-spec-first-renderers.md:19, React 19.2 evidence in docs/research/sources/editor-architecture/react-19-2-external-store-and-background-ui.md:31, and local source citations in /Users/zbeyens/git/lexical/packages/lexical/src/LexicalEditor.ts:1375, /Users/zbeyens/git/lexical/packages/lexical/src/LexicalUpdates.ts:101, /Users/zbeyens/git/prosemirror/state/src/transaction.ts:22, and /Users/zbeyens/git/tiptap/packages/core/src/CommandManager.ts:28. No contradiction was found. |
| shadcn-style composability and hook/component minimalism | 0.10 | 0.93 | Pass 9 reduced each public surface to one obvious path plus at most one advanced escape hatch: onChange / onCommit, renderVoid({ element, target }) / renderShellUnsafe, named hooks / useEditorSelector, state / tx, and Plate-owned product sugar. The revision pass keeps raw Slate minimal while requiring migration-backbone proof, not current Plate adapter support. |
Weighted total: 0.924.
Historical completion threshold passed:
0.920.85Status: complete for Pass 1 only. Completion remains pending.
Current verdict: keep the architecture direction, but do not re-close the plan yet. The code is stronger than the old header says, while the public write-DX story is weaker than the accepted target says.
Score: 0.880.
| Dimension | Weight | Score | Evidence |
|---|---|---|---|
| React 19.2 runtime performance | 0.20 | 0.90 | EditableDOMRoot is mostly wiring through useEditableRootRuntime and stable runtime handlers in /Users/zbeyens/git/slate-v2/packages/slate-react/src/components/editable.tsx:124 and /Users/zbeyens/git/slate-v2/packages/slate-react/src/editable/runtime-event-engine.ts:100; root selector sources are named and operation-filtered in /Users/zbeyens/git/slate-v2/packages/slate-react/src/editable/root-selector-sources.ts:26; stress rows assert render budgets in /Users/zbeyens/git/slate-v2/playwright/stress/generated-editing.test.ts:45 and run through the generated harness at generated-editing.test.ts:930. |
| Slate-close unopinionated DX | 0.20 | 0.82 | Accepted research says normal writes should be editor.update((tx) => tx.nodes.set(...)) in docs/research/decisions/slate-v2-state-tx-public-api-and-extension-namespaces.md:27; live BaseEditor still exposes primitive transform methods in /Users/zbeyens/git/slate-v2/packages/slate/src/interfaces/editor.ts:258; first-party examples still teach editor.update(() => editor.insertNodes(...)) and editor.update(() => editor.removeNodes(...)) in /Users/zbeyens/git/slate-v2/site/examples/ts/images.tsx:100 and images.tsx:147; write-boundary tests explicitly call this the routed primitive path in /Users/zbeyens/git/slate-v2/packages/slate/test/write-boundary-contract.ts:79. |
| Plate and slate-yjs migration-backbone shape | 0.15 | 0.91 | Type fixtures prove plugin-style state / tx groups in /Users/zbeyens/git/slate-v2/packages/slate/test/generic-extension-namespace-contract.ts:132; runtime extension tests prove tx groups read transaction-local state in /Users/zbeyens/git/slate-v2/packages/slate/test/extension-namespaces-contract.ts:103; collab contracts prove deterministic remote replay and local runtime-id null/rebase behavior in /Users/zbeyens/git/slate-v2/packages/slate/test/collab-history-runtime-contract.ts:113 and collab-history-runtime-contract.ts:152; browser rows add stale-target replay in /Users/zbeyens/git/slate-v2/playwright/stress/generated-editing.test.ts:106. |
| Regression-proof testing strategy | 0.20 | 0.92 | Operation-family contracts cover inline voids, markable inline voids, block voids, editable islands, stale targets, tables, search focus, toolbar selection, paste, and IME in /Users/zbeyens/git/slate-v2/playwright/stress/generated-editing.test.ts:45; the plugin contract registry exists in /Users/zbeyens/git/slate-v2/packages/slate-browser/src/playwright/index.ts:2413; Phase 7 recorded bun check:full exit 0 plus focused retry-disabled reruns for the two retry-resolved rows. |
| Research evidence completeness | 0.15 | 0.86 | The React 19.2 page still supports React as projection scheduler, not editor invalidation engine, in docs/research/sources/editor-architecture/react-19-2-external-store-and-background-ui.md:57; the newer state/tx decision is accepted in docs/research/decisions/slate-v2-state-tx-public-api-and-extension-namespaces.md:27; this pass found stale wording in the older steal/reject/defer decision and added a maintain note there, so the lane is usable but needs a proper research refresh pass before closure. |
| shadcn-style composability and hook/component minimalism | 0.10 | 0.86 | renderVoid is minimal at /Users/zbeyens/git/slate-v2/packages/slate-react/src/components/editable-text-blocks.tsx:190; stale hook aliases and callback names grep clean in packages/slate-react/src, tests, and first-party examples; five React.createElement callsites remain in slate-react components, including /Users/zbeyens/git/slate-v2/packages/slate-react/src/components/editable.tsx:187, so JSX cleanup is still a small DX/composability polish item rather than a blocker. |
Weighted total: 0.880.
Pass 1 findings:
state / tx, but live
examples still teach primitive editor methods inside editor.update. Either
make tx.* the author-facing path in examples/docs/tests or explicitly
demote primitive editor methods to advanced/internal bridge status with a
release guard.createElement leftovers are not a runtime architecture blocker,
but if the goal is absolute DX and shadcn-style component readability, the
five remaining slate-react component callsites should be converted or
explicitly justified.Plan delta from Pass 1:
slate-review rerun.0.880.0.924 to historical closure score only.Next owner:
Status: complete for Pass 2 only. Completion remains pending.
Verdict: the research direction is not contradictory, but the live public
surface is not final. state / tx is real and tested. The docs/examples
still teach primitive editor.* writes as normal authoring DX, so this stays a
P1 until the plan either hard-cuts those examples to tx.* or explicitly
classifies primitive methods as advanced/internal bridge APIs.
Score: 0.886.
| Dimension | Weight | Score | Evidence |
|---|---|---|---|
| React 19.2 runtime performance | 0.20 | 0.90 | Dirty runtime-id selection and decoration impact stay in /Users/zbeyens/git/slate-v2/packages/slate/src/core/public-state.ts:1200 and public-state.ts:1226; this pass did not find new React hot-path regression evidence, so performance stays unchanged from Pass 1. |
| Slate-close unopinionated DX | 0.20 | 0.83 | state / tx is proven by /Users/zbeyens/git/slate-v2/packages/slate/test/state-tx-public-api-contract.ts:29, state-tx-public-api-contract.ts:51, and state-tx-public-api-contract.ts:80; live BaseEditor still wires primitive writes in /Users/zbeyens/git/slate-v2/packages/slate/src/create-editor.ts:200 and create-editor.ts:247; docs still teach primitive method DX in /Users/zbeyens/git/slate-v2/docs/concepts/04-transforms.md:3 and /Users/zbeyens/git/slate-v2/docs/concepts/07-editor.md:41. |
| Plate and slate-yjs migration-backbone shape | 0.15 | 0.92 | tx read/write coherence and extension namespace direction remain the accepted backbone in docs/research/decisions/slate-v2-state-tx-public-api-and-extension-namespaces.md; applyOperations remains the explicit replay writer in /Users/zbeyens/git/slate-v2/packages/slate/test/write-boundary-contract.ts:57, which keeps collaboration proof out of React callback naming. |
| Regression-proof testing strategy | 0.20 | 0.92 | Browser-operation family coverage from Pass 1 still stands; this pass added a sharper unit/docs gap: /Users/zbeyens/git/slate-v2/packages/slate/test/write-boundary-contract.ts:79 intentionally preserves primitive writes inside update, so future regression contracts must prove the final chosen public write path rather than accepting both as normal DX. |
| Research evidence completeness | 0.15 | 0.88 | Refreshed docs/research/decisions/slate-v2-state-tx-public-api-and-extension-namespaces.md with live-source evidence and appended docs/research/log.md; the older steal/reject/defer decision now points to the state/tx decision as current naming authority. |
| shadcn-style composability and hook/component minimalism | 0.10 | 0.86 | No new React component API issue surfaced beyond Pass 1; minimalism stays limited by the same public-write clutter: normal docs still expose a large primitive editor object instead of one grouped tx surface. |
Weighted total: 0.886.
Pass 2 findings:
editor.update((tx) => tx.nodes.*), but live docs/tests/examples still teach
editor.update(() => editor.*).tx.* is the accepted target;
primitive editor.* writes are either advanced/internal bridge APIs or need
hard-cut migration from first-party author-facing docs/examples.applyOperations is not part of the mismatch. It remains the explicit
operation replay writer for collaboration and replay proof.Plan delta from Pass 2:
0.880 to 0.886.0.91 to 0.92 because the tx
substrate and replay writer proof are real.0.86 to 0.88 because the stale-source gap is
resolved.0.85 floor because public docs/examples still teach the
wrong normal write surface.Next owner:
tx.* the only normal public write path in docs/examples/tests and
classify primitive editor writes as advanced/internal bridge APIsStatus: complete for Pass 3 only. Completion remains pending.
Verdict: do not revise the accepted API target downward. The normal public write path is:
editor.update((tx) => {
tx.nodes.set(props, { at: target });
});
Primitive editor.* write methods are not the normal authoring API. They may
exist as advanced/internal bridge APIs for legacy transform fixtures, core
runtime composition, codemods, and low-level compatibility during the rewrite,
but first-party docs, examples, walkthroughs, and public API pages must teach
tx.*.
Keeping primitive editor writes as normal public DX would be the wrong call. It
would preserve a large editor object, make state / tx look optional, and
teach the exact surface the architecture is trying to stop.
Score: 0.903.
| Dimension | Weight | Score | Evidence |
|---|---|---|---|
| React 19.2 runtime performance | 0.20 | 0.90 | Unchanged from Pass 2: dirty runtime-id impact and React projection remain the performance owner; this pass did not touch hot runtime evidence. |
| Slate-close unopinionated DX | 0.20 | 0.88 | The accepted tx shape is proven in /Users/zbeyens/git/slate-v2/packages/slate/test/state-tx-public-api-contract.ts:56; current docs still teach primitive methods in /Users/zbeyens/git/slate-v2/docs/concepts/04-transforms.md:3, /Users/zbeyens/git/slate-v2/docs/concepts/06-commands.md:3, and /Users/zbeyens/git/slate-v2/docs/api/transforms.md:3; the pressure decision keeps tx.* normal and demotes primitive writes from public authoring docs. |
| Plate and slate-yjs migration-backbone shape | 0.15 | 0.93 | state / tx extension namespaces stay aligned with the migration backbone in docs/research/decisions/slate-v2-state-tx-public-api-and-extension-namespaces.md:60; keeping primitive editor writes out of normal docs protects Plate-owned product sugar from leaking back into raw Slate. |
| Regression-proof testing strategy | 0.20 | 0.92 | /Users/zbeyens/git/slate-v2/packages/slate/test/write-boundary-contract.ts:27 rejects primitive writes outside editor.update, while write-boundary-contract.ts:79 still proves the old routed primitive path; the revision pass must add public-surface guards that first-party authoring examples/docs use tx.* and low-level primitive fixtures are classified explicitly. |
| Research evidence completeness | 0.15 | 0.89 | The refreshed state/tx decision page records the live-source split in docs/research/decisions/slate-v2-state-tx-public-api-and-extension-namespaces.md:100; no new research contradiction was found. |
| shadcn-style composability and hook/component minimalism | 0.10 | 0.90 | The pass restores the one-normal-path rule for writes: app authors see editor.update((tx) => ...), while primitive editor writes become advanced/internal bridge APIs instead of another public component authoring style. |
Weighted total: 0.903.
Pass 3 findings:
tx.* is normal public DX.editor.* writes as
normal usage.Plan delta from Pass 3:
tx.*.0.85 floor because the plan now has one normal write
path again.pending: the score is below 0.92, the objection/revision
passes are still incomplete, and the final implementation acceptance criteria
must include docs/examples/public-surface guards for the chosen policy.Next owner:
tx.* as the normal pathStatus: complete for Pass 4 only. Completion remains pending.
Verdict: keep the hardened public write policy.
The strongest maintainer objection is fair: legacy Slate users know
Editor.* and Transforms.*, and tx.nodes.set looks like a new dialect. The
answer is not to keep primitive editor.* writes as normal DX. Legacy Slate's
actual split was already "editor value + static helpers + transforms +
commands." tx.* preserves that idea while making update ownership explicit:
reads and writes happen in the transaction view, operations still fall out as
Slate operations, and the editor object stops becoming a dumping ground.
The strongest ecosystem objection is also fair: Plate currently has
editor.api / editor.tf, and slate-yjs current integrations are not written
against state / tx. Raw Slate should still not support those current APIs.
The migration target is the backbone: typed extension namespaces on state and
tx, deterministic operations/commits, explicit operation replay, and local
targets that never become collaboration identity.
Score: 0.918.
| Dimension | Weight | Score | Evidence |
|---|---|---|---|
| React 19.2 runtime performance | 0.20 | 0.90 | No new runtime objection changes the React projection model from Pass 3. |
| Slate-close unopinionated DX | 0.20 | 0.91 | Legacy Slate uses Transforms.insertNodes(editor, ...) and Transforms.setNodes(editor, ...) in /Users/zbeyens/git/slate/docs/api/transforms.md:39 and /Users/zbeyens/git/slate/docs/walkthroughs/05-executing-commands.md:53; v2 docs currently teach primitive editor writes in /Users/zbeyens/git/slate-v2/docs/concepts/04-transforms.md:3 and /Users/zbeyens/git/slate-v2/docs/concepts/06-commands.md:3; the accepted v2 target keeps the Slate transform mental model but moves normal writes to tx.*. |
| Plate and slate-yjs migration-backbone shape | 0.15 | 0.94 | Plate pressure is real: table/media/yjs still use PluginConfig, extendEditorTransforms, editor.api, and editor.tf in /Users/zbeyens/git/plate-2/packages/table/src/lib/BaseTablePlugin.ts:119, /Users/zbeyens/git/plate-2/packages/media/src/lib/image/BaseImagePlugin.ts:58, and /Users/zbeyens/git/plate-2/packages/yjs/src/lib/BaseYjsPlugin.ts:71; v2 proves the intended backbone with typed extension groups in /Users/zbeyens/git/slate-v2/packages/slate/test/generic-extension-namespace-contract.ts:132 and remote replay in /Users/zbeyens/git/slate-v2/packages/slate/test/collab-history-runtime-contract.ts:113. |
| Regression-proof testing strategy | 0.20 | 0.93 | The objection pass makes the missing guard explicit: first-party authoring docs/examples/API pages must grep clean for normal primitive editor.* writes, while low-level transform fixtures stay classified. Existing write-boundary tests already split illegal primitive writes from routed bridge writes in /Users/zbeyens/git/slate-v2/packages/slate/test/write-boundary-contract.ts:27 and write-boundary-contract.ts:79. |
| Research evidence completeness | 0.15 | 0.91 | The accepted state/tx decision explicitly rejects api / tf as raw Slate naming and explains extension groups in docs/research/decisions/slate-v2-state-tx-public-api-and-extension-namespaces.md:27 and docs/research/decisions/slate-v2-state-tx-public-api-and-extension-namespaces.md:60; no contradiction surfaced. |
| shadcn-style composability and hook/component minimalism | 0.10 | 0.93 | The pass keeps one normal write authoring surface and one advanced bridge: app/component authors use editor.update((tx) => ...); internal/runtime fixtures may use primitive editor writes only when classified. |
Weighted total: 0.918.
Maintainer objections answered:
Editable, renderElement, renderLeaf, onKeyDown, and
onChange. It changes where writes are expressed.state / tx look optional.Transforms.*." Accepted in spirit:
docs should teach tx.nodes.*, tx.text.*, tx.selection.*, and
tx.marks.* as the transaction-owned successor to transform families.Ecosystem objections answered:
editor.api / editor.tf as its product adapter vocabulary.
Raw Slate should not import those names.state and tx, not editor-object
mutation.Revision requirements from this pass:
tx.* as the normal write path.applyOperations as the explicit replay writer and outside the normal
authoring-DX ban.Next owner:
Status: complete for Pass 5 only. Completion remains pending until the
closure pass verifies the gates.
Verdict: the plan now owns the hardened public write policy in the main execution sections, not only in the objection notes.
Revision decisions:
tx.* is the only normal public write path.editor.* write methods are advanced/internal bridge APIs.tx.*.applyOperations remains the explicit operation replay writer and is outside
the normal authoring-DX ban.editor.api / editor.tf as adapter vocabulary; raw Slate
does not import those names.Score: 0.923.
| Dimension | Weight | Score | Evidence |
|---|---|---|---|
| React 19.2 runtime performance | 0.20 | 0.91 | The revision does not alter the React runtime owner, but the API target now prevents docs/examples from bypassing transaction-owned dirty commits with primitive write teaching. |
| Slate-close unopinionated DX | 0.20 | 0.92 | Section 4 now makes tx.* the normal authoring path while preserving Slate's transform-family mental model; Row 2b records the adoption story and rejects primitive editor writes as normal docs/API DX. |
| Plate and slate-yjs migration-backbone shape | 0.15 | 0.94 | Phase 6 and the final gates keep migration proof at the backbone layer: typed state / tx extension groups, operation replay, commit metadata, and local target behavior without current-version Plate/slate-yjs adapters. |
| Regression-proof testing strategy | 0.20 | 0.93 | The proof matrix, release-discipline gate, and final gates now require first-party authoring docs/examples/API pages to use tx.* and classify any primitive write fixtures explicitly. |
| Research evidence completeness | 0.15 | 0.91 | No research contradiction remains after the state/tx live-source refresh and Row 2b adoption answer. |
| shadcn-style composability and hook/component minimalism | 0.10 | 0.93 | The final public shape keeps one normal write surface, one operation-replay escape hatch, and one classified bridge for internals. |
Weighted total: 0.923.
Plan changes from revision:
editor.update.Next owner:
active goal state to done only if everything still passes.Status: complete. The slate-review rerun is closed.
Verdict: pass the plan. The rerun did not find a reason to pivot away from the
accepted architecture. It found one real public-DX gap: primitive
editor.* writes were still being treated as normal authoring material in live
docs/examples. The final plan closes that gap by making tx.* the only normal
public write path and by classifying primitive editor writes as
advanced/internal bridge APIs.
Closure score: 0.923.
Closure gates checked:
0.92 threshold0.85 floortx.* write policy are answeredFinal decision:
editor.read((state) => ...).editor.update((tx) => ...).tx.nodes.*, tx.text.*, tx.selection.*, and
tx.marks.*.editor.* writes only as advanced/internal bridge APIs.applyOperations as the explicit replay writer.editor.api / editor.tf vocabulary.Steal only the parts that beat Slate:
Reject:
$ public functions.Verdict: the plan is using research correctly. The external systems are proof sources for specific disciplines, not vague prestige citations.
What held up:
state / tx decision is accepted and directly ties the public API to
Lexical read/update lifecycle, ProseMirror transaction ownership, and Tiptap
extension discoverability.LexicalEditor.read / update are explicit instance methods, active editor
state is required during read/update callbacks, and dirty leaves/elements are
tracked below rendering.CommandManager
builds single-command and chained-command APIs around one transaction.useSyncExternalStore, Activity, deferred work, transitions, and
Performance Tracks make React a strong UI scheduler, but not a replacement
for an editor dirty-node runtime.What this means:
No active claim in the plan is currently relying on uncited external-source assertions.
Final north star:
Slate model + operations
small editor object
read-only state view
writable transaction view
runtime-owned render shells
node-scoped React selectors
generated browser parity proof
Plate/Yjs migration through stable operations and extension namespaces
Keep the editor object small:
editor.read(fn)
editor.update(fn, options?)
editor.getSnapshot()
editor.subscribe(listener)
editor.extend(extension)
editor.schema
Do not document normal app code against flat mutation methods like:
editor.setNodes();
editor.removeNodes();
editor.insertText();
editor.select();
editor.addMark();
Those move behind tx.
Normal authoring surfaces must teach tx.*, not primitive editor writes:
editor.update((tx) => {
tx.nodes.set({ type: "heading" }, { at: target });
tx.text.insert("hello");
tx.selection.set(target);
});
Primitive write methods such as editor.setNodes, editor.insertText,
editor.select, and editor.removeNodes are advanced/internal bridge APIs.
They may remain for core runtime composition, codemods, and low-level legacy
transform fixtures, but first-party docs, examples, walkthroughs, and public
API pages must not present them as normal app authoring DX.
editor.applyOperations(operations) is separate. It is the explicit operation
replay writer for history, collaboration, and replay fixtures, not normal
authoring syntax.
editor.read((state) => {
state.value.get();
state.selection.get();
state.marks.get();
state.nodes.get(target);
state.nodes.parent(target);
state.nodes.children(target);
state.nodes.match(options);
state.nodes.hasPath(path);
state.points.before(target, options);
state.points.after(target, options);
state.ranges.get(target);
state.ranges.edges(target);
state.text.string(target);
state.schema.isInline(element);
state.schema.isVoid(element);
state.schema.isSelectable(element);
state.schema.markableVoid(element);
});
tx includes read groups plus write groups. Reads inside update observe the
transaction-in-progress.
editor.update((tx) => {
tx.selection.get();
tx.selection.set(target);
tx.selection.collapse({ edge: "end" });
tx.selection.move({ distance: 1 });
tx.selection.clear();
tx.nodes.insert(node, { at });
tx.nodes.insertMany(nodes, { at });
tx.nodes.remove({ at });
tx.nodes.set(props, { at });
tx.nodes.unset(keys, { at });
tx.nodes.split(options);
tx.nodes.merge(options);
tx.nodes.wrap(element, options);
tx.nodes.unwrap(options);
tx.nodes.move({ at, to });
tx.text.insert("hello");
tx.text.delete(options);
tx.marks.add("bold", true);
tx.marks.remove("bold");
tx.marks.toggle("bold");
tx.meta.set("source", "keyboard");
tx.history.undo();
tx.history.redo();
tx.normalize();
tx.withoutNormalizing(() => {});
});
Extensions add namespaces to state and tx, not flat editor methods:
defineEditorExtension({
key: "table",
state: {
table(state) {
return {
currentCell() {},
isInTable(target = state.selection.get()) {},
};
},
},
tx: {
table(tx) {
return {
insertRow() {},
removeColumn() {},
};
},
},
});
Usage:
editor.read((state) => {
state.table.currentCell();
});
editor.update((tx) => {
tx.table.insertRow();
});
Verdict: raw Slate should expose lifecycle and primitive grouped capabilities, not a product command catalog.
Keep in raw Slate:
editor.read((state) => ...)editor.update((tx) => ...)read, update, getSnapshot, subscribe,
extend, schemastate and tx: selection, nodes, text,
marks, schema, meta, historystate and txDo not put in raw Slate public DX:
editor.commandseditor.chain()chain().focus().run() as toolbar ceremonymethods that mutate the editor objecteditor.table.insertRow()editor.isVoid, editor.isInline, editor.markableVoid,
editor.isSelectableThe critical distinction:
// Raw Slate primitive.
editor.update((tx) => {
tx.nodes.set({ type: "heading", level: 2 }, { at: target });
});
// Plate or extension sugar, still lifecycle-owned.
editor.update((tx) => {
tx.table.insertRow();
});
That second shape is allowed only because table is an extension namespace on
tx, not because core Slate has a command catalog.
Evidence:
editor.commands and editor.chain() are strong
product DX, but also proves why that ceremony should stay above raw Slateeditor.commands, but its extension
methods path still recomposes methods onto the editor object; that is the
next hard cut for this API laneImplementation consequence:
methods with state and tx group registrationeditor.commands, editor.chain,
direct extension method recomposition, and normal-example predicate
monkeypatchingeditor.tf, editor.api, chains, toolbar commands, and
product sugar as an adapter layer over raw editor.updateeditor.update creates one transaction runtime:
editor.update
-> tx snapshot view
-> target resolution / rebasing
-> primitive grouped writes
-> operations
-> EditorCommit
-> history / collab / React / DOM repair / browser proof
Targets passed to renderers are rebasing runtime targets, not raw Path
values. A target can resolve to the current path, range, runtime id, or null if
the node no longer exists.
Core schema predicates compile from element specs and extension predicates:
defineElement({
type: "mention",
inline: true,
void: "markable-inline",
selectable: true,
});
Manual predicate overrides remain advanced extension policy.
Hard cut:
renderVoid({ element, target });
Do not pass:
focused;
selected;
actions;
children;
attributes;
Renderer example:
function ImageVoid({ element, target }: RenderVoidProps<ImageElement>) {
const editor = useEditor();
const selected = useElementSelected(target);
return (
<ImageCard
src={element.url}
selected={selected}
onRemove={() => {
editor.update((tx) => {
tx.nodes.remove({ at: target });
});
}}
/>
);
}
This preserves content-only render DX and removes app-owned spacer/anchor responsibility.
Hard cut before publish:
useSlateStatic -> useEditor
useSlateSelector -> useEditorSelector
useFocused -> useEditorFocused
useSelected -> useElementSelected
useReadOnly -> useEditorReadOnly
useComposing -> useEditorComposing
useEditorSelector is advanced. Public docs should teach named hooks first.
Replace public onSnapshotChange with:
<Slate
onChange={({ value, selection, operations, snapshot, changed }) => {}}
onCommit={(commit, snapshot) => {}}
/>
onChange is the normal public surface. onCommit is the low-level runtime
tap.
Do not ship raw Slate onValueChange or onSelectionChange. They are filtered
convenience callbacks that Plate or an app adapter can layer on top of
onChange.
Remove public onKeyCommand.
Keep Slate-close naming:
<Editable
onKeyDown={(event, ctx) => {
if (isHotkey("mod+b", event)) {
ctx.editor.update((tx) => {
tx.marks.toggle("bold");
});
return true;
}
}}
/>
Return contract:
true: handled, prevent default, run model-owned repairEditableRepairRequest: handled with explicit repair policyvoid: fall through to runtimeVerdict: keep the target API names, with two hard adjustments.
What stays close to Slate:
renderElement, renderLeaf, onKeyDown, onChange,
and onCommitattributes + childrenWhat must not stay:
onKeyCommand; it is engine-shaped and duplicates onKeyDownonSnapshotChange; public users should not learn snapshots before
value/selection/change semanticschildren, attributes, actions, focused, or
selectedRenderVoidPropsFor casts in examplesuseSlateStatic as the public "get editor" hook nameeditor.isVoid, editor.markableVoid, and editor.isSelectable
as the normal authoring storyFinal naming:
editor.read((state) => ...)editor.update((tx) => ...)renderVoid({ element, target })useEditor()useEditorSelector() for advanced selector workuseElementSelected(target) and useEditorFocused() as opt-in hooks<Editable onKeyDown={(event, ctx) => ...} /><Slate onChange={...} onCommit={...} />Reason:
Editable, renderElement, renderLeaf, onKeyDown, onChange, value,
selection, and operationsstate / tx is better than api / tf for raw Slate because it is
semantic English and not Plate-shapedVerdict: the plan is simpler after one real cut. Raw Slate should expose one normal public path and one advanced escape hatch per surface.
Final public/advanced split:
editor.read and editor.update; advanced
getSnapshot / subscribe for stores and adapterstx; advanced replay through explicit
applyOperationsstate and tx; no flat
method injection on the editor objectrenderElement / renderLeaf keep Slate-style
attributes + childrenrenderVoid({ element, target }); advanced
renderShellUnsafe only when the author also owns browser contractsuseEditor, useElementSelected, and
useEditorFocused; advanced useEditorSelectoronKeyDown(event, ctx); no public onKeyCommand or
onCommandonChange; advanced onCommiteditor.api, editor.tf, command
catalogs, chains, and filtered callbacksCuts from raw Slate:
onValueChange and onSelectionChange; they are convenience filters over
onChange, not separate raw lifecycle APIseditor.chain() as future raw Slate possibility; it belongs in Plate/product
sugar if neededWhat stays:
onCommit stays because collaboration, history, instrumentation, and adapters
need a low-level runtime tap.useEditorSelector stays because advanced UI needs selector power, but docs
should teach named hooks first.renderShellUnsafe may exist only as an explicitly ugly escape hatch with
browser-contract proof.Simplicity rule for implementation:
If a raw Slate API exists only because it is convenient sugar, move it to Plate.
If a raw Slate API can corrupt browser/runtime ownership, make it advanced and
require proof.
Plate may later expose product APIs on top of Slate primitives:
editor.update((tx) => {
tx.table.insertRow();
tx.link.toggle({ href });
});
Raw Slate must not implement or support current Plate editor.tf /
editor.api directly. Those names are Plate-owned product sugar if Plate
chooses to build an adapter later.
Backbone route:
tx.table.* and query groups like state.table.*.Proof required:
state.<plugin> and tx.<plugin> groupsRenderVoidPropsFor castsThe collab contract remains operations, commits, snapshots, and deterministic normalization.
Rules:
source: 'remote'Proof required:
onChange /
onCommitEditorTarget handles fail softly or rebase through runtime APIs;
they never become serialized collaboration identityVerdict: the migration path is credible only as a raw Slate backbone. The previous adapter-fixture requirement was too high and is cut. Slate v2 should prove the substrate that Plate/slate-yjs can migrate to, not support their current versions.
Table/plugin row:
api.create.table, api.table.getSelectedCell, and
api.table.isCellSelected map to state.create.* and state.table.*.tf.insert.tableRow, tf.remove.tableColumn, and tf.table.merge are
examples of product transforms that can map to raw tx.insert.* /
tx.table.* groups later.Link/mark row:
editor.tf.toggleMark(type).
Raw Slate should expose primitive mark transforms on tx.marks.*; Plate can
keep tf.bold.toggle() or equivalent adapter sugar.Void/media row:
isVoid and media insertion transforms today.tx.image.* / tx.media.* adapter groups rather
than renderer-owned children, attributes, or actions.Type-inference row:
PluginConfig generic slots prove the ecosystem depends on
inferred API, transform, option, and selector groups.state and tx so
examples do not need casts like RenderVoidPropsFor.slate-yjs / operation replay row:
applyOperations replay entrypoint.Keep:
editor.api / editor.tf as its own product names.state / tx, not api / tf, not product command
catalogs, and not chain-first toolbar ceremony.Backbone proof required before closure:
state.<plugin> and tx.<plugin> groups| Family | Required proof |
|---|---|
| Inline void navigation | mentions, inlines, and stress family inline-void-boundary-navigation; assert model selection, DOM selection, render budget. |
| Block void navigation | images, embeds, stress family block-void-navigation; assert no spacer layout gap and selection before/on/after. |
| Markable inline void | mentions, stress family markable-inline-void-formatting; assert mark styling, selection, render budget. |
| Editable island | editable-voids; assert native input focus stays inside island and outer editor selection restores. |
| Keyboard command handling | examples using onKeyCommand become onKeyDown(event, ctx) rows; assert handled return prevents default and model repair runs. |
| Change callbacks | unit tests for onChange and onCommit; assert changed distinguishes value-only, selection-only, metadata-only, and remote commits. |
| Hook renames | public surface contract tests; no exported old names before publish. |
| State/tx lifecycle | unit tests: reads outside callback fail or route through snapshot; writes outside update fail; reads inside tx see transaction-local changes. |
| Public write surface | release-discipline guard: first-party authoring docs, examples, walkthroughs, and public API pages teach tx.*; primitive editor write usages are allowed only in classified core/runtime/legacy transform fixtures. |
| Extension namespaces | unit tests: extension state group, tx group, conflict detection, dependency order, cleanup. |
| Plate migration backbone | synthetic table/link/media-style namespace fixtures prove state.<plugin> / tx.<plugin> composition without promising current Plate API support. |
| Collab migration backbone | remote operation replay, commit metadata, deterministic normalization, and target rebase/null behavior without promising current slate-yjs API support. |
| Plugin browser contracts | plugin-provided browser rows can register with the generated proof system without copy-pasting Playwright mechanics. |
| Ecosystem TypeScript | fixtures for nested plugin groups, collision errors, composed inference, and state.<plugin> / tx.<plugin> augmentation. |
Fast CI:
slate-browser core proofstate / tx groupsSparing stress:
bun test:stressSTRESS_REPLAYRelease gate:
bun check:fullVerdict: keep the target architecture, but do not close performance yet.
What held up:
useMountedNodeRenderSelector and useMountedTextRenderSelectorchange.nodeImpactRuntimeIdsslate-react, slate-browser,
integration tests, and generated stress testsWhat still fails the absolute bar:
EditableRenderedVoid still computes focused and selected for every void
renderer before user render code runsuseFocused() is cheap but broad by design; it should not be injected into
every void renderer by defaultuseSelected() is runtime-id filtered, but still opt-out instead of opt-in
for void authorsRequired plan response:
renderVoid must receive element + target onlyuseSelected(target) or
useElementSelected(target)useSlateSelector remains allowed only inside named source hooks or
advanced APIs, not as the ordinary hot-render authoring surfaceVerdict: the regression strategy is finally pointed at the right owner, but it is not closure-grade yet.
What held up:
slate-browser owns operation-family contracts instead of leaving examples
as the primary safety net.STRESS_REPLAY replays the
generated browser steps against the same route and surface.bun check loop and
reserve it for bun check:full, test:stress, and release gates.What still fails closure:
onSnapshotChange and useSlateStatic.onKeyDown(event, ctx) handled-result behavior needs an explicit browser or
unit row replacing public onKeyCommand.onChange and onCommit need final callback contract tests that distinguish
value-only, selection-only, metadata-only, and remote commits.state / tx namespace proof needs both unit type fixtures and at
least one browser row proving commands routed through tx still repair DOM
selection correctly.Required plan response:
slate-browser core
proof, selected integration examples, and targeted render budgets.test:stress and test:stress:replay, not in
default CI.state / tx lifecycle and namespace fixturesonKeyDown(event, ctx) handled-result contractslate-browser contract family or step kind.Hard cut:
onKeyCommandonSnapshotChangeonValueChange / onSelectionChangefocused / selected / actions in renderVoiduseSlateStaticeditor.updateRejected:
editor.update(({ api, tf }) => {})editor.update(() => editor.setNodes(...)) as normal first-party docs/API DXeditor.api / editor.tf in raw Slateeditor.commands as core APIstate and txeditor.read((state) => ...) and editor.update((tx) => ...)
the public lifecycle./Users/zbeyens/git/lexical/packages/lexical/src/LexicalUpdates.ts:101;
ProseMirror transaction ownership in
/Users/zbeyens/git/prosemirror/state/src/transaction.ts:22.editor.update((tx) => ...);
reads move into editor.read((state) => ...).tx.<plugin>.*.state / tx APIs instead of flat editor methodsstate.selection, state.nodes, tx.nodes,
tx.marks, etc.BaseEditor surface is
already large and mixed; read/write groups reduce clutter and prevent writes
from existing in read context./Users/zbeyens/git/slate-v2/packages/slate/src/interfaces/editor.ts:118.editor.api / editor.tf. Weaker because it leaves
writes visible outside update and uses unclear names.editor.setNodes(props, opts) to
editor.update((tx) => tx.nodes.set(props, opts)).tx.*; primitive
editor.insertText, editor.setNodes, editor.select, and similar writes
may remain only as advanced/internal bridge APIs.editor.update(() => editor.insertText()) is shorter and
already guarded by runtime checks."state / tx architecture./Users/zbeyens/git/slate-v2/docs/concepts/04-transforms.md:3;
/Users/zbeyens/git/slate-v2/packages/slate/test/write-boundary-contract.ts:79;
docs/research/decisions/slate-v2-state-tx-public-api-and-extension-namespaces.md:27.state / tx becomes ceremony instead of the
public architecture.tx.nodes.*,
tx.text.*, tx.selection.*, and tx.marks.*; codemods can rewrite common
editor.update(() => editor.setNodes(...)) forms to
editor.update((tx) => tx.nodes.set(...)).tx.* only.editor.tf; raw
Slate extension transforms attach to tx.<plugin>.applyOperations.state.table.* and tx.table.*, not flat
editor.insertRow./Users/zbeyens/git/slate-v2/packages/slate/src/core/editor-extension.ts:156;
Tiptap proves extension commands are valuable in
/Users/zbeyens/git/tiptap/packages/core/src/Extendable.ts:113.state and tx groups.state.table and
tx.table.renderVoid({ element, target })focused, selected, and actions from normal renderVoid.actions.remove() and
selected./Users/zbeyens/git/slate-v2/packages/slate-react/src/components/editable-text-blocks.tsx:208;
void stress render budgets in
/Users/zbeyens/git/slate-v2/playwright/stress/generated-editing.test.ts:410.const selected = useElementSelected(target) and
editor.update((tx) => tx.nodes.remove({ at: target })).onKeyDown(event, ctx) instead of public onKeyCommandonKeyCommand; strengthen Slate-close onKeyDown.onKeyCommand./Users/zbeyens/git/slate-v2/packages/slate-react/src/editable/keyboard-input-strategy.ts:117.onKeyDown, return true or an
EditableRepairRequest.editor.update((tx) => ...) through
the same contract.onChange / onCommit instead of duplicate change callbacksonChange the normal callback and onCommit the low-level
commit tap. Do not ship raw Slate onSnapshotChange, onValueChange, or
onSelectionChange.onSnapshotChange and users who
expected separate value-only or selection-only callbacks in raw Slate.onSnapshotChange is engine-shaped
and makes normal app state sync feel internal./Users/zbeyens/git/slate-v2/packages/slate-react/src/components/slate.tsx:35.onSnapshotChange and document it. Weaker because
it leaves the primary API less Slate-close than necessary.onSnapshotChange((snapshot, commit) => ...) becomes
onCommit((commit, snapshot) => ...) or
onChange(({ snapshot, commit, changed }) => ...). Existing value-only and
selection-only app callbacks become small filters over onChange.onChange for app state and
onCommit for low-level plugin telemetry.useSlateStatic -> useEditor, useSelected ->
useElementSelected, useFocused -> useEditorFocused, and related names.useSlateStatic is obscure, and
useSelected hides whether the selection is editor-wide or element-scoped./Users/zbeyens/git/slate-v2/packages/slate-react/src/index.ts:53.isInline, isVoid, markableVoid, isSelectable
into editor.schema and compiled element specs./Users/zbeyens/git/slate-v2/packages/slate/src/interfaces/editor.ts:188;
node DX decision in
docs/research/decisions/editor-node-dx-should-use-runtime-owned-shells-and-spec-first-renderers.md.slate-browser owns
replayable operation-family contracts./Users/zbeyens/git/slate-v2/playwright/stress/generated-editing.test.ts:46;
replay artifacts in
/Users/zbeyens/git/slate-v2/playwright/stress/replay.test.ts:19.test:stress and
release gates.RenderVoidPropsFor casts and
onKeyCommand usage, e.g.
/Users/zbeyens/git/slate-v2/site/examples/ts/images.tsx:50.Verdict: keep the hard cuts. The maintainer objection pass did not find a reason to pivot back toward compatibility aliases, eager render props, flat editor methods, or duplicate callback surfaces.
The strongest fair objection is that this is a lot of change and can sound like "not Slate anymore." That objection is valid only if the plan loses Slate's authoring center. It does not. The plan keeps:
Editable, renderElement, renderLeaf, onKeyDown, and onChangeThe hard cuts target the places where the legacy shape actively teaches the wrong owner:
Maintainer challenge results:
| Row | Decision | Maintainer answer |
|---|---|---|
1. state / tx lifecycle | Keep | This is the largest mental shift, but it is the right one. It preserves Slate operations while making stale reads and illegal writes harder to author. |
2. grouped state / tx APIs | Keep | The grouped shape is more teachable than flat editor clutter. It also gives plugin namespaces one obvious home. |
| 3. extension namespaces | Keep | Flat method injection is convenient until two plugins pick the same verb. Namespace ownership is worth the break. |
4. renderVoid({ element, target }) | Keep | Convenience props are not worth global focus/selection fanout. Users who draw selection UI can opt into node-scoped hooks. |
5. onKeyDown(event, ctx) | Keep with strict docs/tests | Do not rename Slate's public keyboard surface. Strengthen it with a handled/repair return contract instead of shipping onKeyCommand. |
6. onChange / onCommit | Keep, strengthened | Normal users get Slate-close onChange; low-level consumers get onCommit. Separate raw onValueChange / onSelectionChange would reintroduce callback sprawl. |
| 7. hook renames | Keep | useEditor and node-scoped hook names are clearer than legacy names. No aliases before publish. |
| 8. schema/spec predicates | Keep, but keep escape policy | Schema owns browser-critical node behavior. Advanced predicate policy can exist, but it should compile into schema behavior. |
| 9. generated browser proof | Keep | This is the only credible answer to "I cannot report every bug one by one." Fast CI stays narrow; stress/replay handles human-like breadth. |
| 10. no compatibility aliases | Keep | The API is unpublished. Shipping aliases now is debt with a warning label. |
Required strengthening from this pass:
renderVoid({ element, target }) needs examples for image, mention, embed,
and editable island shapes, with selection UI using opt-in hooks.state / tx namespaces need TypeScript fixtures for core groups and plugin
group augmentation.No objection row moves to drop or revise. Row 6 was strengthened after the
Pass 9 callback simplification because raw onValueChange / onSelectionChange
are now explicitly cut.
Verdict: keep the architecture, but keep the ecosystem scope honest. The
ecosystem pass did not justify backing away from state /
tx, content-only void renderers, callback cuts, hook renames, schema/spec
predicates, or no aliases. It does not require Slate v2 to implement today's
Plate or slate-yjs adapters. The non-negotiable requirement is a migration
backbone: extension namespaces, operation replay, commit metadata, and local
target semantics must be good enough that those libraries can migrate.
Ecosystem challenge results:
| Perspective | Strongest objection | Decision | Required answer |
|---|---|---|---|
| Plate maintainer | Current Plate APIs, tests, and plugins rely heavily on editor.api / editor.tf; raw Slate state / tx could become churn with no product benefit. | Keep raw Slate state / tx; do not support current Plate APIs in raw Slate. Plate may build product sugar later. | Prove the backbone with synthetic table/link/media-style namespace fixtures and operation stability, not current-version adapter fixtures. |
| Plate plugin author | Extension namespaces can hurt inference if plugin groups, selectors, transforms, and options stop composing cleanly. | Keep extension namespaces. | Add TypeScript fixtures for nested plugin groups, collision errors, composed plugin inference, and state.<plugin> / tx.<plugin> augmentation. |
| slate-yjs maintainer | React callback names should not affect collaboration; target refs can go stale after remote operations. | Keep callback cuts and local target render props. Do not support current slate-yjs APIs directly. | Add raw collab-backbone contracts for remote operation replay, commit metadata, target rebasing/nullability, and no dependency on React onChange / onCommit for serialized collaboration. |
| Third-party plugin author | Runtime-owned void shells reduce footguns but can feel less flexible for unusual inline, editable-island, or embedded widgets. | Keep content-only renderers plus ugly unsafe escape hatch. | Add plugin-facing void kind examples for image, mention, embed, table-adjacent widget, and editable island; require renderShellUnsafe users to attach browser contracts. |
| Test/release maintainer | Generated browser contracts can become slow and hard to maintain. | Keep generated proof, split fast and slow lanes. | Add a plugin contract registry: fast core rows in CI, focused plugin rows on package change, full human-like replay in test:stress / release gates. |
| App author | Cutting focused, selected, and actions from void props removes convenient UI state. | Keep opt-in hooks and target-based editor methods. | Add small examples for selection UI, remove/select/set-node commands, stale target behavior, and toolbar usage. |
What this pass changes:
editor.api / editor.tf remain explicitly rejected as raw Slate names.
Plate may own those names later if it builds product sugar.EditorTarget needs a stale-target policy:
No hard cut is dropped. The revision pass must fold these ecosystem constraints into the implementation phases, final gates, and acceptance criteria.
Verdict: the plan now owns the maintainer and ecosystem objections in its main execution sections, not just in review notes.
Revision decisions:
editor.read((state) => ...) and
editor.update((tx) => ...).editor.api / editor.tf out of raw Slate and explicitly allow them as
Plate-owned adapter names.EditorTarget behavior as runtime policy, not an open design
vibe: local handles may rebase or fail softly; remote/collab state never
serializes target identity.The plan now has a high enough score for closure review, but not for automatic
completion. Closure still needs to verify every gate and then set
active goal state to done.
Verdict: close the review lane.
Closure gates checked:
0.924, above the 0.92 threshold0.92Historical note: this closed the previous review lane. The current rerun is closed in sections 2.1 through 2.6 and the 2026-04-28 rerun pass-state ledger below.
| Pass | Status | Evidence added | Plan delta | Open issues | Next owner |
|---|---|---|---|---|---|
| Current-state read and initial score | complete | Rechecked live renderVoid, onKeyCommand, onSnapshotChange, flat editor methods, extension method mutation, and research index/log entries. | Reopened plan from single-pass closure to multi-pass pending state; demoted 0.928 to candidate closure target; active score is 0.806. | None for Pass 1. | Research and live-source refresh pass. |
| Research and live-source refresh | complete | Rechecked Lexical read/update and active-context legality, Lexical extension packaging, ProseMirror transactions and EditorState.tr, Tiptap command/chain and extension hooks, official React 19.2 Activity/Performance Tracks, Slate selector context, and slate-browser selection/stress APIs. | Refreshed the React 19.2 source page to accepted/current; raised research score to 0.90; kept closure pending. | None for Pass 2. | Performance pressure pass. |
| Performance pressure pass | complete | Rechecked runtime-id node/text selectors, root source filters, eager void selection/focus props, render profiler plumbing, and generated render-budget stress rows. | Added Pass 3 performance verdict; raised performance score to 0.88; kept void eager subscription as a closure-blocking hard cut. | None for Pass 3; remaining work belongs to the implementation plan and later closure proof. | DX pressure pass. |
| DX pressure pass | complete | Rechecked legacy Slate onKeyDown, onChange, void docs, current v2 onKeyCommand, onSnapshotChange, RenderVoidProps, hook exports, and the state/tx decision page. | Added Pass 4 DX verdict; raised DX score to 0.89; kept void compatibility as explicitly rejected. | None for Pass 4; unopinionated-core pressure remains. | Unopinionated-core pass. |
| Unopinionated-core pass | complete | Rechecked legacy Slate command/transform docs, current v2 BaseEditor flat method surface, extension method recomposition, internal command registry, public-surface contracts, Tiptap CommandManager, and the accepted state/tx decision page. | Added Pass 5 unopinionated-core verdict; raised active score to 0.871; clarified that editor.commands / editor.chain() stay out of raw Slate and Plate owns product sugar. | None for Pass 5; migration proof remains. | Migration pass. |
| Migration pass | complete | Rechecked Plate table typed API/transform groups, link element config, mark transform sugar, image void/media transforms, Plate Yjs adapter APIs, the accepted state / tx decision, and current Slate v2 operation replay/commit contracts. | Added Pass 6 migration verdict; raised migration score to 0.88 and active score to 0.881; clarified Plate can keep editor.api / editor.tf as adapter sugar while raw Slate stays state / tx. | None for Pass 6; regression proof remains. | Regression pass. |
| Regression pass | complete | Rechecked generated operation-family contracts, inline void, markable inline void, block void, paste image void, editable island, large-document runtime void, table boundary navigation, search decoration focus, mouse toolbar selection, IME, replay artifacts, render profiler assertions, release-discipline scripts, public-surface hard-cut tests, and remaining old callback/hook names in tests/docs. | Added Pass 7 regression verdict; raised regression score to 0.87 and active score to 0.887; clarified that slate-browser owns regression families while examples stay demos. | Final callback, hook rename, onKeyDown(event, ctx), and state / tx namespace contracts still need final-name proof before closure. | Research pass. |
| Research pass | complete | Rechecked research index/log, accepted state / tx decision, cross-corpus steal/reject decision, runtime-owned shell DX decision, React 19.2 source page, read/update corpus ledger, Lexical read/update and active-context source, ProseMirror transaction and bookmark source, and Tiptap command/extension source. | Added Pass 8 research verdict; raised research score to 0.92 and active score to 0.890; confirmed no active plan claim relies on uncited external-source assertions. | None for Pass 8; simplicity pressure remains. | Simplicity pass. |
| Simplicity pass | complete | Rechecked editor lifecycle, mutation, extension, render, hook, keyboard, callback, product-sugar, alias, and escape-hatch surfaces against the one-public-path / one-advanced-escape-hatch rule. | Added Pass 9 simplicity verdict; cut raw onValueChange / onSelectionChange; closed raw editor.chain() as Plate/product-only; raised DX score to 0.92, composability/minimalism to 0.91, and active score to 0.895. | None for Pass 9; maintainer objections remain. | Slate maintainer pass. |
| Slate maintainer pass | complete | Rechallenged all ten objection rows as a skeptical Slate maintainer: state / tx, grouped APIs, extension namespaces, content-only void renderers, onKeyDown, onChange / onCommit, hook renames, schema/spec predicates, generated proof, and no aliases. | Added Pass 10 maintainer verdict; strengthened Row 6 for the callback cuts after Pass 9; raised DX score to 0.93, migration score to 0.89, and active score to 0.899. | None for Pass 10; ecosystem maintainer objections remain. | Ecosystem maintainer pass. |
| Ecosystem maintainer pass | complete | Rechecked Plate editor.api / editor.tf usage, table/image/Yjs plugins, type-test surfaces, slate-yjs init/collab contracts, plugin authoring pressure, app author void ergonomics, and generated browser proof ownership. | Added Pass 11 ecosystem verdict; made the Plate adapter layer, stale-target policy, plugin browser contract registry, and ecosystem TypeScript fixtures required; raised migration score to 0.91, regression score to 0.88, composability/minimalism to 0.92, and active score to 0.905. | None for Pass 11; revision pass must fold constraints into phases/gates. | Revision pass. |
| Revision pass | complete | Folded accepted maintainer/ecosystem constraints into the scorecard, Plate migration target, slate-yjs target, proof matrix, browser strategy, implementation phases, fast gates, open questions, and final completion gates. | Added revision verdict; made Plate adapter fixtures, stale-target policy, plugin browser contract registry, plugin void examples, ecosystem TypeScript fixtures, and slate-yjs remote commit/target proof core plan requirements; raised active score to 0.924. | None for revision; closure must verify every final gate. | Closure pass. |
| Closure score and final gates | complete | Verified score threshold, dimension floors, evidence citations, accepted objection rows, ecosystem/collab answers, implementation phases, final proof gates, pass-state ledger, and plan deltas. | Added closure verdict; set active score to 0.924; review lane is ready for complete-plan execution. | None. | Done. |
| Pass | Status | Evidence added | Plan delta | Open issues | Next owner |
|---|---|---|---|---|---|
| Current-state read and initial score | complete | Rechecked live renderVoid, callback names, hook alias greps, schema predicate greps, BaseEditor primitive method surface, first-party examples, write-boundary contracts, root runtime/event runtime, generated stress rows, plugin browser contract registry, collab runtime contracts, and research decision drift. | Reopened the plan from previous closure; set active score to 0.880; marked the older primitive-method research wording as superseded by the newer state/tx decision; recorded public write-DX drift as P1. | P1 public write DX mismatch remains: accepted target is tx.*, but live examples/tests still teach primitive editor.* writes inside update. | Research and live-source refresh pass. |
| Research and live-source refresh | complete | Rechecked live state / tx contract tests, primitive write-boundary tests, BaseEditor primitive method wiring, docs concepts for transforms/editor, dirty runtime-id impact code, and refreshed the state/tx research decision. | Added Pass 2 verdict and score 0.886; raised migration and research scores; recorded that tx.* is implemented but not yet the taught normal public write path. | P1 public write DX mismatch remains: choose tx.* as the only normal path and demote primitive editor writes, or revise the accepted API target. | Pressure passes. |
| DX/unopinionated-core public write pressure | complete | Rechecked live docs, API pages, examples/tests grep, BaseEditor primitive transform surface, state/tx contract tests, write-boundary tests, and the accepted state/tx research decision. | Added Pass 3 verdict and score 0.903; chose tx.* as the only normal public write path; classified primitive editor writes as advanced/internal bridge APIs. | Objection rows still need to answer maintainer/ecosystem pushback to the hard public write split. | Maintainer and ecosystem objection pass. |
| Remaining pressure bridge | complete | Rechecked whether the hardened write policy changes runtime performance, regression, research, or composability requirements. | No runtime pivot; added docs/examples/public-surface guard as the main new regression/composability requirement. | None separate from revision. | Revision pass. |
| Maintainer and ecosystem objection passes | complete | Challenged tx.*-only normal writes against legacy Slate transforms/commands, Plate editor.api / editor.tf, media/table plugins, slate-yjs plugin pressure, v2 extension namespace type fixtures, and collab replay contracts. | Added Pass 4 verdict and score 0.918; added objection Row 2b; kept primitive editor writes as advanced/internal bridge APIs only. | Revision pass must fold the objection answers into implementation phases and final gates. | Revision pass. |
| Revision pass | complete | Folded the public write-policy objection answers into the Public API Target, proof matrix, browser/release strategy, hard cuts, implementation phases, fast gates, and final gates. | Added Pass 5 verdict and score 0.923; made docs/examples/API tx.* guards an implementation and closure requirement. | None for revision; closure must verify final gates. | Closure pass. |
| Closure pass | complete | Verified threshold, dimension floors, evidence citations, pass-state ledger, objection answers, public API certainty, implementation phases, proof matrix, fast gates, and final gates. | Added Pass 6 closure verdict; set rerun status to done with score 0.923. | None. | Done. |
2026-04-28 second slate-review rerun deltas:
done to pending for a new review cycle.0.880.tx.* is the accepted
target, but examples/tests still teach primitive editor.* writes inside
editor.update.tx.* is
implemented while docs/examples still teach primitive editor.* writes.0.886, but kept completion pending because DX is
still below floor and the public write P1 remains.tx.* as the only normal public write path.0.903.tx.* as the normal public write path after checking legacy Slate,
Plate, slate-yjs, v2 extension namespaces, and collab replay.0.918.0.923.done.Pass 1 rerun deltas:
done to pending.0.928 score to candidate closure target only.0.806.Pass 2 rerun deltas:
docs/research/sources/editor-architecture/react-19-2-external-store-and-background-ui.md
against the official React 19.2 release page.state / tx namespace
decision.0.806 to 0.822.Pass 3 rerun deltas:
focused / selected props as a hard closure blocker.0.822 to 0.841.Pass 4 rerun deltas:
state / tx, target, onKeyDown, onChange, and content-only
renderVoid as final names pending maintainer review.api / tf, public onKeyCommand, public onSnapshotChange,
RenderVoidPropsFor, and app-owned void shell props.0.841 to 0.859.Pass 5 rerun deltas:
editor.commands, editor.chain(), or
chain-first toolbar ceremony.editor.update.state / tx namespaces as the clean replacement
for direct extension method recomposition onto the editor object.0.859 to 0.871.Pass 6 rerun deltas:
editor.api / editor.tf as adapter sugar, not raw Slate
terminology.state / tx extension namespaces are enough for Plate if the
type system preserves inferred plugin groups.0.871 to 0.881.Pass 7 rerun deltas:
onKeyDown(event, ctx), and final state / tx namespace contracts.0.881 to 0.887.Pass 8 rerun deltas:
state / tx namespace decision.0.887 to 0.890.Pass 9 rerun deltas:
onChange plus advanced onCommit.onValueChange and onSelectionChange to Plate/app adapter filters
over onChange.editor.chain() as Plate/product sugar only, not raw Slate.0.890 to 0.895.Pass 10 rerun deltas:
onChange plus advanced onCommit, no raw onValueChange /
onSelectionChange.state / tx type
fixtures.0.895 to 0.899.Pass 11 rerun deltas:
state / tx; editor.api / editor.tf stay outside raw
Slate and are Plate-owned if Plate builds product sugar later.0.899 to 0.905.Revision pass deltas:
EditorTarget policy to slate-yjs target, proof matrix, phases,
open questions, and final gates.0.905 to 0.924.Closure pass deltas:
slate-review completion threshold.0.924.done.Accepted decisions after revision:
Added decisions:
state / tx naming is accepted.tx includes read groups.state / tx namespaces.editor.schema owns predicate access.Revised decisions:
onKeyDown and cuts public
onKeyCommand.onChange and onCommit, not onSnapshotChange.onValueChange and onSelectionChange; Plate/adapters can
reintroduce them as filters over onChange.renderVoid is element + target only.Dropped decisions:
api / tf names for raw Slate.actions for void renderers.editor.chain() in raw Slate.Strengthened acceptance criteria:
EditorTarget behavior is specified and covered for local and remote
changesNo-change decisions:
editor.read / editor.updateNo blocking open question remains for planning.
Non-blocking implementation choices:
Closed implementation choice:
editor.chain() belongs to Plate/product sugar only, not raw Slate.EditorTarget is a local runtime handle; it can rebase or fail softly, but it
is not serialized collaboration identity.editor.api / editor.tf are Plate adapter names only.What would change the decision:
state / tx namespaces cannot preserve plugin
inferenceOwner: packages/slate.
EditorStateView and EditorTransactionVieweditor.update fail in development/testOwner: packages/slate.
state and tx namespacesmethods registrationstate.<plugin> / tx.<plugin> augmentationOwner: packages/slate-react.
renderVoid props with element + targetfocused / selected subscriptionsEditorTarget runtime behavior: rebase when possible, fail
softly when invalid, never serialize target identityuseEditor, useElementSelected, useEditorFocused, and related
renamed hooksRenderVoidPropsFor casts from examplesOwner: packages/slate-react.
onKeyCommandonKeyDown(event, ctx) carry handled/repair return contractonSnapshotChange with onChange and onCommitonValueChange and onSelectionChange; adapters can filter
onChangeOwner: packages/slate and packages/slate-react.
editor.schemarenderShellUnsafe users to register browser contracts for owned DOM
shell behaviorOwner: raw Slate contracts.
editor.update unless
they go through txOwner: packages/slate-browser and Playwright suite.
tx.*, and
primitive editor write usages must be classifiedtest:stress, and bun check:fullDuring implementation:
bun --filter slate test
bun --filter slate-react test:vitest
bun --filter slate-browser test:core
bun --filter slate-react typecheck
bun --filter slate-browser typecheck
bun typecheck:site
bun lint
Type/API proof:
bun test:release-discipline
bun typecheck:packages
bun typecheck:site
bun --filter slate-browser test:core
cd /Users/zbeyens/git/plate-2 && pnpm test:types
Public write-surface proof:
bun test:release-discipline
rg "editor\\.update\\(\\(\\) =>|editor\\.(setNodes|insertText|insertNodes|removeNodes|select)\\(" \
docs site/examples/ts packages/slate-react/src \
-g '!**/dist/**'
The grep is diagnostic. The release-discipline guard owns the allowlist for classified core/runtime/legacy transform fixtures.
Focused browser:
PLAYWRIGHT_RETRIES=0 PLAYWRIGHT_WORKERS=1 bun playwright \
playwright/integration/examples/images.test.ts \
playwright/integration/examples/embeds.test.ts \
playwright/integration/examples/editable-voids.test.ts \
playwright/integration/examples/mentions.test.ts \
--project=chromium
Plugin browser contracts:
PLUGIN_CONTRACTS=table,media,link,mark,editable-island \
PLAYWRIGHT_RETRIES=0 bun test:stress
Stress:
STRESS_FAMILIES=inline-void-boundary-navigation,markable-inline-void-formatting,block-void-navigation,editable-island-native-focus \
PLAYWRIGHT_RETRIES=0 bun test:stress
Release closure:
bun check:full
The implementation is not complete until all gates pass:
onKeyCommandonSnapshotChangeonValueChange / onSelectionChangeactions in RenderVoidPropsfocused / selected in RenderVoidPropsRenderVoidPropsFor casts in first-party examplesuseSlateStatic, useSelected, and useFocused aliases removed before
publisheditor.update fail in development/testeditor.update((tx) => tx.*), not
editor.update(() => editor.*)applyOperations remains the explicit replay writer and is not treated as
normal authoring DXtx see transaction-local statestate / tx namespaces typecheck with plugin augmentationeditor.schema is the documented predicate surfacestate / txeditor.updateEditorTarget handles rebase or fail softly; they are never serialized
collaboration identityonChange / onCommitrenderShellUnsafe examples carry explicit browser contractsbun check:full passesStatus: complete for tracer, lane still pending.
Implemented in /Users/zbeyens/git/slate-v2:
editor.read((state) => ...) receives grouped read state.editor.update((tx) => ...) receives grouped tx write/read methods.tx reads observe same-update draft mutations.Files changed:
/Users/zbeyens/git/slate-v2/packages/slate/src/interfaces/editor.ts/Users/zbeyens/git/slate-v2/packages/slate/src/core/public-state.ts/Users/zbeyens/git/slate-v2/packages/slate/src/create-editor.ts/Users/zbeyens/git/slate-v2/packages/slate/test/state-tx-public-api-contract.ts/Users/zbeyens/git/slate-v2/packages/slate/test/escape-hatch-inventory-contract.tsVerification:
bun test ./packages/slate/test/state-tx-public-api-contract.ts
bun test ./packages/slate/test/read-update-contract.ts ./packages/slate/test/write-boundary-contract.ts ./packages/slate/test/transaction-contract.ts ./packages/slate/test/state-tx-public-api-contract.ts
bun test ./packages/slate/test/transaction-target-runtime-contract.ts ./packages/slate/test/generic-editor-api-contract.ts ./packages/slate/test/surface-contract.ts
bun test:release-discipline
bun lint:fix
bun typecheck:packages
Notes:
bun --filter slate test does not match a package in this checkout; focused
package contracts were run directly.bun typecheck:packages failed on tx generic variance. The accepted
fix keeps read value generics precise and widens tx write parameter types so
Editor<CustomValue> still flows through runtime helpers.Next owner:
state.<extension> /
tx.<extension> group composition, collision proof, cleanup, and type
fixtures.Status: complete for runtime tracer, lane still pending for TypeScript augmentation fixtures.
Implemented in /Users/zbeyens/git/slate-v2:
state / tx groups register into runtime namespace mapsmethods are no longer a public or internal registration
pathmethods are rejected before editor
mutationmethodNames and editor-object method recomposition were removedFiles changed:
/Users/zbeyens/git/slate-v2/packages/slate/src/interfaces/editor.ts/Users/zbeyens/git/slate-v2/packages/slate/src/core/public-state.ts/Users/zbeyens/git/slate-v2/packages/slate/src/core/extension-registry.ts/Users/zbeyens/git/slate-v2/packages/slate/src/core/editor-extension.ts/Users/zbeyens/git/slate-v2/packages/slate/test/extension-namespaces-contract.ts/Users/zbeyens/git/slate-v2/packages/slate/test/extension-methods-contract.ts/Users/zbeyens/git/slate-v2/packages/slate/test/extension-contract.ts/Users/zbeyens/git/slate-v2/packages/slate/test/escape-hatch-inventory-contract.tsVerification:
bun test ./packages/slate/test/extension-namespaces-contract.ts
bun test ./packages/slate/test/extension-methods-contract.ts ./packages/slate/test/extension-namespaces-contract.ts ./packages/slate/test/generic-extension-contract.ts ./packages/slate/test/extension-contract.ts ./packages/slate/test/state-tx-public-api-contract.ts ./packages/slate/test/read-update-contract.ts
bun typecheck:packages
bun lint:fix
bun test:release-discipline
Notes:
EditorExtension.methods was cut instead of renamed. Plugin sugar can exist
above raw Slate, but the raw runtime extension surface is grouped state /
tx.Next owner:
state.<plugin> /
tx.<plugin> groups, nested group shapes, collision errors, composed
inference, and module augmentation.Status: complete for Phase 1 type surface, lane still pending for React/API cuts.
Implemented in /Users/zbeyens/git/slate-v2:
EditorStateExtensionGroups<V> and EditorTxExtensionGroups<V> are public
module-augmentation slotsEditorStateView<V> and EditorUpdateTransaction<V> expose augmented
extension groups while the runtime builders satisfy non-augmented core view
types firststate.<plugin> groupstx.<plugin> groupsFiles changed:
/Users/zbeyens/git/slate-v2/packages/slate/src/interfaces/editor.ts/Users/zbeyens/git/slate-v2/packages/slate/src/core/public-state.ts/Users/zbeyens/git/slate-v2/packages/slate/test/generic-extension-namespace-contract.ts/Users/zbeyens/git/slate-v2/packages/slate/test/tsconfig.generic-types.jsonVerification:
bunx tsc --project packages/slate/test/tsconfig.generic-types.json --noEmit
bun test ./packages/slate/test/extension-methods-contract.ts ./packages/slate/test/extension-namespaces-contract.ts ./packages/slate/test/generic-extension-contract.ts ./packages/slate/test/extension-contract.ts ./packages/slate/test/state-tx-public-api-contract.ts ./packages/slate/test/read-update-contract.ts
bun typecheck:packages
bun lint:fix
bun test:release-discipline
Notes:
Next owner:
RenderVoidProps becomes
{ element, target }, eager focused / selected / actions leave the
default render path, and first-party void renderers move to opt-in
node-scoped hooks/selectors.Status: complete for RenderVoidProps / first-party examples, lane still
pending for callback and keyboard API cuts.
Implemented in /Users/zbeyens/git/slate-v2:
RenderVoidProps<T> is { element: T; target: Path }focused, selected, or actionsuseFocused() / useSelected() only inside
components that draw selected/focused UIeditor.removeNodes / editor.setNodes with the
supplied targetRenderVoidPropsFor and casts disappeared from first-party examplesdefineEditorExtension({ methods }) usage was removedmethods teaching instead of
requiring the removed API pathFiles changed:
/Users/zbeyens/git/slate-v2/packages/slate-react/src/components/editable-text-blocks.tsx/Users/zbeyens/git/slate-v2/packages/slate-react/test/surface-contract.tsx/Users/zbeyens/git/slate-v2/packages/slate/test/public-surface-contract.ts/Users/zbeyens/git/slate-v2/site/examples/ts/custom-types.d.ts/Users/zbeyens/git/slate-v2/site/examples/ts/images.tsx/Users/zbeyens/git/slate-v2/site/examples/ts/embeds.tsx/Users/zbeyens/git/slate-v2/site/examples/ts/paste-html.tsx/Users/zbeyens/git/slate-v2/site/examples/ts/mentions.tsx/Users/zbeyens/git/slate-v2/site/examples/ts/editable-voids.tsx/Users/zbeyens/git/slate-v2/site/examples/ts/inlines.tsx/Users/zbeyens/git/slate-v2/site/examples/ts/forced-layout.tsx/Users/zbeyens/git/slate-v2/site/examples/ts/large-document-runtime.tsxVerification:
bun --filter slate-react typecheck
bun --filter slate-react test:vitest
bun --filter slate build
bun typecheck:site
bun typecheck:packages
bun lint:fix
bun test:release-discipline
rg "RenderVoidPropsFor|as RenderVoidProps|actions\\." site/examples/ts packages/slate-react/src packages/slate-react/test
Notes:
bun typecheck:site initially resolved stale slate/dist declarations with
the previous zero-arg read signature. Rebuilding slate fixed the artifact
path and the site checker passed.Next owner:
onSnapshotChange,
expose polished onChange / onCommit, and replace public onKeyCommand
with Slate-style onKeyDown(event, ctx) handled-result semantics.Status: complete for callback / keyboard naming, lane still pending for hook renames and schema/spec cleanup.
Implemented in /Users/zbeyens/git/slate-v2:
<Slate> exposes value-only onChange(value) and advanced
onCommit(commit, snapshot)onSnapshotChange, onValueChange, and onSelectionChange are gone
from source, tests, and first-party examples<Editable> exposes onKeyDown(event, { editor })onKeyCommand and EditableKeyCommandHandler are gone from source,
tests, and first-party examplesonKeyDownonCommitonChange, value
commits call onChange, and onCommit receives commit telemetryonKeyDown receives editor context and can execute a
model command with handled-result semanticsFiles changed:
/Users/zbeyens/git/slate-v2/packages/slate-react/src/components/slate.tsx/Users/zbeyens/git/slate-v2/packages/slate-react/src/components/editable.tsx/Users/zbeyens/git/slate-v2/packages/slate-react/src/components/editable-text-blocks.tsx/Users/zbeyens/git/slate-v2/packages/slate-react/src/editable/keyboard-input-strategy.ts/Users/zbeyens/git/slate-v2/packages/slate-react/src/editable/runtime-keyboard-events.ts/Users/zbeyens/git/slate-v2/packages/slate-react/src/editable/runtime-event-engine.ts/Users/zbeyens/git/slate-v2/packages/slate-react/src/editable/runtime-root-engine.ts/Users/zbeyens/git/slate-v2/packages/slate-react/src/index.ts/Users/zbeyens/git/slate-v2/packages/slate-react/test/editable-behavior.tsx/Users/zbeyens/git/slate-v2/packages/slate-react/test/react-editor-contract.tsx/Users/zbeyens/git/slate-v2/site/examples/ts/images.tsx/Users/zbeyens/git/slate-v2/site/examples/ts/inlines.tsx/Users/zbeyens/git/slate-v2/site/examples/ts/tables.tsx/Users/zbeyens/git/slate-v2/site/examples/ts/check-lists.tsx/Users/zbeyens/git/slate-v2/site/examples/ts/markdown-shortcuts.tsx/Users/zbeyens/git/slate-v2/site/examples/ts/richtext.tsx/Users/zbeyens/git/slate-v2/site/examples/ts/mentions.tsxVerification:
bun --filter slate-react test:vitest -- editable-behavior react-editor-contract
bun --filter slate-react test:vitest
bun --filter slate-react typecheck
bun typecheck:site
bun typecheck:packages
bun test:release-discipline
bun lint:fix
rg "onKeyCommand|EditableKeyCommandHandler|onSnapshotChange|onValueChange|onSelectionChange" packages/slate-react/src packages/slate-react/test site/examples/ts packages/slate/test -g '!**/dist/**'
Notes:
onSelectionChange grep hit is the internal DOM
selectionchange event listener in selection-reconciler.ts; it is not a
public Slate callback.onChange / onCommit.isContentEditable property so
the event follows the same editable-target branch as browsers.Next owner:
useEditor,
useElementSelected, useEditorFocused, useEditorReadOnly,
useEditorComposing, and useEditorSelector; cut the old public hook
aliases before publish.Status: complete for public hook aliases, lane still pending for schema/spec predicates and browser proof.
Implemented in /Users/zbeyens/git/slate-v2:
useSlateStatic became useEditoruseSelected became useElementSelecteduseFocused became useEditorFocuseduseReadOnly became useEditorReadOnlyuseComposing became useEditorComposinguseSlateSelector became useEditorSelectoruseSlateSelection became useEditorSelectionuseSlate / useSlateWithV were deleted instead of renameduseEditor plus useEditorSelector
for reactive button stateuseElementSelectedslate-react source, tests, and first-party
examplesFiles changed:
/Users/zbeyens/git/slate-v2/packages/slate-react/src/hooks/use-editor.tsx/Users/zbeyens/git/slate-v2/packages/slate-react/src/hooks/use-element-selected.ts/Users/zbeyens/git/slate-v2/packages/slate-react/src/hooks/use-editor-focused.ts/Users/zbeyens/git/slate-v2/packages/slate-react/src/hooks/use-editor-read-only.ts/Users/zbeyens/git/slate-v2/packages/slate-react/src/hooks/use-editor-composing.ts/Users/zbeyens/git/slate-v2/packages/slate-react/src/hooks/use-editor-selector.tsx/Users/zbeyens/git/slate-v2/packages/slate-react/src/hooks/use-editor-selection.tsx/Users/zbeyens/git/slate-v2/packages/slate-react/src/hooks/use-slate.tsx/Users/zbeyens/git/slate-v2/packages/slate-react/src/index.ts/Users/zbeyens/git/slate-v2/packages/slate-react/test/provider-hooks-contract.tsx/Users/zbeyens/git/slate-v2/packages/slate-react/test/use-element-selected.test.tsx/Users/zbeyens/git/slate-v2/site/examples/tsVerification:
bun --filter slate-react test:vitest -- provider-hooks-contract use-element-selected surface-contract
bun --filter slate-react test:vitest
bun --filter slate-react typecheck
bun typecheck:site
bun typecheck:packages
bun test:release-discipline
bun lint:fix
rg "\\buseSlateStatic\\b|\\buseSelected\\b|\\buseFocused\\b|\\buseReadOnly\\b|\\buseComposing\\b|\\buseSlateSelector\\b|\\buseSlateSelection\\b|\\buseSlate\\b|\\buseSlateWithV\\b" packages/slate-react/src packages/slate-react/test site/examples/ts packages/slate/test -g '!**/dist/**'
Notes:
useSlateNodeRef, useSlateProjections, annotation, and widget hooks still
carry Slate-domain names because they are not the confusing editor-state hook
aliases this phase cuts.Next owner:
editor.isInline, editor.isVoid, editor.markableVoid, and
editor.isSelectable monkeypatching with editor.schema / element specs,
while keeping manual predicate overrides as advanced extension policy.Status: complete for first-party schema/spec predicates, lane still pending for Plate/slate-yjs proof and browser parity.
Implemented in /Users/zbeyens/git/slate-v2:
editor.schemaeditor.schema.define(...)elements specsstate.schema and tx.schema expose read-only schema queriesschema.define is intentionally unavailable from state.schema /
tx.schemavoid: 'block' | 'inline' | 'markable-inline' | 'editable-island' drives
default isVoid, isInline, and markableVoid policyselectable: false and readOnly: true drive selectable/read-only policyonSelectionChange listener variable was renamed to
handleNativeSelectionChange so public-callback hard-cut greps are cleanFiles changed:
/Users/zbeyens/git/slate-v2/packages/slate/src/interfaces/editor.ts/Users/zbeyens/git/slate-v2/packages/slate/src/create-editor.ts/Users/zbeyens/git/slate-v2/packages/slate/src/core/public-state.ts/Users/zbeyens/git/slate-v2/packages/slate/src/core/extension-registry.ts/Users/zbeyens/git/slate-v2/packages/slate/src/core/editor-extension.ts/Users/zbeyens/git/slate-v2/packages/slate/test/schema-contract.ts/Users/zbeyens/git/slate-v2/packages/slate-react/src/editable/selection-reconciler.ts/Users/zbeyens/git/slate-v2/site/examples/ts/images.tsx/Users/zbeyens/git/slate-v2/site/examples/ts/embeds.tsx/Users/zbeyens/git/slate-v2/site/examples/ts/editable-voids.tsx/Users/zbeyens/git/slate-v2/site/examples/ts/inlines.tsx/Users/zbeyens/git/slate-v2/site/examples/ts/paste-html.tsx/Users/zbeyens/git/slate-v2/site/examples/ts/large-document-runtime.tsx/Users/zbeyens/git/slate-v2/site/examples/ts/mentions.tsxVerification:
bun test ./packages/slate/test/schema-contract.ts
bun test ./packages/slate/test/schema-contract.ts ./packages/slate/test/state-tx-public-api-contract.ts ./packages/slate/test/extension-namespaces-contract.ts ./packages/slate/test/extension-contract.ts
bun --filter slate build
bun typecheck:site
bun typecheck:packages
bun --filter slate-react typecheck
bun --filter slate-react test:vitest
bun test:release-discipline
bun lint:fix
rg "editor\\.isInline\\s*=|editor\\.isVoid\\s*=|editor\\.markableVoid\\s*=|editor\\.isSelectable\\s*=|editor\\.isElementReadOnly\\s*=|nextIsInline|nextIsVoid|nextIsSelectable|nextMarkableVoid|nextIsElementReadOnly" site/examples/ts -g '!**/dist/**'
rg "onKeyCommand|EditableKeyCommandHandler|onSnapshotChange|onValueChange|onSelectionChange|state\\.schema\\.define|tx\\.schema\\.define|\\buseSlateStatic\\b|\\buseSelected\\b|\\buseFocused\\b|\\buseReadOnly\\b|\\buseComposing\\b|\\buseSlateSelector\\b|\\buseSlateSelection\\b|\\buseSlate\\b|\\buseSlateWithV\\b" packages/slate-react/src packages/slate-react/test site/examples/ts packages/slate/test/schema-contract.ts packages/slate/test/state-tx-public-api-contract.ts -g '!**/dist/**'
Notes:
bun typecheck:site initially failed against stale slate/dist
declarations. bun --filter slate build refreshed the declarations and the
site checker passed.state.schema.define and tx.schema.define were cut during review of this
slice because read/update views should not mutate global schema policy.Next owner:
state / tx
groups, deterministic operation replay, remote commit metadata, and
local-only target behavior. No current Plate or slate-yjs adapter support.Status: complete for raw Slate migration-backbone contracts, lane still pending for browser parity and release proof.
Implemented in /Users/zbeyens/git/slate-v2:
state.table.* and tx.table.* groupstx.value.get() after
the group mutates the documentFiles changed:
/Users/zbeyens/git/slate-v2/packages/slate/test/extension-namespaces-contract.ts/Users/zbeyens/git/slate-v2/packages/slate/test/collab-history-runtime-contract.ts/Users/zbeyens/git/slate-v2/packages/slate/test/generic-extension-namespace-contract.tsVerification:
bunx tsc --project packages/slate/test/tsconfig.generic-types.json --noEmit
bun test ./packages/slate/test/extension-namespaces-contract.ts ./packages/slate/test/collab-history-runtime-contract.ts
bun test ./packages/slate/test/extension-methods-contract.ts ./packages/slate/test/generic-extension-contract.ts ./packages/slate/test/extension-contract.ts ./packages/slate/test/extension-namespaces-contract.ts ./packages/slate/test/state-tx-public-api-contract.ts ./packages/slate/test/read-update-contract.ts ./packages/slate/test/collab-history-runtime-contract.ts ./packages/slate/test/apply-onchange-hard-cut-contract.ts
bun test:release-discipline
bun typecheck:packages
bun lint:fix
Notes:
state.nodes.children([]) for root-row counts. Root document rows are
document value reads: state.value.get() and tx.value.get().Next owner:
Status: complete. The accepted architecture/DX hard-cut lane is done.
Implemented in /Users/zbeyens/git/slate-v2:
slate-browser plugin contract registry for generated browser rowsapplyOperationsstale-target-remote-rebase stress rownullFiles changed:
/Users/zbeyens/git/slate-v2/packages/slate-browser/src/playwright/index.ts/Users/zbeyens/git/slate-v2/packages/slate-browser/test/core/scenario.test.ts/Users/zbeyens/git/slate-v2/packages/slate-react/src/editable/browser-handle.ts/Users/zbeyens/git/slate-v2/packages/slate-react/test/kernel-authority-audit-contract.ts/Users/zbeyens/git/slate-v2/playwright/stress/generated-editing.test.tsVerification:
bun --filter slate-browser test:core
bun --filter slate-browser typecheck
bun --filter slate-browser build
bunx tsc --project playwright/tsconfig.json --noEmit
STRESS_FAMILIES=table-cell-boundary-navigation PLAYWRIGHT_RETRIES=0 PLAYWRIGHT_WORKERS=1 bun test:stress
STRESS_FAMILIES=stale-target-remote-rebase PLAYWRIGHT_RETRIES=0 PLAYWRIGHT_WORKERS=1 bun test:stress
bun --filter slate-react test:vitest -- kernel-authority-audit-contract
bun --filter slate-react typecheck
bun typecheck:packages
bun test:release-discipline
bun lint:fix
bun check:full
PLAYWRIGHT_RETRIES=0 bun run playwright playwright/integration/examples/richtext.test.ts playwright/integration/examples/inlines.test.ts --project=chromium --project=firefox -g "persistent native word-delete|generated inline cut typing gauntlet"
Notes:
bun check:full exited 0. It reported two retry-resolved browser flakes:
Chromium richtext persistent native word-delete and Firefox inline cut typing
gauntlet.Next owner:
Status: complete.
Reason:
slate-review rerun closed with one accepted implementation owner:
first-party authoring docs, examples, walkthroughs, and public API pages must
teach editor.update((tx) => tx.*), not primitive editor.* writes.Scope:
.tmp/slate-v2.active goal state, active goal state, and
this plan ledger.Next owner:
.tmp/slate-v2
so it fails on unclassified first-party primitive editor.* write teaching.tx.*.editor.* write usage as
advanced/internal bridge, core/runtime, codemod, or legacy transform fixture
usage.Driver gates:
bun test:release-discipline
rg "editor\\.update\\(\\(\\) =>|editor\\.(setNodes|insertText|insertNodes|removeNodes|select)\\(" docs site/examples/ts packages/slate-react/src -g '!**/dist/**'
Completion:
active goal state is done; the guard and targeted proof passed.Completed in /Users/zbeyens/git/slate-v2:
packages/slate/test/public-surface-contract.ts.editor.* writes instead of tx.*.site/examples/ts/forced-layout.tsxdocs/concepts/11-normalizing.mddocs/walkthroughs/07-enabling-collaborative-editing.mdVerification:
bun test ./packages/slate/test/public-surface-contract.ts --bail 1
bun test:release-discipline
bun typecheck:site
bun typecheck:packages
bun lint:fix
bun test:release-discipline
bun typecheck:site
rg "editor\\.(collapse|delete|deselect|insertFragment|insertNodes|insertText|mergeNodes|move|moveNodes|removeNodes|select|setNodes|splitNodes|unsetNodes|unwrapNodes|wrapNodes)\\(" docs/api docs/concepts docs/walkthroughs site/examples/ts -g '!**/dist/**'
PLAYWRIGHT_RETRIES=0 PLAYWRIGHT_WORKERS=1 bun playwright playwright/integration/examples/images.test.ts playwright/integration/examples/inlines.test.ts --project=chromium
Final verification result:
Next owner: