Back to Plate

Slate v2 Non-Node Editor State Architecture Ralplan

docs/plans/2026-05-20-slate-v2-non-node-editor-state-architecture-ralplan.md

53.0.6184.6 KB
Original Source

Slate v2 Non-Node Editor State Architecture Ralplan

status: done created: 2026-05-20 closed: 2026-05-21 reopened: 2026-05-21 completion_id: 019e3627-238b-7993-a8cf-26be45504c47 current_pass: verification-sweep-pass-selection-lifecycle-frame current_pass_status: complete next_pass: none previous_score: 0.92 score: 0.86 target_score: 0.94 final_handoff_status: complete ralplan_lane_status: complete

Current Verdict

Hard take: do not store document title, document settings, comments, or non-content metadata as invisible Slate nodes. That repeats the legacy editor.children trap: every durable thing becomes fake editable content, and selection/rendering/history/collab all start lying.

Best architecture after the latest API refresh:

  1. Use canonical Value = { roots, state? } in the runtime and persistence layer.
  2. Keep the 99% single-root authoring path ergonomic with initialValue: { children, state? }, normalized to roots.main.
  3. Add atom-like state fields for persisted non-node document state.
  4. Keep root identity on content locations and committed operations, not inside numeric Slate paths.
  5. Commit state field writes through a first-class, transaction-aware statePatches channel.
  6. Keep comments and product annotations in external anchored channels by default.
  7. Treat editable header/footer/global regions as content roots, not as state fields.
  8. Treat multi-editor shared history as one shared document runtime with multiple root-bound views, never shared node object identity.

Jotai reopen verdict: a "single flexible store" is correct only as one editor state container/registry. It is wrong as one arbitrary object blob. Jotai's good idea is one immutable descriptor per independent value, with granular reads, writes, equality, persistence, and subscription. Slate should expose that as state fields, not as public atom naming and not as multi-store-first DX.

Naming note: this pass supersedes the previous defineEditorStateStore and defineEditorStateField naming. The current public descriptor factory is defineStateField. Older plan sections that still say state store, initialDocument, state.fields, or tx.fields are historical unless the section explicitly says it is current authority.

Latest API refresh, 2026-05-20: the previous closeout is reopened because the accepted API moved from conservative initialDocument / Value = children compatibility to canonical Value = { roots, state? }, input normalization, rooted operations, and document runtime/editor view separation. This activation refreshes authority and syncs issue/reference/proof rows with no issue-count claim change.

Ralplan compliance reopen, 2026-05-20: the previous closeout batched multiple state-field follow-up passes and wrote top-level done. That was wrong. The reopened lane may only close after required follow-up passes and a fresh final-gates pass. The latest final-gates pass is now complete.

Runtime provider reopen, 2026-05-21: the source implementation has core runtime/view APIs, but the React public provider shape is not good enough for multi-root examples yet. Live source shows:

  • core already has createEditorRuntime and createEditorView in .tmp/slate-v2/packages/slate/src/editor-runtime-view.ts;
  • createEditorView currently exposes root/read-only/focus policy over one runtime, but it is core-only and not a React provider API;
  • useSlateEditor still creates one React editor from createReactEditor in .tmp/slate-v2/packages/slate-react/src/hooks/use-slate-editor.ts;
  • <Slate> still takes editor and creates one editor context plus one selector bus in .tmp/slate-v2/packages/slate-react/src/components/slate.tsx;
  • no public SlateRuntime, useSlateRuntime, useSlateViewState, or <Slate root="..."> API exists in live slate-react;
  • the current Document State example is correctly single-root and should stay focused on state fields.

Current verdict for the new pass: add an optional common React runtime provider for multi-view documents, but keep <Slate> as the public view provider. Do not expose SlateViewProvider as public API. It is an internal implementation name at most.

This is a public API and data-model change. It needs a real plan before implementation.

Cursor-selection drift reopen, 2026-05-21: the state/runtime direction remains right, but the selection architecture is not yet absolute-best. The live source has good concepts, yet root stamping, DOM import, DOM export, repair ownership, history focus policy, and browser proof are still distributed across too many owners. The next architecture target is a focused selection-authority consolidation, not a whole-editor rewrite and not a pivot to ProseMirror, Lexical, or Tiptap.

React lifecycle amendment, 2026-05-21: after comparing ../react-prosemirror, the selection-authority target also needs a React lifecycle access gate. Slate should not let arbitrary render/effect code read a stale view or DOM selection and then import it into model state. React-side selection access must happen through runtime-owned event callbacks or post-commit layout effects.

Multi-root history regression amendment, 2026-05-21: content undo/redo must preserve root identity while preparing batch.selectionBefore, but selection restore is scoped to the invoking view root. If the active body view undoes a header-owned batch, header content changes and the body caret stays where it is. If the active header view undoes a header-owned batch, the header selection is restored. The fix is not an example-only focus hack: slate-history must keep rooted batch selections, then skip cross-root selection replay instead of projecting that selection into the active root. Example toolbar history runs through the active root view and refocuses that editable while preserving its DOM range; title/state-only history keeps opting out of DOM focus changes.

Final Architecture Authority

This section is the current source of truth for the architecture. Older store-first, initialDocument, EditorDocumentSnapshot, state.fields, and tx.fields wording in dated pass notes is superseded unless it clearly refers to external/product stores such as comments, annotations, presence, or service-owned metadata.

Public API target:

ts
const documentTitle = defineStateField({
  key: 'document.title',
  collab: 'shared',
  initial: () => 'Untitled',
  history: 'push',
  persist: true,
})

const spellcheck = defineStateField({
  key: 'document.settings.spellcheck',
  collab: 'shared',
  initial: () => true,
  history: 'push',
  persist: true,
})

const editor = createEditor({
  extensions: [documentTitle, spellcheck],
  initialValue: {
    children,
    state: {
      [documentTitle.key]: 'Q2 Plan',
      [spellcheck.key]: true,
    },
  },
})

const title = editor.read((state) => state.getField(documentTitle))

editor.update((tx) => {
  tx.setField(documentTitle, 'Q3 Plan')
})

React DX target:

ts
const title = useStateFieldValue(documentTitle)
const setTitle = useSetStateField(documentTitle)

React multi-root DX target:

tsx
const runtime = useSlateRuntime({
  extensions: [documentTitle, spellcheck],
  initialValue: {
    roots: { header, main, footer },
    state: {
      [documentTitle.key]: 'Q2 Plan',
      [spellcheck.key]: true,
    },
  },
})

<SlateRuntime runtime={runtime}>
  <Slate root="header">
    <Editable aria-label="Header" />
  </Slate>

  <Slate root="main">
    <Editable aria-label="Body" />
  </Slate>

  <Slate root="footer">
    <Editable aria-label="Footer" />
  </Slate>
</SlateRuntime>

Provider naming decision:

  • SlateRuntime is the optional common provider for shared value, history, collab, state fields, and runtime selector bus.
  • Slate remains the public view provider. In a SlateRuntime, root selects the view root. Outside a SlateRuntime, Slate editor={editor} remains the single-editor path.
  • SlateViewProvider is not public API. If implementation needs that name internally, keep it unexported.

Cross-view hook target:

ts
const editor = useEditor()
const currentRoot = useEditorState((state) => state.view.root())

const title = useSlateRuntimeState((state) => state.getField(documentTitle))

const headerText = useSlateViewState('header', (state) =>
  state.text.string([])
)

Hook boundary:

  • useEditor and useEditorState read the current <Slate> view.
  • useSlateRuntime creates or returns the shared runtime.
  • useSlateRuntimeState subscribes to runtime-level data such as state fields, history, collab export state, and cross-document counters.
  • useSlateViewState(root, selector) subscribes to another root view through the shared runtime selector bus, not by reaching into a sibling React provider.
  • Cross-view hooks must be unavailable without SlateRuntime; throwing there is better than silently binding to the wrong editor.

Internal target:

  • canonical runtime/persisted value: Value = { roots: Record<RootKey, Element[]>, state?: Record<string, unknown> }.
  • input convenience only: InitialValue = Element[] | { children: Element[]; state? } | { roots; state? }.
  • single-root input normalizes to { roots: { main: children }, state }.
  • one document-runtime-owned state container.
  • many immutable field descriptors.
  • content operations are root-explicit while Path remains numeric and root-local.
  • Point / Range are root-aware.
  • EditorCommit.statePatches.
  • EditorCommit.dirtyStateKeys.
  • EditorCommitSource literal 'state'.
  • EditorRuntime owns value, roots, state fields, operations, history, and collaboration.
  • editor views bind a runtime to one root and own selection, DOM bridge, focus, and read-only state.
  • React runtime provider owns the shared editor.subscribe bridge and selector bus once per runtime. Nested <Slate root> providers derive root-bound views from that runtime.
  • A single <Slate editor> remains valid and should internally behave like the one-view shortcut, not force users into a runtime provider for normal docs.
  • Sibling view subscriptions go through the runtime selector bus. They must not depend on sibling provider lookup, prop-drilled editors, or shared node object identity.
  • descriptor-level persist, history, collab, equals, serialize, deserialize, and optional diff/applyPatch/invertPatch.
  • history and collab use string shorthands in normal examples. Object policies are escape hatches for future policy metadata, not required boilerplate.

Naming rules:

  • Use state field for durable non-node editor/document values.
  • Use external store only for product-owned services such as comments, annotations, presence, permissions, and audit trails.
  • Do not expose public atom naming in raw Slate; the design is atom-inspired, not Jotai-branded.
  • Do not collapse durable state into one arbitrary object blob. One field per independently persisted/subscribed value is the default DX.
  • Do not expose runtime Value as a union. Union shapes are only accepted at the creation/input boundary.
  • Do not nest state under roots by default. Per-root state is a field policy, not a separate root-owned state container.

Intent And Boundary Record

  • intent: support durable editor/document state that is not visible contiguous body content, without abusing hidden nodes.
  • desired outcome: title/settings/document metadata can be persisted, serialized, collaborated, and optionally included in history with the same transaction discipline as body edits.
  • in scope: document title, document settings, state field descriptors, history policy, persistence, collab routing, dirty/source subscriptions, comments as external anchors, multi-root pressure, shared-history pressure.
  • non-goals:
    • no product comment service in raw Slate.
    • no fake hidden root nodes for document metadata.
    • no same node object mounted in two editors.
    • no current-version Plate/slate-yjs adapter work in this plan.
    • no Slate v2 source edits from this Ralplan pass.
  • decision boundary: the plan may define raw Slate substrate and API target; a later Ralph pass owns implementation.
  • open user question: none.

Decision Brief

Principles

  • Content is content. Metadata is metadata.
  • Every durable change must have a replayable commit record.
  • History policy belongs at the transaction/state-field boundary, not inside React components.
  • Raw Slate stays unopinionated. Product stores stay product-owned.
  • Future multi-root editing must not be blocked by today's title/settings API.

Drivers

  • legacy Slate only serializes editor.children, which forces fake node hacks.
  • current Slate v2 already has editor.read, editor.update, extension state groups, update metadata, and commit listeners.
  • current operations only model node/text/selection/fragment changes.
  • history currently consumes operation-only commits.
  • comments/annotations already have external store pressure.

Viable Options

OptionProsConsVerdict
Hidden/invisible nodesuses current children and operationscorrupts selection/content model, teaches bad practice, bad for title/settingsdrop
Runtime WeakMap state onlyalready possible with extension runtimeStatenot persisted, not replayable, not collab-safe, not history-safedrop for durable state
Extend Operation with arbitrary app opsone replay streamrisks turning operation model into an app-state dumppartial
Add first-class statePatches beside document operationshonest model, serializable, history/collab ready, does not fake nodesnew commit/history/collab plumbingkeep
External app store onlybest for comments/presence/product datacannot solve document title/settings as raw document metadatapartial

Chosen option: EditorCommit = document operations + statePatches + metadata. History and collab consume the commit record. Adapters may flatten into one transport stream, but raw Slate should not pretend a title change is a node op.

Current Source Evidence

SurfaceCurrent live shapeImplication
Update metadataEditorUpdateMetadata has history, collab, origin, selection; history mode is `mergepush
State/tx groupsEditorStateExtensionGroups and EditorTxExtensionGroups exist in .tmp/slate-v2/packages/slate/src/interfaces/editor.ts:454 and :462; BaseEditor.read/update is public at :515.Store APIs should appear as extension/state/tx groups.
Operationsoperation union is node/text/selection/replace only in .tmp/slate-v2/packages/slate/src/interfaces/operation.ts:130.No durable non-node state operation exists today.
Operation lawoperation docs say operations are what enable history/collab in .tmp/slate-v2/packages/slate/src/interfaces/operation.ts:142.Any persisted state change needs equivalent replay law.
History stateslate-history uses WeakMaps for history/control state in .tmp/slate-v2/packages/slate-history/src/history-extension.ts:54.Runtime state already exists, but it is not a persisted document-state model.
History policyhistoric updates use metadata: { history: { mode: 'skip' } } in .tmp/slate-v2/packages/slate-history/src/history-extension.ts:131; merge/push logic reads metadata/tags at :265.Store history defaults can compose with existing update metadata.
DirtinessgetOperationDirtiness computes classes from operations in .tmp/slate-v2/packages/slate/src/core/public-state.ts:667.A state patch channel must add state dirtiness, or React will miss field-specific subscriptions.
Transaction snapshotcurrent transaction snapshot stores children, marks, metadata, operations, selection, and tags in .tmp/slate-v2/packages/slate/src/core/public-state.ts:2810.Add state patch capture here, not after commit.
Runtime extension stateextension setup has context.runtimeState(initialValue) in .tmp/slate-v2/packages/slate/src/core/editor-extension.ts:429.Keep for ephemeral extension runtime. Do not overload it for persisted title/settings.
Annotation storeSlateAnnotation has external anchor, data, id, projection in .tmp/slate-v2/packages/slate-react/src/annotation-store.ts:18; store source can be array or function at :787.Comments already point to external anchored channels.

Ecosystem Strategy

EditorObserved mechanismSlate targetVerdict
ProseMirrorTransactions track doc, selection, marks, metadata; decorations are view data; bookmarks anchor durable selection. See docs/research/sources/editor-architecture/prosemirror-transaction-view-dom-runtime.md:27 and :41.Steal transaction-owned metadata and bookmark discipline. Add state patches as commit records. Keep Slate paths/runtime ids, not PM integer positions.agree
ProseMirror StateFieldState fields define init, apply, optional toJSON, optional fromJSON in ../raw/prosemirror/packages/state/src/plugin.ts:95; EditorState.toJSON/fromJSON serialize mapped plugin fields in ../raw/prosemirror/packages/state/src/state.ts:217; history skips undo with transaction metadata in ../raw/prosemirror/packages/history/src/history.ts:277 and documents addToHistory: false at :388. Context7 /websites/prosemirror_net confirmed the same API.Steal descriptor-level init/apply/serialize/deserialize and transaction metadata policy. Do not copy plugin complexity as public DX.agree
Lexical read/update/tagsLexical docs require synchronous read/update contexts in ../raw/lexical/repo/packages/lexical-website/docs/intro.md:64; update tags cover history push/merge, collaboration, skip-collab, skip-scroll, skip-DOM-selection in ../raw/lexical/repo/packages/lexical-website/docs/concepts/updates.md:67; source constants match at ../raw/lexical/repo/packages/lexical/src/LexicalUpdateTags.ts:10. Context7 /facebook/lexical confirmed the read/update evidence.Steal lifecycle tags and context discipline. Slate should keep typed metadata plus tags, not $ helpers.agree
Lexical NodeStateNodeState can attach ad-hoc JSON state to any node and even RootNode metadata in ../raw/lexical/repo/packages/lexical-website/docs/concepts/node-state.md:3 and :15; it serializes non-default values at :116, is copy-on-write at :147, and still has Yjs/listener gaps at :190.Steal parse-backed JSON, default elision, equality, and copy-on-write lessons. Reject RootNode metadata as Slate's durable-document-state answer; it is still metadata stored through content.partial
TiptapExtension storage is namespaced and exposed via editor.storage in ../raw/tiptap/docs/src/content/editor/extensions/custom-extensions/create-new/extension.mdx:146; commands are extension-defined at :204; editor exposes storage, commands, and chain() in ../raw/tiptap/repo/packages/core/src/Editor.ts:223; React docs push useEditorState selectors and transaction rerender control in ../raw/tiptap/docs/src/content/guides/performance.mdx:92 and :139. Context7 /ueberdosis/tiptap-docs confirmed the same selector/storage posture.Steal easy extension authoring, namespaced field discoverability, and selector subscriptions. Reject chain-first/product-service APIs as raw Slate core.partial
JotaiOfficial docs model an immutable atom config as the unit of state, with primitive, derived, and write functions; a Store/Provider is only the container. selectAtom, focusAtom, and splitAtom prove property/item-level granularity, equality, and stable keys are the actual performance story. Context7 /websites/jotai confirmed this.Steal atom-like field descriptors and one shared editor container. Do not copy atom naming into raw Slate, and do not collapse everything into one object value.agree
Slate v2 overlay researchExisting landscape rejects forcing annotation metadata into editor runtime and callback/array APIs in docs/research/systems/editor-architecture-landscape.md:174.Keep comments external; expose field/controller APIs for durable state.agree

Jotai Atom-Granularity Reopen Pass - 2026-05-20

Status: complete for this single pass only. The broader Ralplan lane remains pending because issue discovery, objection review, and final gates have not been rerun after the naming change.

Evidence read:

  • current plan target and live Slate v2 source for editor state, transaction snapshots, commit sources, and annotations.
  • solution notes on derived lint decorations, Yjs readiness, projection stores, and stable annotation store inputs.
  • Context7 official Jotai docs for atom configs, Store/Provider, selectAtom, focusAtom, and splitAtom.

Decision:

  • Replace defineEditorStateStore as the primary public concept with defineEditorStateField.
  • Keep one editor-owned state container internally. The public unit is a typed field descriptor, not many user-created store instances and not one mutable editor.state object.
  • Keep object-valued fields legal for coarse document domains, but make scalar fields first-class. Persisting only document.title must not require a { title, settings } object value.
  • Keep statePatches as the commit channel. Current target names are dirtyStateKeys, source 'state', and document snapshot state.
  • React hooks should mirror Jotai's ergonomic split: a read hook for one field, and a setter hook for simple cases, while editor.update remains the transaction API for grouped changes.

Historical provisional API from this pass. It is superseded by the latest defineStateField / initialValue / state.getField authority above:

ts
const documentTitle = defineEditorStateField({
  key: 'document.title',
  collab: { default: 'shared' },
  initial: () => 'Untitled',
  history: { default: 'push' },
  persist: true,
})

const spellcheck = defineEditorStateField({
  key: 'document.settings.spellcheck',
  collab: { default: 'shared' },
  initial: () => true,
  history: { default: 'push' },
  persist: true,
})

const editor = createEditor({
  extensions: [documentTitle, spellcheck],
  initialDocument: {
    children,
    state: {
      [documentTitle.key]: 'Q2 Plan',
      [spellcheck.key]: true,
    },
  },
})

const title = editor.read((state) => state.fields.get(documentTitle))

editor.update(
  (tx) => {
    tx.fields.set(documentTitle, 'Q3 Plan')
  },
  {
    metadata: {
      history: { mode: 'push' },
    },
  }
)

React DX target:

tsx
function DocumentTitleInput() {
  const title = useEditorStateFieldValue(documentTitle)
  const setTitle = useSetEditorStateField(documentTitle)

  return (
    <input
      value={title}
      onChange={(event) => {
        setTitle(event.target.value, {
          metadata: { history: { mode: 'merge' } },
        })
      }}
    />
  )
}

Selector target for object-valued fields:

ts
const margins = defineEditorStateField({
  key: 'document.layout',
  initial: () => ({
    margins: { bottom: 72, left: 72, right: 72, top: 72 },
  }),
  persist: true,
})

const topMargin = useEditorStateFieldValue(
  margins,
  (layout) => layout.margins.top
)

Research/Ecosystem Refresh Pass - 2026-05-20

Status: complete.

Evidence read:

  • compiled research index/log and editor-architecture summaries: docs/research/index.md, docs/research/log.md, docs/research/sources/editor-architecture/prosemirror-transaction-view-dom-runtime.md, docs/research/sources/editor-architecture/lexical-read-update-extension-runtime.md, docs/research/sources/editor-architecture/tiptap-extension-command-react-dx.md, docs/research/decisions/slate-v2-state-tx-public-api-and-extension-namespaces.md, docs/research/decisions/slate-v2-collaborative-annotation-channels.md, and docs/research/systems/editor-architecture-landscape.md.
  • current local Slate v2 source: .tmp/slate-v2/packages/slate/src/interfaces/editor.ts, .tmp/slate-v2/packages/slate/src/interfaces/operation.ts, .tmp/slate-v2/packages/slate/src/core/public-state.ts, .tmp/slate-v2/packages/slate-history/src/history-extension.ts, and .tmp/slate-v2/packages/slate-react/src/annotation-store.ts.
  • raw official source/docs for ProseMirror, Lexical, and Tiptap under ../raw/prosemirror, ../raw/lexical, and ../raw/tiptap.
  • Context7 official-doc checks for /websites/prosemirror_net, /facebook/lexical, and /ueberdosis/tiptap-docs.

Current-source result:

  • Slate v2 already has update metadata for history/collab/selection (.tmp/slate-v2/packages/slate/src/interfaces/editor.ts:162), commit sources including decoration, annotation, and external (:1081), and commit records with operations/metadata/dirtiness (:1623).
  • Slate v2 operations remain node/text/selection/replace only (.tmp/slate-v2/packages/slate/src/interfaces/operation.ts:130), and the current transaction snapshot captures children/marks/metadata/operations/tags but no state patches (.tmp/slate-v2/packages/slate/src/core/public-state.ts:65 and :2810).
  • Commit dirtiness is derived from operations and maps to dirty paths/runtime ids (.tmp/slate-v2/packages/slate/src/core/public-state.ts:667). A document state field channel must add keyed state dirtiness instead of widening all state writes to dirtyScope: all.
  • Slate history already uses metadata/tag policy for push, merge, and skip (.tmp/slate-v2/packages/slate-history/src/history-extension.ts:131 and :265), but it currently consumes committed operations, not state patches (:233).
  • Slate annotations are already external anchored stores with id/data/projection and per-id subscriptions (.tmp/slate-v2/packages/slate-react/src/annotation-store.ts:13 and :71).

Ecosystem decision changes:

  • ProseMirror strengthens the descriptor model: state fields own init/apply plus optional JSON, and history opt-out is transaction metadata. That maps directly to defineEditorStateField({ initial, apply?, serialize?, deserialize?, history }).
  • Lexical strengthens tag/dirtiness policy, but its RootNode NodeState path is not the Slate answer. For Slate, title/settings should be document state fields beside children, not RootNode payloads hidden inside the content model.
  • Tiptap strengthens DX expectations: named extension storage and selector subscriptions are table stakes. Raw Slate should provide the substrate; Plate can provide product-friendly wrappers and menus.

Research-layer action:

  • No new compiled research page is needed. Existing compiled pages remain correct after local raw and Context7 refresh.
  • docs/research/log.md records this maintain pass.

Public API Target

This is directional API, not implementation spec. After the latest API refresh, the primary public noun is state field, not state store. The canonical runtime value is multi-root; the common single-root authoring shape is input convenience only.

ts
const documentTitle = defineStateField({
  key: 'document.title',
  collab: 'shared',
  initial: () => 'Untitled',
  history: 'push',
  persist: true,
})

const spellcheck = defineStateField({
  key: 'document.settings.spellcheck',
  collab: 'shared',
  initial: () => true,
  history: 'push',
  persist: true,
})

const editor = createEditor({
  extensions: [documentTitle, spellcheck],
  initialValue: {
    children,
    state: {
      [documentTitle.key]: 'Q2 Plan',
      [spellcheck.key]: true,
    },
  },
})

const title = editor.read((state) => state.getField(documentTitle))

editor.update(
  (tx) => {
    tx.setField(documentTitle, 'Q3 Plan')
  },
  {
    metadata: {
      history: { mode: 'push' },
    },
  }
)

For typing in a title input:

ts
const title = useStateFieldValue(documentTitle)
const setTitle = useSetStateField(documentTitle)

setTitle(nextTitle, {
  metadata: {
    history: { mode: 'merge' },
  },
})

For non-document preferences:

ts
const editorZoom = defineStateField({
  collab: 'local',
  key: 'runtime.zoom',
  history: 'skip',
  initial: () => 1,
  persist: false,
})

React hook target:

  • useStateFieldValue(field, selector?, options?) subscribes by field key and uses selector equality.
  • useSetStateField(field) handles simple one-field updates through editor.update.
  • useEditorState remains the broad editor snapshot hook.
  • Field selectors must be driven by dirtyStateKeys, not by dirtyScope.

Input normalization rule:

ts
type RootKey = string

type Value = {
  roots: Record<RootKey, Element[]>
  state?: Record<string, unknown>
}

type InitialValue =
  | Element[]
  | {
      children: Element[]
      state?: Record<string, unknown>
    }
  | {
      roots: Record<RootKey, Element[]>
      state?: Record<string, unknown>
    }
  • Value is canonical runtime/persisted shape and must not be a union.
  • InitialValue is the only union boundary.
  • createEditor({ initialValue: children }) remains valid and normalizes to { roots: { main: children } }.
  • createEditor({ initialValue: { children, state } }) is the best 99% single-root document-state DX and normalizes to { roots: { main: children }, state }.
  • createEditor({ initialValue: { roots, state } }) is the advanced multi-root path.

Internal Runtime Target

Add a state field descriptor registry:

ts
type CollabPolicy =
  | 'external'
  | 'local'
  | 'shared'
  | false
  | {
      default: 'external' | 'local' | 'shared'
      // Future policy metadata lives here, not in the simple path.
    }

type HistoryPolicy =
  | 'merge'
  | 'push'
  | 'skip'
  | false
  | {
      default: 'merge' | 'push' | 'skip'
      // Future policy metadata lives here, not in the simple path.
    }

type StateField<T> = {
  key: string
  collab?: CollabPolicy
  diff?: (previous: T, next: T) => unknown
  initial: () => T
  history?: HistoryPolicy
  invertPatch?: (patch: unknown, previous: T, next: T) => unknown
  applyPatch?: (previous: T, patch: unknown) => T
  persist?: boolean
  serialize?: (value: T) => unknown
  deserialize?: (json: unknown) => T
  equals?: (a: T, b: T) => boolean
  scope?: 'document' | 'root'
}

Policy rule: persist, history, and collab are universal axes, but the simple path must stay terse:

ts
defineStateField({
  key: 'document.title',
  initial: () => 'Untitled',
  persist: true,
  history: 'push',
  collab: 'shared',
})

Expanded policy objects are allowed only when the policy needs extra metadata:

ts
defineStateField({
  key: 'document.layout',
  initial: defaultLayout,
  persist: true,
  history: { default: 'push' },
  collab: { default: 'shared' },
  diff,
  applyPatch,
  invertPatch,
})

Do not add more top-level policy axes preemptively. Future migration, authorization, conflict resolution, or adapter-specific behavior should first try deserialize, serialize, collab, or extension-owned metadata before becoming a universal field option.

Add root identity to committed content operations, without putting root identity inside Path:

ts
type RootedPath = {
  root: RootKey
  path: Path
}

type Point = {
  root: RootKey
  path: Path
  offset: number
}

type Range = {
  anchor: Point
  focus: Point
}

type InsertTextOperation = {
  type: 'insert_text'
  root: RootKey
  path: Path
  offset: number
  text: string
}

type MoveNodeOperation = {
  type: 'move_node'
  root: RootKey
  path: Path
  newRoot: RootKey
  newPath: Path
}

Single-root views default the root at the command layer:

ts
tx.insertText('x', { at: [0, 0] })

The committed operation is still root-explicit:

ts
{
  type: 'insert_text',
  root: 'main',
  path: [0, 0],
  offset: 3,
  text: 'x',
}

Add state patches to the transaction and commit model. State changes do not become content operations:

ts
type StatePatch =
  | {
      key: string
      next: unknown
      previous: unknown
      type: 'set_state'
    }
  | {
      inversePatch: unknown
      key: string
      patch: unknown
      type: 'patch_state'
    }

type EditorCommit = {
  dirtyPaths: readonly RootedPath[]
  dirtyStateKeys: readonly string[]
  operations: Operation[]
  statePatches: StatePatch[]
  metadata: EditorUpdateMetadata
  tags: EditorUpdateTag[]
}

Add document runtime / view separation:

ts
const runtime = createEditorRuntime({ initialValue })

const mainEditor = createEditorView(runtime, { root: 'main' })
const headerEditor = createEditorView(runtime, { root: 'header' })
  • runtime owns value, roots, state fields, operations, history, and collab.
  • view owns root binding, selection, DOM bridge, focus, and read-only policy.
  • createEditor(...) remains the 99% shortcut that creates a runtime and a main view.

Implementation owners:

  • transaction capture: extend .tmp/slate-v2/packages/slate/src/core/public-state.ts:2810.
  • commit dirtiness: extend .tmp/slate-v2/packages/slate/src/core/public-state.ts:667.
  • public types: extend .tmp/slate-v2/packages/slate/src/interfaces/editor.ts:80, :454, :462, and :1005.
  • source listeners: add an EditorCommitSource for state fields around .tmp/slate-v2/packages/slate/src/interfaces/editor.ts:1081; recommended literal is 'state', not another use of 'external'. Then key subscriptions by field key rather than waking all source subscribers.
  • history: teach .tmp/slate-v2/packages/slate-history/src/history-extension.ts:233 to batch state patches plus operations.
  • serialization: hard-break Value to canonical { roots, state? }; keep InitialValue convenience for old Element[] and { children, state? } inputs.

Descriptor rule: small fields may use whole-value previous/next patches. A large field that opts into history or shared collab must provide diff/applyPatch/invertPatch; otherwise core should reject that policy combination instead of creating huge undo/collab payloads.

History Policy Target

Yes, provide history opt-in/out, with defaults. This should behave like isSelectable / isReadonly in spirit: a descriptor declares the default, and a transaction can override it.

DX rule: use history: 'push' | 'merge' | 'skip' | false and collab: 'shared' | 'local' | 'external' | false in normal field definitions. Use { default: ... } objects only when future policy metadata is needed.

Default policy:

State kindPersistHistoryCollabReason
document titleyespush by default, merge while typingshareduser-visible document state
document settings that affect content/renderingyespushsharedpart of document intent
editor preferencesoptional/localskiplocaluser preference, not document history
comments/thread metadataexternalskip in document historyexternal/shared by productown audit/history outside document undo
annotation anchorsexternal or persisted depending adapterusually skip in document historyexternal/sharedanchor movement follows document ops; thread lifecycle is product-owned
presence/cursors/viewportnoskipexternal/localruntime state
remote collab importsyes if document stateskip local history unless saveToHistorysharedcurrent metadata already has collab.saveToHistory

History batches must include both operations and statePatches. Undoing a title change should not require fake text nodes. Undoing a document edit should not undo a comment thread creation unless the app explicitly routes comments into the same store/history domain.

Persistence And Collaboration Target

Persisted document value should become:

ts
type Value = {
  roots: Record<RootKey, Element[]>
  state?: Record<string, unknown>
}

Rules:

  • state.value.get() returns canonical { roots, state? }.
  • state.children.get() or a root-bound view helper may return the current view's root children for 99% single-root ergonomics.
  • InitialValue accepts old Element[], { children, state? }, and { roots, state? }, then normalizes once at editor creation.
  • only descriptors with persist: true and serializer support enter snapshots.
  • collab adapters receive commit records with operations + statePatches.
  • state field descriptors declare whether a state patch is shared, local, or external.
  • content operations include a root key; state patches do not unless a descriptor declares scope: 'root'.
  • external comment stores keep their canonical service data outside raw Slate.

This aligns with the accepted annotation decision: document value owns document content; comment threads, permissions, and service metadata belong outside raw Slate (docs/research/decisions/slate-v2-collaborative-annotation-channels.md:23).

Non-Contiguous And Multi-Editor Proposal

Do not solve header/footer by jamming nodes into metadata.

Editable header/footer/global regions are content. The right design is a multi-root document model in canonical Value; the 99% single-root input shape is still allowed as InitialValue convenience:

ts
type Value = {
  roots: {
    main: Element[]
    footer?: Element[]
    header?: Element[]
  }
  state: Record<string, unknown>
}

Open design:

  • path identity needs a root id, not just numeric array indexes.
  • selection can be root-local by default; cross-root selection must be explicit.
  • history scope belongs to the document runtime, not each React editor view.
  • view-local selection/presence remains per editor surface.
  • state fields remain document-scoped by default. Per-root state uses field policy, for example defineStateField({ key: 'root.readonly', scope: 'root' }).

For two editors sharing one document/history, the target is:

  • one EditorRuntime / document runtime.
  • multiple editor views bound to the runtime.
  • view-local selection and DOM bridges.
  • shared history stack owned by the document runtime.
  • never the same node objects mounted in two editor instances.

Issue #6016 is the guardrail: shared node identity across editors is classified as unsupported misuse in docs/slate-v2/ledgers/fork-issue-dossier.md.

Issue Ledger Accounting

ClawSweeper status: applied cache-first. This pass makes no Fixes #... claim. It writes a sync-note section in docs/slate-issues/gitcrawl-v2-sync-ledger.md for the reviewed issue surface instead of changing fixed/improved counts.

Related but not fixed:

Issue/clusterRelationCurrent classification
#4477 commentsexternal anchored channels and product comment storesimproves pressure only; product comments not fixed. See docs/slate-v2/ledgers/issue-coverage-matrix.md:141.
#4483 dynamic decorationsstore/controller APIs and dirty source policyimproves projection-store pressure; not exact legacy API closure. See docs/slate-v2/ledgers/issue-coverage-matrix.md:140.
#5987 async decoration caret jumpsource-owned projection/state invalidationimproves; exact async app repro not auto-closed. See docs/slate-v2/ledgers/issue-coverage-matrix.md:142.
#3383 overlapping metadatareinforces external metadata/store lanerelated; no closure claim. See docs/slate-issues/gitcrawl-live-open-ledger.md:463.
#5515 Undo/Redo Allhistory scope and shared-history semanticsrelated; collaboration makes simple "undo all" messy. See docs/slate-issues/open-issues-dossiers/5558-5480.md:547.
#3741 move_node OT undo/redocommit replay/collab metadata pressurerelated; exact moved-node payload not solved. See docs/slate-issues/open-issues-dossiers/3797-3708.md:940.
#3715 collaboration docsfuture examples need real collab/document-state storydocs pressure. See docs/slate-issues/open-issues-dossiers/3797-3708.md:1091.
#4612 external state updatesnon-node state fields must use explicit transaction/state APIs, not React controlled valueimproves-claimed unchanged. See docs/slate-v2/ledgers/issue-coverage-matrix.md:156.
#3705/#3756/#3921 history set_selectionhistory batching and selection state pressurecluster pressure. See docs/slate-issues/gitcrawl-clusters.md:21.
#6016 same initial value in two editorsrejects shared node identity as shared-history solutiontriage-closed/non-fix. See docs/slate-v2/ledgers/fork-issue-dossier.md.
#5537 programmatic focus with multiple editorsmulti-view runtime must keep focus/input ownership per viewcluster-synced; exact browser closure still needs proof. See docs/slate-v2/ledgers/fork-issue-dossier.md.
#5117 placeholder height leaks across multiple editorsmulti-root example must prove view-local DOM measurement and placeholder statefuture-proof/example pressure only. See docs/slate-issues/open-issues-dossiers/5129-5066.md.
#3482 void children requirementwarns against fake invisible child tricksrelated design pressure. See docs/slate-issues/gitcrawl-live-open-ledger.md:590.

No Fixes #... rows are accepted in this pass.

Status: complete.

Evidence read:

  • generated live rows in docs/slate-issues/gitcrawl-live-open-ledger.md.
  • manual sync rows in docs/slate-issues/gitcrawl-v2-sync-ledger.md.
  • fork-local issue sections in docs/slate-v2/ledgers/fork-issue-dossier.md.
  • issue coverage rows in docs/slate-v2/ledgers/issue-coverage-matrix.md.
  • PR reference annotation/collaboration section in docs/slate-v2/references/pr-description.md.

Result:

  • no new exact fixed issue claim.
  • no broad live GitHub search needed.
  • current related set is already classified in durable ledgers.
  • added one sync-note override section to docs/slate-issues/gitcrawl-v2-sync-ledger.md because #3705/#3756/#3921 table rows lag the fork dossier's later history-selection classification.
  • kept #4477/#4483/#5987/#3383/#5515/#3741/#3715/#6016/#3482 statuses unchanged.

Status: complete for this activation only. The broader lane remains pending.

Evidence read:

  • generated live rows for #6016, #5537, #5117, #4612, #4483, #4477, #3383, #5515, #3741, #3715, and #3482 in docs/slate-issues/gitcrawl-live-open-ledger.md.
  • manual sync rows in docs/slate-issues/gitcrawl-v2-sync-ledger.md.
  • fork dossier sections for #6016, #5537, #4612, #3383, #4483, #4477, and #3741 in docs/slate-v2/ledgers/fork-issue-dossier.md.
  • matrix rows for #4483, #4477, #4612, and matrix-only future-proof #5117 in docs/slate-v2/ledgers/issue-coverage-matrix.md.
  • PR reference non-claim section in docs/slate-v2/references/pr-description.md.

Result:

  • no new Fixes #... claim.
  • no new improved count.
  • #6016 stays triage-closed: sharing the same node object graph across editor runtimes is still unsupported. The new answer is one shared SlateRuntime with root-bound views, not two editors sharing nodes.
  • #5537 stays cluster-synced: multi-editor programmatic focus pressure becomes a required provider/browser proof row, not a closure claim.
  • #5117 stays future-proof/example pressure: the multi-root example must prove view-local placeholder/measurement ownership before any claim.
  • #4612 stays improves-claimed unchanged: the provider API must not resurrect controlled React value.
  • PR reference and sync ledger were updated as non-claim API/accounting sync only.

Next issue-ledger pass should decide whether the wide table rows for #3705/#3756/#3921 should be mechanically rewritten, or whether the explicit sync-note override is sufficient until the next generated ledger cleanup.

Issue-Ledger Pass - 2026-05-20

Status: complete.

Evidence read:

  • generated live rows in docs/slate-issues/gitcrawl-live-open-ledger.md.
  • current manual sync notes in docs/slate-issues/gitcrawl-v2-sync-ledger.md.
  • frozen corpus rows in docs/slate-issues/open-issues-ledger.md.
  • macro cluster and gitcrawl cluster files: docs/slate-issues/issue-clusters.md and docs/slate-issues/gitcrawl-clusters.md.
  • test, benchmark, package, and requirements maps: docs/slate-issues/test-candidate-map.md, docs/slate-issues/benchmark-candidate-map.md, docs/slate-issues/package-impact-matrix.md, and docs/slate-issues/requirements-from-issues.md.
  • fork-local issue sections and accepted claim matrix: docs/slate-v2/ledgers/fork-issue-dossier.md and docs/slate-v2/ledgers/issue-coverage-matrix.md.
  • PR reference claim/count narrative in docs/slate-v2/references/pr-description.md.

Decision:

  • Keep the explicit 2026-05-20 sync-note override in docs/slate-issues/gitcrawl-v2-sync-ledger.md as current truth for #3705/#3756/#3921. The old wide table rows are stale carryover; rewriting three giant manual rows by hand would create noisy drift without changing the accepted coverage matrix or PR claims.
  • No docs/slate-v2/ledgers/issue-coverage-matrix.md update is needed. It already has the current #3705, #3756, and #3921 classifications, and this plan adds no exact Fixes #... row.
  • docs/slate-v2/references/pr-description.md must stay a non-claim sync: fixed and improved counts do not change, and non-node document state is an architecture proposal until Ralph implementation proof passes.
  • Add #4612 to the adjacent pressure set: external state updates are already improved by explicit initialization and tx.value.replace; document state fields should follow the same transaction-owned API shape without resurrecting React controlled value.
  • Test and benchmark maps reinforce later implementation proof for history, projection invalidation, and subscription breadth. They do not add a new benchmark target for "document title" or settings state.

Action summary:

  • v2 sync ledger: no further row rewrite after the sync-note override.
  • coverage matrix: no exact claim change.
  • PR description: no count or claim change; non-claim future API reference synced in the revision/handoff pass.

State-Field Issue-Ledger Pass - 2026-05-20

Status: complete for this activation only. The broader Ralplan lane remains pending because terminology/handoff, maintainer-objection/risk, and final gates are still runnable.

Evidence read:

  • generated live issue rows in docs/slate-issues/gitcrawl-live-open-ledger.md.
  • current manual state-field sync note in docs/slate-issues/gitcrawl-v2-sync-ledger.md.
  • frozen corpus and cluster rows in docs/slate-issues/open-issues-ledger.md and docs/slate-issues/gitcrawl-clusters.md.
  • fork-local dossier sections in docs/slate-v2/ledgers/fork-issue-dossier.md.
  • accepted claim matrix in docs/slate-v2/ledgers/issue-coverage-matrix.md.
  • PR open-debt note in docs/slate-v2/references/pr-description.md.

Decision:

  • no fixed issue count changes.
  • no improved issue count changes.
  • PR reference already has the correct non-claim state-field target and remains unchanged.
  • docs/slate-issues/gitcrawl-v2-sync-ledger.md already owns the reviewed surface and remains unchanged.
  • docs/slate-v2/ledgers/issue-coverage-matrix.md now tightens #4612 so document state fields are explicitly future transaction-owned API pressure, not a controlled React value revival.
  • related state-field pressure set remains #4477/#4483/#5987/#3383/#5515/#3741/#3715/#4612/#3705/#3756/#3921/#6016/#3482.

Next owner: state-field terminology/handoff hardening.

State-Field Terminology/Handoff Hardening Pass - 2026-05-20

Status: complete for this activation only. The broader Ralplan lane remains pending because maintainer-objection/risk and final gates are still runnable.

Evidence read:

  • active plan final authority, proof matrix, performance/DX pass, high-risk proof plan, maintainer objection ledger, Ralph-ready handoff, and previous store-based handoff.
  • active completion and continuation files under .tmp/019e3627-238b-7993-a8cf-26be45504c47/.

Decision:

  • at that pass, API and Ralph-ready handoff used defineEditorStateField, initialDocument.state, state.fields, tx.fields, dirtyStateKeys, and source 'state'.
  • latest API authority supersedes those constructor/accessor names with defineStateField, initialValue, state.getField, and tx.setField.
  • proof rows used field-key selector and editor-state-field naming.
  • current performance and risk rows now talk about state field writes, keyed state dirtiness, and O(changed fields).
  • remaining store wording is intentionally confined to external/product stores, Tiptap/Jotai source terminology, file paths such as annotation-store, and the explicitly historical Previous Store-Based Handoff section.

Next owner: state-field maintainer objection and risk pass.

  • implementation: none in Slate Ralplan.
  • verification: pnpm lint:fix for planning artifacts.

Legacy Regression Proof Matrix

Required later implementation proof:

BehaviorTest ownerExpected proof
title change persistspackages/slate/test/document-state-contract.tssnapshot roundtrip contains state[documentTitle.key]
title undopackages/slate-history/test/document-state-history-contract.tsundo/redo toggles title without touching body nodes
title typing mergesamerepeated title input merges when metadata asks merge
skip historysamepreference state patch with history: skip does not enter undo stack
collab exportpackages/slate/test/collab-document-state-contract.tslocal shared state patch appears in adapter export
remote importsameremote state patch applies with history skip and selection side-effect suppression
comments externalpackages/slate-react/test/annotation-store-contract.tsxcomment data update does not change Slate value/history
dirty subscriptionspackages/slate-react/test/state-field-selector-contract.tsxfield-key subscriber wakes without broad document rerender
body edit isolationsamebody typing emits no state patches and wakes no field-key subscriber
title edit isolationsametitle typing emits no dirty paths and wakes no body runtime subscriber
legacy constructionpackages/slate/test/create-editor-value-contract.tscreateEditor({ initialValue: children }) normalizes to { roots: { main: children } }
single-root document state constructionsamecreateEditor({ initialValue: { children, state } }) normalizes to canonical { roots: { main: children }, state }
multi-root constructionsamecreateEditor({ initialValue: { roots, state } }) roundtrips persisted state field descriptors
canonical runtime valuesamestate.value.get() returns { roots, state? }, not a union and not raw Element[]
descriptor patch guardpackages/slate/test/document-state-patch-contract.tslarge historyable/shared field without patch hooks is rejected or downgraded by explicit policy
state sourcepackages/slate-react/test/state-field-selector-contract.tsx'state' source plus descriptor-key subscription wakes exact field listeners only
rooted operationspackages/slate/test/rooted-operation-contract.tscommitted content operations include root; Path stays numeric and root-local
root-aware points/rangessamePoint and Range carry root identity and transforms ignore unrelated roots
editor runtime/view splitpackages/slate/test/editor-runtime-view-contract.ts plus React browser rowone runtime owns value/history/collab; views own root, selection, DOM bridge, focus, and read-only state

Browser Stress And Parity Strategy

Browser proof waits for implementation, but the required user-visible rows are:

  • title input edits while body selection is live.
  • undo body text vs undo title change.
  • remote document state import does not steal focus or scroll.
  • comment-only read-only view can add/update comment metadata without changing document history.
  • two editor surfaces over one shared runtime do not share node object identity.
  • header/body/footer views over one runtime keep root-local selection and shared history.

Performance/DX/Migration/Regression Pressure Pass - 2026-05-20

Status: complete.

Evidence read:

  • current Slate v2 source for metadata, commit sources, commit shape, operations, transaction capture, source subscriptions, CreateEditorOptions, history batching, React selectors, projection stores, and annotation stores: .tmp/slate-v2/packages/slate/src/interfaces/editor.ts:162, :1005, :1081, :1623, and :2342; .tmp/slate-v2/packages/slate/src/interfaces/operation.ts:130; .tmp/slate-v2/packages/slate/src/core/public-state.ts:65, :667, :2621, :2703, :2784, and :2972; .tmp/slate-v2/packages/slate-history/src/history-extension.ts:131 and :233; .tmp/slate-v2/packages/slate-react/src/hooks/use-editor-selector.tsx:180; .tmp/slate-v2/packages/slate-react/src/projection-store.ts:486; and .tmp/slate-v2/packages/slate-react/src/annotation-store.ts:71 and :893.
  • performance rules: .agents/skills/performance/rules/cohort-segmentation.md, .agents/skills/performance/rules/repeated-unit-budget.md, .agents/skills/performance/rules/effect-subscription-budget.md, .agents/skills/performance/rules/interaction-inp-matrix.md, .agents/skills/performance/rules/memory-dom-tagging.md, and .agents/skills/performance/rules/editor-native-behavior-proof.md.

API correction:

  • CreateEditorOptions currently exposes initialValue?: V for content only in .tmp/slate-v2/packages/slate/src/interfaces/editor.ts:1005. The earlier draft's runtime Value = { children, state } example was incomplete for planned multi-root support.
  • Hard-break runtime Value to { roots, state? } so history/collab/rooted operations have one canonical shape.
  • Keep initialValue as the public input boundary and let it accept Element[], { children, state? }, and { roots, state? }.
  • Core read/write DX should be generic and descriptor-based: state.getField(documentTitle) and tx.setField(documentTitle, updater). Product helpers like setTitle belong in extensions or Plate, not core.

Performance contract:

InteractionCommit contractWake contract
body text editoperations non-empty with root, statePatches=[], dirtyStateKeys=[]root/body/runtime/decoration subscribers only; field-key subscribers stay asleep
title editoperations=[], statePatches=[document.title], no dirty rooted pathscommit plus state source; only document.title selectors wake
settings editkeyed state patch with descriptor history/collab policyonly the setting field and explicitly subscribed document-level listeners wake
preference editpersist=false, history=skip, collab=localno history, no collab export, no body rerender
comment thread editexternal annotation/comment service, unless app deliberately routes anchors through a document state fieldannotation id/runtime subscribers wake; Slate value/history stay unchanged
header editrooted content operation with root: 'header'header view and runtime subscribers wake; body view stays asleep except shared-history observers

Do not use dirtyScope: all for state patches. That would make a title keystroke look like a whole-document edit and erase the perf win. Add dirtyStateKeys to EditorCommit, add a state commit source, and make keyed React hooks subscribe by descriptor key.

Cohorts and budgets:

CohortShapeRequired budget
normal0-500 blocks, 1-3 persisted fields, few annotationstitle/settings edits wake zero body blocks
large2k-10k blocks, 10 state fields, 1k annotationsstate patch cost is O(changed fields), not O(blocks + annotations)
stress10k+ blocks, 50 state field keys, 10k annotationsbody typing keeps statePatches=[]; title typing keeps dirty paths empty
pathologicalmegabyte JSON field or huge thread mapdescriptor must provide diff/applyPatch/invertPatch or stay external

Migration contract:

  • old raw Slate input: createEditor({ initialValue: children }).
  • single-root document-state input: createEditor({ initialValue: { children, state } }).
  • multi-root input: createEditor({ initialValue: { roots, state } }).
  • read canonical value: state.value.get() returns { roots, state? }.
  • read root content through root-bound helpers or state.root('main').
  • history migration: existing operation batches keep working; state patch batches add inverse patches beside inverted root-explicit operations.
  • collab migration: adapters export ordered commit records containing operations plus state patches, with remote imports defaulting to local history skip unless metadata says otherwise.

Regression contract additions:

  • state-only commit: no operation, no dirty path, no childrenChanged, one dirty state key.
  • body-only commit: no state patches and no field subscriber wake.
  • mixed commit: body operation plus state patch stay one undoable transaction when history policy says push/merge.
  • initialValue compatibility: legacy Element[] input still works, but the runtime value is canonical { roots, state? }.
  • single-root and multi-root initialValue roundtrip: persisted descriptors serialize and deserialize only their own keys.

Score delta:

  • React/runtime performance rises because keyed state dirtiness is now explicit, but it stays below closure score until React selector contracts exist.
  • Slate-close DX rises because initialValue remains the single public construction option while runtime value stops pretending the document is only one children array.
  • Migration and regression scores rise because the plan now has named compatibility and dirtiness contracts, not just a high-level proof matrix.

Runtime Provider Pressure Pass - 2026-05-21

Status: complete for this activation only. The broader Ralplan lane remains pending because handoff hardening and final gates are still runnable.

Evidence read:

  • core runtime/view source: .tmp/slate-v2/packages/slate/src/editor-runtime-view.ts:39 and :58.
  • core public runtime/view types: .tmp/slate-v2/packages/slate/src/interfaces/editor.ts:590.
  • current React <Slate> provider: .tmp/slate-v2/packages/slate-react/src/components/slate.tsx:89, :135, :233, and :259.
  • current selector bus: .tmp/slate-v2/packages/slate-react/src/hooks/use-editor-selector.tsx:104 and :213.
  • current single-editor hook and state-field hooks: .tmp/slate-v2/packages/slate-react/src/hooks/use-slate-editor.ts:14 and .tmp/slate-v2/packages/slate-react/src/hooks/use-state-field.ts:40.
  • current proof and example rows: .tmp/slate-v2/packages/slate/test/editor-runtime-view-contract.ts:13, .tmp/slate-v2/packages/slate-react/test/state-field-selector-contract.tsx:25, and .tmp/slate-v2/site/examples/ts/document-state.tsx:333.
  • skill lenses applied: Vercel React rerender/subscription rules, performance cohort/budget/native-behavior rules, performance-oracle complexity checks, high-risk deliberate pass, and TDD vertical-slice discipline.

Pressure verdict:

  • Keep SlateRuntime plus <Slate root>; it is the right public shape.
  • Do not expose SlateViewProvider; that would add another provider noun when <Slate> already means "bind React children to an editor surface".
  • Make <Slate> props XOR:
    • outside SlateRuntime: require editor.
    • inside SlateRuntime: accept root, defaulting to main for the 99% single-root runtime path.
    • reject editor plus root together.
  • Allow <Slate root readOnly> to map to view-local createEditorView readOnly; Editable readOnly can still own DOM read-only presentation.
  • Nested SlateRuntime providers are allowed and isolate their runtimes; a <Slate root> binds to the nearest runtime.

Performance:

  • applicability: applied.
  • Vercel rules used: client-event-listeners, rerender-defer-reads, rerender-derived-state, rerender-split-combined-hooks, advanced-event-handler-refs, and advanced-use-latest.
  • extra rules used: cohort-segmentation, repeated-unit-budget, rare-state-isolation, effect-subscription-budget, interaction-inp-matrix, memory-dom-tagging, react-19-runtime-proof, and editor-native-behavior-proof.
  • repeated units: root views, mounted blocks/leaves, state-field subscribers, runtime-level listeners, and cross-view selectors.
  • cohorts: normal = one main root, 0-500 blocks, 1-3 fields; medium = header/main/footer, up to 2k body blocks, 10 fields; large = header/main/footer, 10k body blocks, 1k annotations; stress = 50k blocks, 50 fields, 10k comments/annotations, active collab; pathological = huge JSON field, custom renderers, heavy decorations, mobile IME.
  • budgets: one editor subscription bridge per SlateRuntime; focus/focusout listeners deduped per runtime, not per root view; zero product comment payloads in repeated block props; root-view selectors filtered by root and dirty keys; state-field selectors wake only their field key.
  • React/runtime primitives: use useSyncExternalStore-style selector boundaries and stable callback refs for long-lived subscriptions. React transitions/deferred values are allowed only for non-urgent side panels, previews, summaries, and diagnostics. They must not wrap visible typing, selection import/export, IME, or DOM text sync.
  • interaction metrics: type in header, type in body, type in footer, title field typing, select in header then type in body, undo in header, undo title field, click body after title edit, remote title patch, remote header patch, copy/select-all per root, and placeholder mount/update.
  • trace/CWV proof: React Performance Tracks and browser traces matter for provider fanout and interaction latency; load metrics are secondary because this is editor runtime behavior, not page startup.
  • memory tags: runtime count, root view count, selector listener count, runtime listener count, document listener count, dirty root set size, dirty state key count, annotation/projection bucket count, DOM node count per root, and heap after repeated title/header/body edits.
  • degradation contract: no native behavior degradation for normal/medium/large cohorts. Stress modes may add explicit opt-in degradation later, but this provider API cannot depend on virtualization or hidden editable content.
  • dashboard/RUM gap: later implementation should tag slate.runtimeId, root, interaction, blockCount, fieldCount, annotationCount, viewCount, browser, and inputMode.

Regression proof additions:

BehaviorTest ownerExpected proof
provider shortcutpackages/slate-react/test/slate-runtime-provider-contract.test.tsx<SlateRuntime runtime><Slate /></SlateRuntime> binds main; <Slate root="header"> binds header.
prop boundarysame<Slate root> outside runtime throws; <Slate editor root> throws; <Slate editor> outside runtime still works.
nearest runtimesamenested SlateRuntime providers isolate root views and selectors.
listener budgetsamedocument focus listeners are deduped per runtime, not multiplied by header/body/footer views.
root selector isolationsamebody typing does not rerender header/footer root selectors or field-key subscribers.
cross-view hooksameuseSlateViewState('header', selector) updates from header commits and ignores body-only commits.
runtime state hooksameuseSlateRuntimeState reads state fields/history/collab metadata without binding to a root view.
read-only rootsamereadOnly root view rejects editor.update through view hooks while writable root still edits.
placeholder localityplaywright/integration/examples/multi-root-document.test.tsheader/body/footer placeholder measurement never leaks across views.
focus localitysameundo/redo and state-field edits from title/header do not steal focus into body.
native behaviorsameper-root select-all/copy/paste/follow-up typing stays native or explicitly model-backed.

High-risk addendum:

FailureConsequenceGuard
Naive React provider creates one selector bus per root viewheader/body/footer edits broadcast too broadlySlateRuntime owns the runtime selector bus; <Slate root> derives view state.
Naive focus listeners stay in every <Slate>#5537-style focus bugs and listener fanoutdedupe document focus listeners per runtime; view focus is root-local state.
<Slate root> silently works without runtimewrong editor binding and confusing DXthrow unless a runtime provider exists.
<Slate editor root> is acceptedtwo authorities for one providerreject as invalid props.
Cross-view hook reads sibling React contextbrittle provider topologysubscribe through runtime/root key only.
Multi-root example teaches comments in Slate statebad architecturekeep comments external; use header/footer/title/settings only.
Placeholder measurement is keyed by editor singleton only#5117 repeatskey measurement by runtime plus root/view.

TDD shape:

Use vertical slices:

  1. Red/green <SlateRuntime> plus <Slate root> main/header binding.
  2. Red/green prop boundary errors.
  3. Red/green selector fanout for root views.
  4. Red/green useSlateRuntimeState and useSlateViewState.
  5. Red/green focus/listener budget.
  6. Red/green multi-root browser example.

Next owner: runtime-provider-handoff-hardening.

Applicable Skill Review Matrix

SkillStatusResult
slate-ralplanappliedlatest API pass and final gates complete.
clawsweeperapplied cache-firstrelated issue set recorded; no Fixes claim; sync-note section added for reviewed surface.
major-taskappliedtreated as public API/data-model architecture.
research-wikiapplied maintainreused existing editor-architecture compiled lane; no new research page yet.
learnings-researcherappliedrelevant prior learnings: Yjs core contracts before package work, derived lint state should be snapshot-derived, projection logic split, history merge heuristics.
vercel-react-best-practicesappliedshared runtime provider must dedupe global listeners, avoid root-view over-subscription, keep callback refs stable, and defer only non-urgent side UI.
performance-oracleappliedstate patch cost must be O(changed fields); root-view selector fanout must be O(changed roots + changed fields), not O(all roots * blocks).
performanceappliedrepeated units are body blocks, root views, field selectors, runtime listeners, and annotation buckets; require keyed state/root dirtiness, cohort budgets, INP rows, and memory tags.
intent-boundary-passappliedscope/non-goals recorded.
steelman-passappliedmaintainer objections expanded; raw Slate state-field substrate stays small, keyed, and extension-owned.
high-risk-deliberate-passappliedblast radius, proof gates, provider prop boundaries, focus/listener failure modes, rollback/remediation, and Ralph TDD order expanded.
tddapplied to planRalph must use vertical public-contract slices, not broad horizontal test writing.
ralphprepared, not executedRalph-ready handoff is embedded below; no Slate v2 source edits start until the user explicitly invokes Ralph.

High-Risk Pre-Mortem

Trigger: public API plus persisted data-model plus history/collab change.

Blast radius:

SurfaceFiles/packagesRisk
Core public APIpackages/slate/src/interfaces/editor.ts, packages/slate/src/create-editor.tscanonical Value, InitialValue, descriptor typing, rooted operations, EditorCommit, runtime/view APIs, and state patches become public contracts.
Core transaction/runtimepackages/slate/src/core/public-state.ts, extension registry/runtimestate patches must rollback with failed transactions, publish ordered commits, and avoid broad dirtiness.
Historypackages/slate-history/src/history-extension.tsundo/redo batches must invert and replay mixed operation/state-patch commits without corrupting selection or body content.
React subscriptionspackages/slate-react/src/hooks, selector context, annotation/projection storestitle/settings changes must not rerender body blocks or annotation buckets.
Collaboration substratepackages/slate/test/collab-*, future slate-yjs adapterlocal/remote state patches need ordered export/import, origin metadata, and history skip defaults.
Examples/docssite examples and Playwright example rowsexamples must not teach hidden nodes, controlled value, or comments in raw Slate value.
Multi-root/shared runtimepackages/slate/src/interfaces/editor.ts, runtime/view internals, React DOM bridgeroot identity must stay deterministic across operations, points/ranges, history, collab, and multiple editor views.

Failure scenarios:

  1. State patches become arbitrary app storage and raw Slate turns into a product framework.
    • mitigation: descriptor registry, explicit persist/history/collab policy, no product comment service in core.
  2. History becomes slow because every field write snapshots large JSON.
    • mitigation: descriptor equality, patch deltas, per-key dirty classes, benchmark large metadata stores.
  3. Collab adapters cannot reconcile state patches with document ops.
    • mitigation: commit record ordering, field descriptors with shared/local/external, fake collab adapter contracts before real Yjs package work.
  4. State field writes wake the whole editor and kill typing responsiveness.
    • mitigation: dirtyStateKeys, descriptor-key subscriptions, render counter tests, and rerender-breadth benchmark.
  5. The migration path makes the 99% single-root case noisy.
    • mitigation: keep initialValue as the input boundary, accept { children, state? }, normalize once to canonical { roots, state? }, and keep root-explicit details out of common transforms.
  6. Root identity is added inconsistently and breaks selection/collab.
    • mitigation: add root to committed operations, Point, Range, refs, and dirty paths together; keep numeric Path root-local.

Verdict: keep, but split implementation into substrate, history, selector, and example phases. Do not ship one giant API PR.

High-Risk Deliberate Proof-Expansion Pass - 2026-05-20

Status: complete.

Evidence read:

  • active plan after steelman pass.
  • current Slate v2 scripts and gates in .tmp/slate-v2/package.json.
  • package scripts in .tmp/slate-v2/packages/slate/package.json, .tmp/slate-v2/packages/slate-history/package.json, and .tmp/slate-v2/packages/slate-react/package.json.
  • existing proof families under .tmp/slate-v2/packages/slate/test, .tmp/slate-v2/packages/slate-history/test, .tmp/slate-v2/packages/slate-react/test, .tmp/slate-v2/playwright/integration/examples, and .tmp/slate-v2/scripts/benchmarks.
  • representative contracts: .tmp/slate-v2/packages/slate/test/commit-metadata-contract.ts, .tmp/slate-v2/packages/slate/test/state-tx-public-api-contract.ts, .tmp/slate-v2/packages/slate/test/collab-adapter-extension-contract.ts, .tmp/slate-v2/packages/slate-history/test/history-contract.ts, .tmp/slate-v2/packages/slate-react/test/render-profiler-contract.test.tsx, and .tmp/slate-v2/packages/slate-react/test/annotation-store-contract.tsx.

Ralph execution entry gates:

  • The accepted public names for the first implementation pass are defineStateField, canonical Value = { roots, state? }, InitialValue, state.getField, tx.setField, root-explicit operations, root-aware Point/Range, statePatches, dirtyStateKeys, and source 'state'.
  • No implementation pass may add hidden nodes, controlled value wrappers, a product comment service, or shared node-object multi-editor support.
  • Ralph should use vertical TDD slices. One failing public contract first, then the smallest implementation for that contract, then the next contract.
  • Browser/examples wait until the core/history/react contracts compile and pass.

Expanded proof plan:

StageRequired new or expanded proofCommand from .tmp/slate-v2
Core value/inputpackages/slate/test/create-editor-value-contract.ts: Element[], { children, state? }, and { roots, state? } all normalize to canonical { roots, state? }; persisted field descriptors serialize only their own keys.bun test ./packages/slate/test/create-editor-value-contract.ts
Rooted operationspackages/slate/test/rooted-operation-contract.ts: committed content operations include root; Path remains numeric; Point/Range carry root; transforms ignore unrelated roots.bun test ./packages/slate/test/rooted-operation-contract.ts
Runtime/view splitpackages/slate/test/editor-runtime-view-contract.ts: one runtime owns value/history/collab; views own root, selection, DOM bridge, focus, and read-only policy.bun test ./packages/slate/test/editor-runtime-view-contract.ts
Core field/commitpackages/slate/test/document-state-contract.ts: state-only commit has no operations, no rooted dirty paths, one dirtyStateKeys entry; body-only commit has empty state patches.bun test ./packages/slate/test/document-state-contract.ts
Patch guardpackages/slate/test/document-state-patch-contract.ts: large historyable/shared fields without diff/applyPatch/invertPatch are rejected or downgraded by explicit policy.bun test ./packages/slate/test/document-state-patch-contract.ts
Commit metadata regressionExtend packages/slate/test/commit-metadata-contract.ts for mixed operation + state patch order, frozen metadata, tags, and source publication.bun test ./packages/slate/test/commit-metadata-contract.ts
Public type contractExtend packages/slate/test/state-tx-public-api-contract.ts and generic type tsconfig for Value, InitialValue, descriptor inference, rooted operations, and optional typed extension aliases.bun test ./packages/slate/test/state-tx-public-api-contract.ts && bun --filter slate typecheck
Historypackages/slate-history/test/document-state-history-contract.ts: title push, title typing merge, preference skip, mixed operation/state undo, redo, and remote import history skip.bun test ./packages/slate-history/test/document-state-history-contract.ts
Collab substrateExtend or add packages/slate/test/collab-document-state-contract.ts: local shared patch export, remote import with history: skip, local/external policy suppression, ordered mixed commit export.bun test ./packages/slate/test/collab-document-state-contract.ts
React selector localitypackages/slate-react/test/state-field-selector-contract.test.tsx: useStateFieldValue wakes exact field selectors only; body typing wakes no field selector; title typing wakes no body runtime subscriber.cd packages/slate-react && bunx vitest run --config ./vitest.config.mjs test/state-field-selector-contract.test.tsx
Annotation/comment boundaryExtend packages/slate-react/test/annotation-store-contract.tsx: comment data updates stay out of Slate value/history while anchors follow document edits.cd packages/slate-react && bunx vitest run --config ./vitest.config.mjs test/annotation-store-contract.test.tsx
Browser exampleAdd playwright/integration/examples/document-state.test.ts: title input, body selection live, undo title vs body, remote state import no focus/scroll steal, two root views over one runtime.playwright test playwright/integration/examples/document-state.test.ts --project=chromium
PerformanceExtend existing benchmark families instead of inventing a one-off harness: core editor-state-field, rooted-operation-transform, history-retained-memory, collab-readiness, and React rerender breadth.bun bench:core:editor-state-field:local && bun bench:core:rooted-operation-transform:local && bun bench:core:history-retained-memory:local && bun bench:core:collab-readiness:local && bun bench:react:rerender-breadth:local
Broad package gatesPackage typecheck/test after focused contracts pass.bun --filter slate typecheck && bun --filter slate-history typecheck && bun --filter slate-react typecheck && bun test:vitest
Release gateRequired before claiming implementation closure.bun check, then bun check:full if examples/browser behavior changed

Rollback and remediation:

  • Before release, rollback is a hard cut: remove defineStateField, statePatches, rooted-operation/runtime-view public API, and the state-field hooks if the proof set fails. Do not ship a half-public API.
  • If core field descriptors pass but history/collab fails, keep persisted field descriptors internal/experimental and default history/collab to skip/local until mixed commit replay is proven.
  • If React locality fails, do not expose useStateFieldValue; keep field updates available through core and delay React examples.
  • If large-field patching fails, keep whole-value patches only for small fields and reject history: push|merge plus collab: shared without descriptor patch hooks.
  • If browser focus/scroll rows fail, state imports must default to selection: { dom: 'preserve', focus: false, scroll: false } and examples stay unpublished.

TDD order for Ralph:

  1. InitialValue normalization to canonical Value.
  2. root-explicit committed operations plus root-aware points/ranges.
  3. runtime/view split for single-root shortcut and multi-root views.
  4. state-only commit and body-only isolation.
  5. mixed operation/state-patch commit ordering and metadata.
  6. history push/merge/skip for document state fields.
  7. collab export/import policy.
  8. React descriptor-key selector hook.
  9. example/browser proof.
  10. benchmark and broad gates.

Verdict: keep and split. The plan is ready for a later Ralph implementation handoff only after the revision/handoff pass removes remaining wording drift and packages these proof rows as ordered implementation work.

Maintainer Objection Ledger

DecisionStrongest fair objectionBest antithesisChosen answerProof requiredVerdict
Add persisted state fields beside children"This turns Slate into an app-state framework."Raw Slate should stay a content editor; apps already have Redux/Zustand/server stores for titles/settings.Keep only descriptor, transaction, snapshot, history, collab, and dirtiness substrate. Product helpers, thread services, permissions, and domain schemas stay outside core.Extension descriptor contract, no built-in product stores, docs showing title/settings only as examples.keep
Use statePatches instead of hidden nodes"Operations are Slate's replay law; a second mutation stream is dangerous."Extend Operation with set_state so history/collab stay one stream.Keep Operation content/selection focused. Make EditorStatePatch an ordered commit record with descriptor-owned apply/invert. Collab adapters can flatten commit records into one transport stream without pretending state is a node op.Unit contract for ordered mixed commits and history inversion.keep, with replay invariants
Canonical Value = { roots, state? } with InitialValue convenience"This makes the normal one-root editor pay for multi-root."Keep runtime Value = Element[] and add a separate multi-root mode later.A union runtime value would poison operations/history/collab. Canonical roots give one replay shape; InitialValue keeps the 99% case concise with { children, state? }.Value normalization, rooted operation, collab export/import, and editor view tests.keep
Generic state.getField / tx.setField API"This is less nice than tx.documentMeta.setTitle()."Require extensions to expose named state/tx groups for every field.Generic descriptor access is the raw substrate. Extensions may expose typed aliases through existing state and tx extension groups, but raw Slate examples should first show the generic shape so core does not grow product verbs.Type inference contract for descriptor get/set and optional extension group alias.keep, with optional aliases
useStateFieldValue(field, selector)"Another React hook duplicates useEditorState."Make users pass shouldUpdate to useEditorState.A broad selector hook is too easy to misuse. A field-key hook bakes in keyed subscription and equality. useEditorState remains broad; field selectors use state dirtiness.Render-count test proving title typing wakes the title hook and not body blocks.keep
dirtyStateKeys and 'state' source"A new dirtiness class complicates commits."Reuse 'external' or dirtyScope: all.Reusing 'external' is semantically wrong and dirtyScope: all is a perf bug. Add keyed state dirtiness; source-level notification alone is insufficient without a field key.Source subscription contract for commit, 'state', and descriptor-key listeners.keep
Descriptor diff/applyPatch/invertPatch hooks"Patch hooks are too much API for title/settings."Always snapshot previous/next values.Small fields can use whole-value previous/next. Large historyable/collaborative fields must provide descriptor patch hooks or remain external. This keeps normal DX small and prevents megabyte undo entries.Pathological-field test requiring either patch hooks or history/collab rejection.revise descriptor docs
Document title history defaults"Undoing title changes with body edits could surprise users."Default every non-node field to history skip.User-visible document state should be undoable by default. Continuous title input uses transaction metadata merge; app preferences and comments default to skip.History push/merge/skip tests for document title, settings, and preference field.keep
Comments external by default"Comments are document state too."Put comments in persisted fields so snapshots are complete.Comment anchors may be document-adjacent, but thread lifecycle, permissions, audit, and remote service data are product-owned. Raw Slate should support external anchored stores and optional app-defined persisted anchor fields, not ship a comment model.Example split: external comments plus persisted document title/settings.keep
Root-explicit operations and root-aware points/ranges"This is a lot of payload for single-root editors."Put the root key inside Path or keep paths global.Numeric Slate paths stay root-local; committed ops carry root only for replay/history/collab. Single-root transforms default root from the view.Operation transform, point/range, path-ref, dirty-path, history, and collab contracts.keep
Editor runtime/view split"This sounds like a framework abstraction."Keep one editor instance per surface and coordinate externally.Shared history needs one document runtime with multiple root-bound views; shared node identity is misuse and collides with #6016. createEditor(...) remains the 99% shortcut.Runtime/view tests plus browser proof with two views over one runtime.keep

Steelman Maintainer-Objection Pass - 2026-05-20

Status: complete.

Evidence read:

  • active plan sections for public API, internal runtime, perf contracts, history, persistence/collab, comments, multi-root, and pass ledger.
  • current Slate v2 extension slots: .tmp/slate-v2/packages/slate/src/interfaces/editor.ts:1299, :1310, :1367, :1461, and :1554, plus commit listener setup in .tmp/slate-v2/packages/slate/src/core/editor-extension.ts:584.
  • steelman-pass and high-risk-deliberate-pass rules.

Accepted revisions:

  • Keep generic state.getField / tx.setField as the raw substrate, but explicitly allow extensions to expose typed state / tx aliases through the existing extension group system. Do not make product verbs part of core.
  • Treat statePatches as ordered commit records with replay invariants, not as arbitrary app events. History/collab adapters may flatten them into transport events, but core should not widen Operation until a separate operation-law decision proves that is better.
  • Make 'state' a source class plus descriptor-key subscriptions. A source-only listener is still too broad.
  • Keep diff/applyPatch/invertPatch advanced and optional. If a field is large and wants history or shared collab, descriptor patch hooks are required; otherwise the field stays local/external or whole-value only.

Dropped alternatives:

  • no runtime union Value.
  • no canonical runtime Value = { children, state }.
  • no initialDocument.
  • no hidden root/header/title nodes.
  • no generic custom Operation escape hatch in this plan.
  • no reuse of 'external' for document state patches.
  • no global dirtyScope: all for state field writes.

Deferred implementation choices:

  • exact view creation export names may still be adjusted, but the target split is runtime owns document state/history/collab and view owns root/selection/DOM.
  • whether the descriptor factory should auto-create typed extension aliases is an implementation-phase TypeScript ergonomics question, not an architecture blocker.

State-Field Maintainer Objection And Risk Pass - 2026-05-20

Status: complete for this activation only. The broader Ralplan lane remains pending because final gates still have to audit every pass row, state file, and handoff boundary.

Evidence read:

  • current final authority, state-field proof rows, performance/DX contract, high-risk proof plan, maintainer objection ledger, and Ralph-ready handoff.
  • active completion and continuation files under .tmp/019e3627-238b-7993-a8cf-26be45504c47/.

Maintainer-risk decisions:

RiskDecisionReason
Raw Slate becomes an app-state frameworkkeep state fields, with product stores excludedfield descriptors cover document-owned title/settings; comments, permissions, audit trails, and product services stay external.
Public API feels less direct than setTitlekeep generic state.getField / tx.setField as raw substrateextensions may expose typed aliases, but raw Slate should not ship product verbs.
Field patches split replay law from operationskeep statePatches in ordered commits, not Operationoperations remain content/selection law; history/collab consume commit records containing operations plus state patches.
React selectors wake too broadlykeep dirtyStateKeys and field-key subscriptionssource-only listeners or dirtyScope: all are rejected as perf bugs.
Large field history/collab payloads explodekeep patch hooks optional but required for large shared/history fieldssmall scalar fields can use previous/next values; large fields must provide patch/apply/invert hooks or stay local/external.
Jotai inspiration leaks into Slate namingkeep state field, reject public atom namingthe borrowed idea is descriptor granularity, not Jotai branding.
Header/footer/multi-editor pressure overloads state fieldskeep roots in canonical Value, not in state fieldseditable regions are content roots, not metadata; state fields remain document/root metadata, not hidden content.

Score result:

  • React/runtime and DX remain strong because field-key dirtiness, InitialValue, and canonical { roots, state? } now align through the proof plan.
  • Risk stays below closure until final gates verify pass rows, continuation state, PR non-claim text, and handoff boundaries.
  • Current capped score: 0.92 after the React runtime-provider closure pass.

Next owner: Ralph only when the user invokes it.

Hard Cuts

  • cut hidden nodes for title/settings.
  • cut comment/thread metadata inside raw Slate value by default.
  • cut same-node-object multi-editor sharing.
  • cut React state as the authoritative durable state for document metadata.
  • cut unkeyed global dirty notifications for state field changes.
  • cut initialDocument.
  • cut runtime union Value.
  • cut root keys inside numeric Path.

Implementation Phases

  1. Canonical value and input normalization.
    • owner: .tmp/slate-v2/packages/slate.
    • gate: InitialValue normalization and canonical Value contracts.
  2. Rooted content model.
    • owner: .tmp/slate-v2/packages/slate.
    • gate: root-explicit operations, root-aware points/ranges, refs, dirty paths, history/collab replay contracts.
  3. Runtime/view split.
    • owner: .tmp/slate-v2/packages/slate and .tmp/slate-v2/packages/slate-react.
    • gate: single-root createEditor shortcut plus advanced runtime/view tests.
  4. Core state field descriptors and transaction state patches.
    • owner: .tmp/slate-v2/packages/slate.
    • gate: focused core tests from .tmp/slate-v2.
  5. History patch batching.
    • owner: .tmp/slate-v2/packages/slate-history.
    • gate: undo/redo state patch contracts.
  6. Snapshot persistence API.
    • owner: .tmp/slate-v2/packages/slate.
    • gate: serialize/deserialize roundtrip.
  7. Collab adapter contract.
    • owner: .tmp/slate-v2/packages/slate.
    • gate: fake adapter export/import tests.
  8. React selector/subscription support.
    • owner: .tmp/slate-v2/packages/slate-react.
    • gate: field-key subscriber locality tests.
  9. Examples/docs.
    • owner: site/docs after API stabilizes.
    • gate: title/settings example plus external comments and multi-root view examples.

Fast Driver Gates

  • planning state: node tooling/scripts/completion-check.mjs from /Users/zbeyens/git/plate-2 once the lane is eligible for closure.
  • core source gate: targeted .tmp/slate-v2 package tests for document state.
  • history gate: targeted .tmp/slate-v2 history tests.
  • react gate: targeted .tmp/slate-v2 slate-react selector tests.
  • broad gate before implementation closure: .tmp/slate-v2 bun check, then the relevant focused browser/integration rows if React/browser examples change.

Confidence Scorecard

Current reopened score: 0.92. The previous state-field/rooted-runtime plan remains strong, and the React multi-root provider now has issue, performance, DX, migration, regression, high-risk pressure rows, implementation proof, and browser proof. The lane is closed as implemented; no fixed/improved issue count is changed by this closure pass.

DimensionWeightScoreEvidence
React 19.2 runtime performance0.200.90keyed state dirtiness, descriptor-key subscriptions, rooted dirty paths, runtime/view separation, provider-level listener dedupe, root-view selector budgets, cohort rows, render-count tests, browser row, rerender-breadth benchmark gate, rollback rule, focused provider tests, and multi-root browser proof recorded.
Slate-close unopinionated DX0.200.94keeps initialValue as the only creation input, preserves children as the 99% input shape, hard-breaks runtime Value to canonical roots, uses state.getField / tx.setField, keeps <Slate> as the view provider, enforces prop XOR, and permits optional typed extension aliases without core product verbs.
Plate/slate-yjs migration backbone0.150.93canonical roots, root-explicit operations, state patches, ordered commit records, replay invariants, rollback policy, shared runtime/multiple view policy, history/collab gate, fake adapter route, and PR non-claim sync are explicit.
Regression-proof testing0.200.91named core/history/react/collab/browser/benchmark contracts include value normalization, rooted operations, root-aware points/ranges, runtime/view split, state-only, body-only, mixed, patch-hook, source-subscription, focus/scroll, provider prop boundaries, listener budget, cross-view hooks, placeholder locality, browser root workflows, and release-gate rows.
Research evidence completeness0.150.93compiled lane, local raw ProseMirror/Lexical/Tiptap source/docs, Context7 official-doc check, current Slate v2 source refresh, current issue ledger sync, and PR-reference sync audit.
shadcn-style composability/minimal hooks0.100.93generic descriptor API keeps core minimal while existing extension groups can expose optional typed aliases; runtime/view split keeps UI surfaces out of core state fields; public SlateViewProvider is rejected, <Slate> stays the view provider, and prop XOR keeps the provider API small.

Weighted total: 0.92. The plan is closed as implementation-complete.

Pass State Ledger

Stop-hook note: active goal state is status: done for this lane. A future reopened pass must set it back to pending before doing work.

PassStatusEvidence addedPlan deltaOpen issuesNext owner
runtime-provider-and-multi-root-example-current-statecompletelive source read: .tmp/slate-v2/packages/slate/src/editor-runtime-view.ts, .tmp/slate-v2/packages/slate-react/src/components/slate.tsx, .tmp/slate-v2/packages/slate-react/src/hooks/use-slate-editor.ts, .tmp/slate-v2/site/examples/ts/document-state.tsx, research index/log, live issue ledger, v2 sync ledger, PR referencereopened lane; accepted SlateRuntime as optional common provider, kept public <Slate> as view provider, rejected public SlateViewProvider, added useSlateRuntimeState, useSlateViewState, and separate multi-root example targetissue/reference sync not run in this activation; no fixed/improved issue claimrelated-issue-discovery
related-issue-discoverycompletecache-first scan of live ledger, v2 sync ledger, coverage matrix, fork dossier, open-issue dossiers, and PR reference for #6016, #5537, #5117, #4612, #4477, #4483, #3383, #5515, #3741, #3715, and #3482synced provider API non-claim text; added #5117 fork-dossier section; kept fixed/improved counts unchangedprovider/example proof still pending by designruntime-provider-pressure-pass
runtime-provider-pressure-passcompletelive source read for core runtime/view, React <Slate>, selector bus, state-field hooks, current core/react tests, and document-state example; Vercel React, performance, performance-oracle, high-risk, and tdd lenses appliedadded prop XOR policy, listener/subscription budgets, cohort/memory/INP rows, high-risk failure guards, and vertical red-test order for SlateRuntime, <Slate root>, cross-view hooks, and multi-root examplehandoff still needs one hardening pass before final gatesruntime-provider-handoff-hardening
runtime-provider-handoff-hardeningcompleteRalph-ready handoff, implementation slices, first red tests, required commands, issue sync, and stop rules read and updatedhardened objective, scope lock, accepted target, slice 9/10 requirements, prop-boundary/browser proof rows, issue surface, and stop rules for the runtime provider APIfinal gates still need to audit state, references, and pass rowsclosure-score-final-gates-runtime-provider
closure-score-final-gates-runtime-providercompleteaudited plan top state, scorecard, pass ledger, completion state, continue prompt, issue sync ledger, coverage matrix, fork dossier, PR reference, and Ralph-ready handoffclosed the architecture review as Ralph-ready before implementation; later Ralph rows close the source lane as implemented; no issue count changenonereact-runtime-provider-contract
current-state readcompletelive source, research, solution, issue-ledger rows cited abovecreated plan and target shapenone blockingrelated-issue-discovery
related issue discoverycompletecache-first live/sync/fork/matrix rows plus sync-note overrideno new Fixes claim; reviewed surface classifiedrow-level rewrite decision handed to issue-ledger passissue-ledger-pass
issue-ledger passcompletefull ledger, cluster, test, benchmark, package, requirements, matrix, dossier, and PR-reference scankept sync-note override; added #4612 adjacent-state boundary; no count or PR-reference changenone for this passresearch/ecosystem-refresh
intent/boundarycompletescope/non-goals recordedtarget narrowednonedecision brief hardening
research/ecosystemcompletecompiled research, local raw ProseMirror/Lexical/Tiptap docs/source, Context7 official-doc check, and current Slate v2 source refreshstrategy table strengthened; rejected Lexical RootNode metadata as Slate target; no new research page needednone for this passperformance-dx-migration-regression-pressure
performance/DX/migration/regressioncompletecurrent source paths plus performance rules read; initialDocument, keyed dirtiness, cohorts, budgets, and regression contracts addedremoved bad value: { children, state } draft; added dirtyStateKeys, state source, and descriptor diff pressureno implementation proof yetsteelman-maintainer-objection
steelman maintainer-objectioncompletecurrent extension slot source plus steelman/high-risk skill rules readadded detailed objection ledger; accepted optional typed extension aliases, replay invariants, descriptor-key subscriptions, and patch-hook constraintsnone for this passhigh-risk-deliberate-proof-expansion
high-risk deliberate proof expansioncomplete.tmp/slate-v2 package scripts, test families, benchmark scripts, representative contracts, and high-risk/tdd rules readexpanded blast radius, proof gates, rollback/remediation, Ralph entry gates, and vertical TDD orderimplementation proof still pending by designrevision-and-handoff-hardening
revision and handoff hardeningcompleteplan, completion state, continuation prompt, issue ledgers, PR reference, and Ralph skill readadded Ralph-ready handoff, locked public API names, synced PR reference as non-claim, raised score to thresholdnoneclosure-score-final-gates
closure-score/final-gatescompleterequirement audit, state file sync, continuation sync, PR non-claim check, issue-sync check, learning checkclosed plan as Ralph-ready; no source implementation startednoneRalph only when user invokes it
jotai atom granularity current-state readcompletecurrent plan/source, solution notes, Context7 Jotai atom/store/select/focus/split docsreopened API target around defineEditorStateField, initialDocument.state, state.fields, tx.fields, dirtyStateKeys, and source 'state'follow-up passes must reconcile store wording, issue impact, objections, and final gatesrelated-issue-discovery
state-field related issue/reference synccompletelive issue rows, v2 sync ledger, coverage matrix, PR reference non-claim sectionno new issue counts; synced PR reference and v2 sync note to state-field APInoneissue-ledger-pass
state-field issue-ledger passcompletelive rows, sync note, frozen corpus, cluster rows, fork dossier, coverage matrix, and PR reference read; #4612 matrix note tightenedno count changes; PR reference and sync ledger stay non-claim; state-field pressure set confirmednone for this passstate-field terminology/handoff hardening
state-field terminology/handoff hardeningcompleteactive plan authority, proof rows, high-risk proof plan, maintainer objection ledger, Ralph-ready handoff, and previous store-based handoff readcurrent API/proof/handoff wording moved to state-field vocabulary; remaining store wording is external/source/historical onlynone for this passstate-field maintainer objection and risk pass
state-field maintainer objection and risk passcompletefinal authority, proof rows, performance/DX contract, high-risk proof plan, maintainer objection ledger, Ralph-ready handoff, and state files readkept state fields, statePatches, dirtyStateKeys, field-key subscriptions, optional typed aliases, patch-hook guard, external comments, and deferred multi-root/shared-runtime scopenone for this passclosure-score-final-gates-state-field-final
closure-score/final-gates-state-field-finalcompleteplan top, final authority, pass ledger, Ralph-ready handoff, completion state, continue prompt, PR reference, sync ledger, and coverage matrix readclosed lane as Ralph-ready; no .tmp/slate-v2 implementation claim or issue count changenoneRalph only when user invokes it
latest-api authority refreshcompletecurrent user decision chain, active plan, live Slate v2 operation/value source, and stale completion/continue state readcurrent authority now uses defineStateField, canonical Value = { roots, state? }, InitialValue, state.getField, tx.setField, rooted operations, root-aware points/ranges, and runtime/view splitclosed by later issue/reference/proof syncissue-reference-and-proof-sync-latest-api
issue-reference-and-proof-sync-latest-apicompletePR reference, v2 sync ledger, issue coverage matrix, active proof rows, and Ralph handoff readsynced non-claim PR note, #4612 coverage row, sync ledger, proof commands, and final summary to latest API namesnone; no fixed/improved count changeclosure-score-final-gates-latest-api-final
closure-score-final-gates-latest-api-finalcompleteplan top, authority, scorecard, pass ledger, final handoff, completion state, continue prompt, PR reference, sync ledger, and coverage matrix readclosed latest-API lane as Ralph-ready; no .tmp/slate-v2 implementation claim or issue count changenoneRalph only when user invokes it
state-field-policy-shorthand-dxcompletecurrent authority, public API target, runtime descriptor type, history policy, Ralph handoff, and final summary readchanged history/collab examples and descriptor type to shorthand-first DX with object policy escape hatchesnone; no issue/reference count changeclosure-score-final-gates-state-field-policy-shorthand
closure-score-final-gates-state-field-policy-shorthandcompleteplan top, authority, policy type, handoff, final summary, completion state, and continue state readclosed policy-shorthand update; no .tmp/slate-v2 implementation claimnoneRalph only when user invokes it
ralph tdd canonical value/initialvaluecompletered: bun test ./packages/slate/test/create-editor-value-contract.ts failed on object initialValue; green: bun test ./packages/slate/test/create-editor-value-contract.ts ./packages/slate/test/state-tx-public-api-contract.ts, bun test ./packages/slate/test, bun --filter slate typecheck, bun --filter slate-dom typecheck, bun --filter slate-react typecheck, and bun lint:fix passedadded first runtime normalization slice for legacy children, { children, state }, and { roots, state }; state.value.get() now returns canonical rooted value; PR reference synced as non-claim first-slice prooffull plan remains pending; rooted operations are nextrooted-operation-contract
ralph rooted-operation-contractcompletered: bun test ./packages/slate/test/rooted-operation-contract.ts failed because committed insert_text lacked root and a header Point transformed against a main-root operation; green: bun test ./packages/slate/test/rooted-operation-contract.ts ./packages/slate/test/create-editor-value-contract.ts ./packages/slate/test/state-tx-public-api-contract.ts, bun test ./packages/slate/test, bun --filter slate typecheck, bun --filter slate-dom typecheck, bun --filter slate-react typecheck, and bun lint:fix passedadded root-stamped content operations, root-local Point/Range transforms, root-preserving selection clone, and tightened NodeIn<V> so editor objects do not leak into node transform genericsfull plan remains pending; runtime/view split is nexteditor-runtime-view-contract
ralph editor-runtime-view-contractcompletered: bun test ./packages/slate/test/editor-runtime-view-contract.ts failed because createEditorRuntime and createEditorView were missing; green: bun test ./packages/slate/test/editor-runtime-view-contract.ts ./packages/slate/test/rooted-operation-contract.ts ./packages/slate/test/create-editor-value-contract.ts ./packages/slate/test/state-tx-public-api-contract.ts, bun test ./packages/slate/test, bun --filter slate typecheck, bun --filter slate-dom typecheck, bun --filter slate-react typecheck, and bun lint:fix passedadded createEditorRuntime, createEditorView, state.view, view-local root/focus/read-only policy, and root-bound read/update facades over one runtime editorfull plan remains pending; document state fields are nextdocument-state-contract
ralph document-state-contractcompletered: bun test ./packages/slate/test/document-state-contract.ts failed because defineStateField was missing; green: bun test ./packages/slate/test/document-state-contract.ts ./packages/slate/test/editor-runtime-view-contract.ts ./packages/slate/test/rooted-operation-contract.ts ./packages/slate/test/create-editor-value-contract.ts ./packages/slate/test/state-tx-public-api-contract.ts, bun test ./packages/slate/test, bun --filter slate typecheck, bun --filter slate-dom typecheck, bun --filter slate-react typecheck, and bun lint:fix passedadded defineStateField, state-field descriptor types, persisted initial state registration, and state.getField(field)full plan remains pending; state patch writes are nextdocument-state-patch-contract
ralph document-state-patch-contractcompletered: bun test ./packages/slate/test/document-state-patch-contract.ts failed because tx.setField was missing, then failed because large shared/history fields lacked a patch-hook guard; green: bun test ./packages/slate/test/document-state-patch-contract.ts ./packages/slate/test/document-state-contract.ts ./packages/slate/test/editor-runtime-view-contract.ts ./packages/slate/test/rooted-operation-contract.ts ./packages/slate/test/create-editor-value-contract.ts ./packages/slate/test/state-tx-public-api-contract.ts, bun test ./packages/slate/test, bun --filter slate typecheck, bun --filter slate-dom typecheck, bun --filter slate-react typecheck, and bun lint:fix passedadded tx.setField, EditorCommit.statePatches, dirtyStateKeys, StateFieldDescriptor patch hooks, 'state' source publication, rollback-safe state writes, and a large shared/history patch-hook guardfull plan remains pending; state history is nextdocument-state-history-contract
ralph document-state-history-contractcompletered: bun test ./packages/slate-history/test/document-state-history-contract.ts failed because state.history.undos() stayed empty after a state-only title commit; green: bun test ./packages/slate-history/test/document-state-history-contract.ts, bun test ./packages/slate-history/test/index.spec.ts ./packages/slate-history/test/history-contract.ts ./packages/slate-history/test/integrity-contract.ts ./packages/slate-history/test/document-state-history-contract.ts, bun test ./packages/slate/test, bun --filter slate typecheck, bun --filter slate-history typecheck, bun --filter slate-dom typecheck, bun --filter slate-react typecheck, and bun lint:fix passedadded Batch.statePatches, internal applyStatePatches, state-only undo/redo replay through historic operation-free commits, and stale rooted-operation expectations in history integrity testsfull plan remains pending; collab state transport is nextcollab-document-state-contract
ralph collab-document-state-contractcompletered: bun test ./packages/slate/test/collab-document-state-contract.ts failed on missing tx.statePatches.replay, then on missing Editor.getCollabStatePatches; green: bun test ./packages/slate/test/collab-document-state-contract.ts, bun test ./packages/slate/test/collab-adapter-extension-contract.ts ./packages/slate/test/collab-history-runtime-contract.ts ./packages/slate/test/collab-canonical-reconcile-contract.ts ./packages/slate/test/collab-bookmark-position-contract.ts ./packages/slate/test/collab-selection-stress-contract.ts ./packages/slate/test/collab-document-state-contract.ts, bun test ./packages/slate/test, bun --filter slate typecheck, bun --filter slate-history typecheck, bun --filter slate-dom typecheck, bun --filter slate-react typecheck, and bun lint:fix passedadded low-level tx.statePatches.replay for remote imports and Editor.getCollabStatePatches so adapters export only collab: 'shared' field patchesfull plan remains pending; React state-field selector hooks are nextstate-field-selector-contract
ralph state-field-selector-contractcompletered: bun --filter slate-react test:vitest -- state-field-selector-contract.test.tsx failed because useStateFieldValue was missing; broad provider hook run also exposed stale canonical-value assertions and a real state.nodes.children() root bug; green: bun --filter slate-react test:vitest -- state-field-selector-contract.test.tsx, bun --filter slate-react test:vitest -- provider-hooks-contract.test.tsx state-field-selector-contract.test.tsx, bun --filter slate-react test:vitest, bun test ./packages/slate/test, bun test ./packages/slate/test/create-editor-value-contract.ts ./packages/slate/test/collab-document-state-contract.ts, bun --filter slate typecheck, bun --filter slate-react typecheck, bun --filter slate-dom typecheck, bun --filter slate-history typecheck, and bun lint:fix passedadded useStateFieldValue, useSetStateField, field-dirty selector filtering, a setter through tx.setField, root state.nodes.children() support, and React provider test updates for canonical state.value.get()full plan remains pending; browser/example proof is nextbrowser-example-proof
ralph browser-example-proofcompletered: PLAYWRIGHT_RETRIES=0 bun playwright playwright/integration/examples/document-state.test.ts --project=chromium first failed because stale examples treated canonical state.value.get() as raw children, then failed because openExample selected the title textbox instead of the Slate editable; green: bun typecheck:site, PLAYWRIGHT_RETRIES=0 bun playwright playwright/integration/examples/document-state.test.ts --project=chromium, dev-browser --connect http://127.0.0.1:9222 against http://localhost:3100/examples/document-state with screenshot /Users/zbeyens/.dev-browser/tmp/slate-document-state.png, bun --filter slate-react typecheck, bun --filter slate typecheck, bun typecheck:root, and bun lint:fix passedadded the Document State example using defineStateField, initialValue: { children, state }, useStateFieldValue, useSetStateField, tx.history.undo/redo, and tx.statePatches.replay; fixed stale site examples to read body children from state.runtime.snapshot().children or tx.nodes.children() instead of canonical state.value.get()full plan remains pending; performance/release gates are nextperformance-release-gates
ralph performance-release-gatescompletemissing-script classification: bun run bench:core:editor-state-field:local and bun run bench:core:rooted-operation-transform:local are not defined in current package.json; green gates: bun bench:core:editor-store:local, bun bench:core:history-retained-memory:local, bun bench:core:collab-readiness:local, bun bench:react:rerender-breadth:local, bun test:bun, bun test:vitest, and bun check passedrepaired stale benchmark harnesses from removed withHistory/register/commitListeners APIs to current history() and setup/onCommit; changed root test to explicit Bun-owned packages plus Vitest so root check no longer runs browser-only/Vitest files under Bun; updated stale canonical-value tests to read body children from runtime snapshots or node APIsfull plan remains pending; final gates are nextfinal-gates
ralph final-gatescompleteaudited plan top status, pass ledger, completion state, continuation prompt, PR reference, browser evidence, benchmark evidence, and final bun check evidencemarked execution lane done; missing state-field/rooted-operation benchmark scripts remain recorded as future harness coverage, not release proof; no fixed/improved issue count changednonenone
ralph react-runtime-provider-contractcompletered/green cd packages/slate-react && bunx vitest run --config ./vitest.config.mjs test/slate-runtime-provider-contract.test.tsx; bun --filter slate-react typecheck; bun --filter slate typecheck; bun lint:fixadded SlateRuntime, useSlateRuntime, useSlateRuntimeState, useSlateViewState, root-bound <Slate>, provider-boundary errors, runtime selector/listener bridge, and root-local view readsmulti-root browser proof remained pending at this pointmulti-root-example-browser-proof
ralph multi-root-example-browser-proofcompletePLAYWRIGHT_RETRIES=0 bun playwright playwright/integration/examples/multi-root-document.test.ts --project=chromium; dev-browser --connect http://127.0.0.1:9222 against http://localhost:3100/examples/multi-root-documentadded the multi-root document example, active-root editing model, root-local selection ownership, rooted set_selection, inactive-root export guards, and route/sidebar wiringnonefinal-gate-sync-runtime-provider
final-gate-sync-runtime-providercompletebun check, focused multi-root Playwright, focused provider/core tests, focused slate-react regression tests, bun lint:fix, and dev-browser proofsynced plan top, completion state, and continuation prompt to implementation-complete; no issue count changenonenone
header-focus-regressioncompletered then green: PLAYWRIGHT_RETRIES=0 bun playwright playwright/integration/examples/multi-root-document.test.ts --project=chromium -g "focuses the header editor"; green full file: PLAYWRIGHT_RETRIES=0 bun playwright playwright/integration/examples/multi-root-document.test.ts --project=chromium; green bun lint:fix; green bun typecheck:site; green dev-browser --connect http://127.0.0.1:9222 label-click proof; learning captured in docs/solutions/ui-bugs/2026-05-21-slate-v2-multi-root-chrome-clicks-must-activate-root-before-focus.mdroot chrome clicks now activate the root, make the root editable before focus handling, focus the editable, and put follow-up typing in the headernonenone
header-text-surface-caret-regressioncompletered then green: PLAYWRIGHT_RETRIES=0 bun playwright playwright/integration/examples/multi-root-document.test.ts --project=chromium -g "inactive header text surface"; green full file: PLAYWRIGHT_RETRIES=0 bun playwright playwright/integration/examples/multi-root-document.test.ts --project=chromium; green bun lint:fix; green bun typecheck:site; green Browser plugin proof with selectionAnchorInHeader: true and header-only typingremoved the inactive-root readOnly toggle so all root text surfaces stay natively editable; kept chrome-click activation/focus handoff for non-editable labels/badgesnonenone
header-sequential-key-order-regressioncompleteBrowser plugin reproduced the bug by clicking the header and pressing ordered keys h, e, l, l, o, yielding ollehConfidential quarterly plan; green: bun -e Playwright probe against http://localhost:3100/examples/multi-root-document yielded Confidential quarterly planhello; green: bun test ./packages/slate/test/rooted-operation-contract.ts ./packages/slate/test/create-editor-value-contract.ts ./packages/slate/test/state-tx-public-api-contract.ts ./packages/slate/test/editor-runtime-view-contract.ts; green: bun --filter slate typecheck; green: bun typecheck:site; green: bun lint:fix; green after lint: PLAYWRIGHT_RETRIES=0 bun playwright playwright/integration/examples/multi-root-document.test.ts --project=chromiumroot-bound views now stamp rootless imported selection points onto the view root; the multi-root browser row presses ordered keys and asserts hello is present while olleh is absent; learning note updatednonenone
cursor-selection-drift-current-state-readcompletelive source read: core rooted operations and view root scoping in .tmp/slate-v2/packages/slate/src/interfaces/operation.ts:13, .tmp/slate-v2/packages/slate/src/core/public-state.ts:456, .tmp/slate-v2/packages/slate/src/core/public-state.ts:2079, .tmp/slate-v2/packages/slate/src/editor-runtime-view.ts:52; React selection import/export and provenance in .tmp/slate-v2/packages/slate-react/src/editable/selection-controller.ts:533, .tmp/slate-v2/packages/slate-react/src/editable/runtime-selection-engine.ts:30, .tmp/slate-v2/packages/slate-react/src/editable/editing-kernel.ts:265; regression rows in .tmp/slate-v2/packages/slate/test/editor-runtime-view-contract.ts:89 and .tmp/slate-v2/playwright/integration/examples/multi-root-document.test.ts:155; ecosystem evidence in ../prosemirror/state/src/transaction.ts:26, ../prosemirror/view/src/selection.ts:9, ../lexical/packages/lexical/src/LexicalEditorState.ts:105, and ../lexical/packages/lexical/src/LexicalUpdates.ts:616current architecture is good but not absolute-best; accepted a targeted selection-authority consolidation: internal rooted selection invariant, single event-frame import/export boundary, root/provenance dev asserts, authority inventory guards, and generated browser gauntletsno issue/reference sync in this activation; next pass must classify the related selection/focus/history issue surfacerelated-issue-discovery
related-issue-discoverycompletecache-first reads: docs/slate-v2/ledgers/fork-issue-dossier.md, docs/slate-v2/ledgers/issue-coverage-matrix.md, docs/slate-issues/gitcrawl-v2-sync-ledger.md, and docs/slate-issues/gitcrawl-live-open-ledger.md; focused rows covered selection, cursor, focus, undo/redo/history, multi-root, browser, beforeinput, composition, paste, and drop pressureselection drift rewrite is issue-backed by recurring families, not just the multi-root demo bug; no new fixed/improved claim is justified without exact browser/device proofsynced non-claim cluster note in docs/slate-issues/gitcrawl-v2-sync-ledger.md; no PR reference count changeclosure-score-final-gates-cursor-selection-drift
closure-score-final-gates-cursor-selection-driftcompleteaudited plan top state, cursor-selection architecture section, related issue discovery section, pass ledger, completion state, continuation prompt, and sync ledger note; ran node tooling/scripts/completion-check.mjs after state syncclosed the Ralplan review as Ralph-ready; selection-authority consolidation remains an implementation target, not a completed Slate v2 source/browser claimnone for this planning laneRalph only when user invokes it
react-prosemirror-lifecycle-amendmentcompleteread ../react-prosemirror/src/hooks/useEditor.ts:33, ../react-prosemirror/src/hooks/useEditorEffect.ts:12, ../react-prosemirror/src/hooks/useEditorEventCallback.ts:25, and ../react-prosemirror/src/ReactEditorView.ts:124 / :275added React lifecycle access as part of SelectionFrame: lifecycle phase, view/commit epoch, runtime-owned event callback access, post-commit layout-effect access, static guard against stale render/effect DOM selection imports, and proof rownone; no implementation or issue-count claimnone
ralph-selection-lifecycle-frame-contractcompletered: bun test:vitest -- editing-kernel-contract.test.ts failed because editable event frames lacked lifecycle/epoch metadata and non-event lifecycle frames could import DOM selection; green: same focused Vitest passed; bun --filter slate-react typecheck passed; bun lint:fix passed; focused Vitest passed again after lintadded EditableReactLifecyclePhase, frame lifecyclePhase, viewEpoch, commitEpoch, and a dev/test transition guard rejecting DOM selection import from non-event lifecycle framesno issue/reference claim change; ClawSweeper already swept this selection/focus/history surfacediff-review-pass
diff-review-pass-selection-lifecycle-framecompletereviewed packages/slate-react/src/editable/editing-kernel.ts and packages/slate-react/test/editing-kernel-contract.ts difffixed the gap where missing frames still allowed import-dom; added frame root ownership defaulting to state.view.root(); no remaining P0/P1/P2 findingsno issue/reference claim changeverification-sweep-pass
verification-sweep-pass-selection-lifecycle-framecompletegreen: bun test:vitest -- editing-kernel-contract.test.ts; green: bun --filter slate-react typecheck; green: bun --filter slate-react test:vitest; green: bun lint:fix; green browser regression: PLAYWRIGHT_RETRIES=0 bun playwright playwright/integration/examples/multi-root-document.test.ts --project=chromium; green: bun test:vitest -- kernel-authority-audit-contract.test.ts; green: node tooling/scripts/completion-check.mjs after state syncclosed the lifecycle/root-frame implementation slice; this does not claim the whole future generated matrix or absolute browser robustnessnonenone
cross-root-history-caret-regressioncompletered: bun test ./packages/slate/test/editor-runtime-view-contract.ts failed because undoing the second, header-owned batch restored header selection through a main-view command; red browser row reproduced stacked header/body edits where the second toolbar undo moved the body caret; green: bun --filter slate-history build && bun test ./packages/slate-history/test ./packages/slate/test/editor-runtime-view-contract.ts; green: PLAYWRIGHT_RETRIES=0 bun playwright playwright/integration/examples/multi-root-document.test.ts --project=chromium; green: bun --filter slate-history typecheck && bun --filter slate typecheck && bun typecheck:site; green: bun lint:fixview-scoped history restore now skips cross-root selection replay; toolbar history executes through the active root view and refocuses that editable while preserving the DOM rangeno fixed/improved issue claim changenone

Ralph-Ready Handoff

Use this only after the user explicitly invokes [$ralph]. Slate Ralplan does not edit .tmp/slate-v2 source.

Objective

Implement Slate v2's latest editor value architecture: canonical rooted Value, ergonomic InitialValue, atom-like state fields, root-explicit content operations, one document runtime with root-bound editor views, and the React runtime-provider API for multi-root examples. Document title/settings are state fields. Header/footer/global editable regions are roots. Comments remain external anchored stores by default.

Scope Lock

Allowed implementation owners in .tmp/slate-v2:

  • packages/slate/src/interfaces/editor.ts
  • packages/slate/src/interfaces/operation.ts
  • packages/slate/src/create-editor.ts
  • packages/slate/src/core/public-state.ts
  • packages/slate/src/core/editor-extension.ts only if field descriptor registration needs extension setup support.
  • root-aware transform/location helpers touched by Point, Range, and operation replay.
  • packages/slate-history/src/history-extension.ts
  • packages/slate-react/src/components/slate.tsx
  • packages/slate-react/src/hooks/** and selector/subscription support.
  • packages/slate-react/src/index.ts only for public React exports.
  • site/examples/ts/document-state.tsx only for keeping the focused single-root state-field example current.
  • a separate site example for the multi-root/header-footer provider API.
  • focused tests and examples named in the proof plan.

Forbidden in the first Ralph pass:

  • hidden title/settings/header/footer nodes.
  • runtime Value as Element[], a union, or { children, state }.
  • root keys inside numeric Path.
  • product comment/thread services in raw Slate.
  • shared node object identity across editors.
  • generic custom-operation escape hatches.
  • global dirtyScope: all for state field writes.
  • public atom naming in raw Slate.
  • public SlateViewProvider.
  • <Slate root> without SlateRuntime.
  • <Slate editor root> with two competing authorities.

Accepted Public Target

  • defineStateField(descriptor).
  • canonical Value = { roots: Record<RootKey, Element[]>, state? }.
  • InitialValue = Element[] | { children, state? } | { roots, state? }.
  • createEditor({ initialValue: children }) normalizes to { roots: { main: children } }.
  • createEditor({ initialValue: { children, state } }) is the 99% document state constructor.
  • createEditor({ initialValue: { roots, state } }) is the multi-root constructor.
  • state.getField(field).
  • tx.setField(field, valueOrUpdater).
  • EditorCommit.statePatches.
  • EditorCommit.dirtyStateKeys.
  • EditorCommitSource literal 'state'.
  • policy shorthands: history: 'push', history: 'skip', collab: 'shared', collab: 'local'; object policy form only for extra metadata.
  • root-explicit operations while Path stays numeric and root-local.
  • root-aware Point and Range.
  • createEditorRuntime({ initialValue }).
  • createEditorView(runtime, { root }).
  • useSlateRuntime({ initialValue }).
  • <SlateRuntime runtime={runtime}>.
  • <Slate root="header"> inside SlateRuntime creates a root-bound view.
  • <Slate> inside SlateRuntime defaults to root="main".
  • <Slate editor={editor}> remains the single-editor shortcut.
  • <Slate root> outside SlateRuntime throws.
  • <Slate editor={editor} root="header"> throws.
  • nested SlateRuntime providers isolate their runtimes.
  • <Slate root readOnly> maps to view-local read-only policy.
  • useSlateRuntimeState(selector, options?).
  • useSlateViewState(root, selector, options?).
  • useStateFieldValue(field, selector?, options?).
  • useSetStateField(field).
  • optional typed extension aliases through existing state / tx extension groups, never core product verbs.

Implementation Slices

  1. Canonical value and input normalization.
    • Hard-break runtime/persisted Value to { roots, state? }.
    • Keep InitialValue convenience for Element[], { children, state? }, and { roots, state? }.
    • First proof: bun test ./packages/slate/test/create-editor-value-contract.ts.
  2. Root identity and runtime/view split.
    • Add root-aware locations and root-explicit committed content operations.
    • Add document runtime ownership and root-bound editor views.
    • First proof: bun test ./packages/slate/test/rooted-operation-contract.ts and bun test ./packages/slate/test/editor-runtime-view-contract.ts.
  3. Core state field registry.
    • Add defineStateField, descriptor registration, persisted initialization, serialization, and state.getField.
    • First proof: bun test ./packages/slate/test/document-state-contract.ts.
  4. Core state field writes and commit dirtiness.
    • Add tx.setField, state patch capture, rollback, dirtyStateKeys, and 'state' source publication.
    • Prove state writes do not become content operations.
  5. Patch policy and metadata.
    • Add whole-value patches for small fields and patch-hook guard for large historyable/shared fields.
    • Extend commit metadata tests for ordered mixed operation/state commits.
  6. History.
    • Teach history batches to invert/replay operations plus state patches.
    • Prove title push, title typing merge, preference skip, mixed undo/redo, and remote import skip.
  7. Collab substrate.
    • Export local shared state patches; suppress local/external policies; import remote patches with history skip and selection side-effect suppression.
  8. React selector locality.
    • Add useStateFieldValue and useSetStateField.
    • Prove title edits wake exact field selectors only and body edits wake no field selectors.
  9. React shared runtime provider.
    • Add useSlateRuntime, SlateRuntime, <Slate root>, useSlateRuntimeState, and useSlateViewState.
    • Keep public <Slate> as the view provider; do not export SlateViewProvider.
    • Enforce prop XOR: either <Slate editor> outside a runtime or <Slate root> inside a runtime, never both.
    • Deduplicate document focus/focusout listeners and editor subscription bridge per runtime, not per root view.
    • Root-view selectors must fan out by dirty root/state key; body edits must not rerender header/footer selectors.
    • Prove sibling root reads subscribe through the runtime bus and do not require prop-drilled sibling editors.
  10. Comments and examples.
  • Keep comments external by default.
  • Keep Document State focused on title/settings state fields.
  • Add a separate Multi-root Document or Headers and Footers example using SlateRuntime, Slate root="header", Slate root="main", and Slate root="footer".
  • The multi-root example must prove header/body/footer focus, placeholder measurement, undo/redo, select-all/copy/paste, and follow-up typing.
  1. Performance and release gates.
  • Run focused benchmarks, package typechecks, then bun check; run bun check:full only when browser/example behavior changed enough to claim release proof.

First Red Tests

Write one failing public contract at a time:

  1. packages/slate/test/create-editor-value-contract.ts
  2. packages/slate/test/rooted-operation-contract.ts
  3. packages/slate/test/editor-runtime-view-contract.ts
  4. packages/slate/test/document-state-contract.ts
  5. packages/slate/test/document-state-patch-contract.ts
  6. packages/slate-history/test/document-state-history-contract.ts
  7. packages/slate/test/collab-document-state-contract.ts
  8. packages/slate-react/test/state-field-selector-contract.test.tsx
  9. packages/slate-react/test/slate-runtime-provider-contract.test.tsx
  10. playwright/integration/examples/document-state.test.ts
  11. playwright/integration/examples/multi-root-document.test.ts

Do not write the whole suite upfront.

Required Commands

Focused gates from .tmp/slate-v2:

  • bun test ./packages/slate/test/create-editor-value-contract.ts
  • bun test ./packages/slate/test/rooted-operation-contract.ts
  • bun test ./packages/slate/test/editor-runtime-view-contract.ts
  • bun test ./packages/slate/test/document-state-contract.ts
  • bun test ./packages/slate/test/document-state-patch-contract.ts
  • bun test ./packages/slate/test/commit-metadata-contract.ts
  • bun --filter slate typecheck
  • bun test ./packages/slate-history/test/document-state-history-contract.ts
  • bun --filter slate-history typecheck
  • cd packages/slate-react && bunx vitest run --config ./vitest.config.mjs test/state-field-selector-contract.test.tsx
  • cd packages/slate-react && bunx vitest run --config ./vitest.config.mjs test/slate-runtime-provider-contract.test.tsx
  • bun --filter slate-react typecheck
  • PLAYWRIGHT_RETRIES=0 bun playwright playwright/integration/examples/document-state.test.ts --project=chromium
  • PLAYWRIGHT_RETRIES=0 bun playwright playwright/integration/examples/multi-root-document.test.ts --project=chromium
  • dev-browser --connect http://127.0.0.1:9222 against http://localhost:3100/examples/document-state
  • dev-browser --connect http://127.0.0.1:9222 against http://localhost:3100/examples/multi-root-document

The slate-runtime-provider-contract test must include:

  • <SlateRuntime runtime><Slate /></SlateRuntime> binds main.
  • <Slate root="header"> binds header.
  • <Slate root> outside runtime throws.
  • <Slate editor root> throws.
  • nested runtimes isolate selectors.
  • document listeners and editor subscription bridge are deduped per runtime.
  • useSlateViewState('header', selector) ignores body-only commits.
  • useSlateRuntimeState reads runtime state without binding to one root.
  • read-only root view rejects view writes while writable root edits.

The multi-root-document browser row must include:

  • click/type header, body, and footer.
  • edit title/state field without focus stealing.
  • undo/redo in header and in title/state field.
  • select-all/copy/paste per root.
  • placeholder measurement does not leak between roots.
  • follow-up typing after selection repair stays in the active root.

Broad gates from .tmp/slate-v2:

  • bun bench:core:editor-store:local
  • bun bench:core:history-retained-memory:local
  • bun bench:core:collab-readiness:local
  • bun bench:react:rerender-breadth:local
  • bun test:bun
  • bun test:vitest
  • bun check
  • missing harness coverage to add later: bench:core:editor-state-field:local and bench:core:rooted-operation-transform:local.
  • bun check:full is not required for this close because the browser changed route already has focused Playwright plus dev-browser proof and this lane is not making release-quality raw-device/browser claims.

Issue And Reference Sync

  • No Fixes #... claim belongs to this plan.
  • Current related/non-fix issue surface is #4477/#4483/#5987/#3383/#5515/#3741/#3715/#4612/#3705/#3756/#3921/#6016/#5537/#5117/#3482.
  • Keep docs/slate-issues/gitcrawl-v2-sync-ledger.md as the current sync note owner for this architecture pass.
  • Keep docs/slate-v2/ledgers/issue-coverage-matrix.md unchanged unless Ralph implementation produces a new exact fixed/improved proof.
  • docs/slate-v2/references/pr-description.md is synced only as a non-claim future API note. Counts stay unchanged.

Stop Rules

  • If runtime Value remains a union or raw Element[], stop and fix the canonical value shape.
  • If 99% single-root construction requires roots boilerplate, stop and fix InitialValue normalization.
  • If root identity gets embedded inside numeric Path, stop and fix rooted location design.
  • If document runtime and editor view ownership blur, stop before React/browser examples.
  • If multi-root React examples require prop-drilling sibling editor objects, stop and add the shared runtime provider first.
  • If public API needs SlateViewProvider, stop and reuse <Slate> as the view provider.
  • If <Slate root> works without SlateRuntime, stop and add the provider boundary error.
  • If <Slate editor root> works, stop and enforce prop XOR.
  • If root views multiply document focus listeners or selector buses, stop and move fanout to SlateRuntime.
  • If useSlateViewState reads sibling React context instead of runtime/root subscriptions, stop and fix the hook boundary.
  • If header/body/footer placeholder measurement leaks across roots, stop before publishing the multi-root example.
  • If state field writes require dirtyScope: all, stop and redesign dirtiness.
  • If title typing rerenders body blocks, stop before examples.
  • If history/collab cannot invert/replay mixed commits, keep state fields internal/experimental and default history/collab to skip/local.
  • If browser state import steals focus or scroll, default imports to preserve DOM selection/focus/scroll and do not publish the example.

Runtime Provider Handoff Hardening Pass - 2026-05-21

Status: complete for this activation only. The broader Ralplan lane remains pending because final gates still have to audit pass rows, state files, reference sync, and closure criteria.

Evidence read:

  • active plan top state, runtime-provider verdict, pressure pass, pass ledger, Ralph-ready handoff, first red tests, required commands, issue sync, and stop rules.
  • active completion and continuation files under .tmp/019e3627-238b-7993-a8cf-26be45504c47/.
  • ralph skill handoff contract.

Plan deltas:

  • Objective now includes the React runtime-provider API, not only core rooted value/state fields.
  • Scope lock now names packages/slate-react/src/components/slate.tsx, React exports, the existing Document State example, and the separate multi-root/header-footer example.
  • Accepted target now records prop boundaries: <Slate> inside SlateRuntime defaults to main, <Slate root> outside a runtime throws, <Slate editor root> throws, nested runtimes isolate, and <Slate root readOnly> maps to view-local read-only policy.
  • Implementation slice 9 now requires runtime-owned subscription/focus fanout, prop XOR, root-filtered selector updates, and no public SlateViewProvider.
  • Implementation slice 10 now requires a separate multi-root example that proves focus, placeholder measurement, undo/redo, select-all/copy/paste, and follow-up typing across header/body/footer roots.
  • Red tests and command gates now spell out the expected provider-contract rows instead of relying on a vague test filename.
  • Issue sync surface now includes #5537 and #5117 beside the previous non-node state issue set.
  • Stop rules now fail fast on missing runtime boundary errors, accepted editor + root, listener/selector fanout per root, sibling-context cross-view hooks, and placeholder leakage.

Decision:

  • Keep the public API as SlateRuntime plus public <Slate> root views.
  • The handoff is now Ralph-ready for the runtime-provider implementation slice.
  • Do not mark the lane done until closure-score-final-gates-runtime-provider verifies the whole reopened schedule.

Closure-Score/Final-Gates Runtime Provider Pass - 2026-05-21

Status: complete.

Evidence read:

  • active plan top, current verdict, runtime-provider decision, scorecard, pass ledger, Ralph-ready handoff, issue sync, required commands, stop rules, and completion-state files.
  • active goal state.
  • active goal state.
  • docs/slate-issues/gitcrawl-v2-sync-ledger.md.
  • docs/slate-v2/ledgers/issue-coverage-matrix.md.
  • docs/slate-v2/ledgers/fork-issue-dossier.md.
  • docs/slate-v2/references/pr-description.md.

Requirement audit:

RequirementEvidenceResult
Runtime-provider API authorityReact multi-root DX target, Provider naming decision, Runtime Provider Pressure Pass, and Ralph-Ready Handoff define SlateRuntime, <Slate root>, prop XOR, and no public SlateViewProvider.complete
Handoff readinessRalph-Ready Handoff names objective, scope, public target, implementation slices, red tests, commands, issue sync, and stop rules.complete
Related issue accountingRelated Issue Discovery Pass - 2026-05-21, gitcrawl-v2-sync-ledger, coverage matrix, fork dossier #5117, and PR reference all keep fixed/improved counts unchanged.complete
Performance and React pressureRuntime Provider Pressure Pass records Vercel React rules, repeated units, listener/subscription budgets, cohorts, INP rows, memory tags, native-behavior proof, and dashboard tags.complete
Regression proof planRegression proof additions, First Red Tests, and Required Commands name package/browser rows for prop boundaries, cross-view hooks, listener budget, placeholder locality, and multi-root browser workflows.complete
Workspace verification boundaryRalph implementation rows now cite focused provider/core tests, focused Playwright, dev-browser, and bun check before implementation closure.complete

Ralph React Runtime Provider Contract Pass - 2026-05-21

Status: complete.

Evidence:

  • red: cd packages/slate-react && bunx vitest run --config ./vitest.config.mjs test/slate-runtime-provider-contract.test.tsx failed on missing useSlateRuntime, missing provider boundary, and missing <Slate root> API.
  • green after implementation and lint: cd packages/slate-react && bunx vitest run --config ./vitest.config.mjs test/slate-runtime-provider-contract.test.tsx.
  • green after implementation and lint: bun --filter slate-react typecheck.
  • green after implementation and lint: bun --filter slate typecheck.
  • bun lint:fix passed and formatted the touched files.

Implementation:

  • Added public SlateRuntime, useSlateRuntime, useSlateRuntimeState, and useSlateViewState.
  • Reused public <Slate> as the root-bound view provider inside SlateRuntime.
  • Kept <Slate editor> as the single-editor shortcut.
  • Enforced <Slate root> inside SlateRuntime and rejected <Slate editor root>.
  • Moved runtime root views to one runtime-owned selector context, subscription bridge, and focus listener pair.
  • Made core editor views read root-local top-level children for state.nodes.children().

Next:

  • Closed by the multi-root browser proof pass below.

Ralph Multi-Root Example Browser Proof Pass - 2026-05-21

Status: complete.

Evidence:

  • PLAYWRIGHT_RETRIES=0 bun playwright playwright/integration/examples/multi-root-document.test.ts --project=chromium.
  • dev-browser --connect http://127.0.0.1:9222 against http://localhost:3100/examples/multi-root-document.
  • focused slate-react regression run for selection controller/reconciler, rendering, app-owned customization, and React editor contracts.

Implementation:

  • Added the separate multi-root-document example with header, main, and footer roots under one SlateRuntime.
  • Kept all roots visible and made the clicked root active/editable while the other roots stay read-only, because simultaneous live editables fought focus in the browser.
  • Root-stamped set_selection, filtered view selection by root, and guarded inactive roots from exporting DOM selection while another control/editor owns focus.
  • Routed the example into the site examples index and added focused Playwright coverage for root edits, title undo/redo focus, root-local select-all/copy, paste, placeholders, and follow-up typing.

Decision:

  • The shared runtime provider API is implemented with public SlateRuntime and root-bound public <Slate> views.
  • Public SlateViewProvider remains rejected.
  • No fixed/improved issue count changes.

Final Gate Sync Runtime Provider Pass - 2026-05-21

Status: complete.

Evidence:

  • bun check.
  • PLAYWRIGHT_RETRIES=0 bun playwright playwright/integration/examples/multi-root-document.test.ts --project=chromium.
  • dev-browser --connect http://127.0.0.1:9222 against http://localhost:3100/examples/multi-root-document.
  • cd packages/slate-react && bunx vitest run --config ./vitest.config.mjs test/slate-runtime-provider-contract.test.tsx.
  • bun test ./packages/slate/test/editor-runtime-view-contract.ts ./packages/slate/test/rooted-operation-contract.ts.

Final decision:

  • The React runtime-provider and multi-root example lane is complete.
  • Completion state, continuation prompt, and plan top state agree.
  • The accepted public API remains SlateRuntime plus root-bound public <Slate> views.
  • No fixed/improved issue count changes.

Header Focus Regression Pass - 2026-05-21

Status: complete.

Evidence:

  • red: PLAYWRIGHT_RETRIES=0 bun playwright playwright/integration/examples/multi-root-document.test.ts --project=chromium -g "focuses the header editor" failed because clicking Header editor left #multi-root-header inactive and contenteditable="false".
  • green: PLAYWRIGHT_RETRIES=0 bun playwright playwright/integration/examples/multi-root-document.test.ts --project=chromium -g "focuses the header editor".
  • green: PLAYWRIGHT_RETRIES=0 bun playwright playwright/integration/examples/multi-root-document.test.ts --project=chromium.
  • green: bun lint:fix.
  • green: bun typecheck:site.
  • green: dev-browser --connect http://127.0.0.1:9222 against http://localhost:3100/examples/multi-root-document; clicking the visible Header editor label focused #multi-root-header, made header contenteditable="true", and typed only into the header.

Implementation:

  • The root section now activates its root on mouse down capture.
  • Inactive roots are switched with flushSync before browser focus handling.
  • Clicks on root chrome focus the editable and place the caret at the end, so follow-up typing lands in that root.

Decision:

  • The multi-root example must treat the visible root chrome as part of the editor activation target.
  • No fixed/improved issue count changes.

Header Text Surface Caret Regression Pass - 2026-05-21

Status: complete.

Evidence:

  • video/frame proof showed the click path was inside the header text surface, not only on the Header editor label.
  • Browser plugin proof before the final fix reproduced the failure: activeElement was #multi-root-header, but window.getSelection() was not anchored inside the header.
  • red: PLAYWRIGHT_RETRIES=0 bun playwright playwright/integration/examples/multi-root-document.test.ts --project=chromium -g "inactive header text surface".
  • green: PLAYWRIGHT_RETRIES=0 bun playwright playwright/integration/examples/multi-root-document.test.ts --project=chromium -g "inactive header text surface".
  • green: PLAYWRIGHT_RETRIES=0 bun playwright playwright/integration/examples/multi-root-document.test.ts --project=chromium.
  • green: bun lint:fix.
  • green: bun typecheck:site.
  • green: Browser plugin in-app browser against http://localhost:3100/examples/multi-root-document; clicking inside the header text surface produced selectionAnchorInHeader: true, and typing inserted only into the header.

Implementation:

  • Removed the inactive-root readOnly toggle from the multi-root example.
  • All root text surfaces remain natively editable so first-click caret placement is handled by the browser.
  • Kept root activation on editable mouse down.
  • Kept explicit focus-at-end handoff for chrome clicks where the target is a label or badge, not editable text.

Decision:

  • Multi-root examples should not simulate active-root ownership by making inactive roots contenteditable=false; that breaks native caret placement.
  • Root-local selection ownership is the right isolation layer.
  • No fixed/improved issue count changes.

Header Sequential Key Order Regression Pass - 2026-05-21

Status: complete.

Evidence:

  • Browser plugin in-app browser reproduced the user-reported failure without cheating: click the header text surface, then press h, e, l, l, o. The header rendered ollehConfidential quarterly plan.
  • The Browser plugin type API could not be used because its virtual clipboard integration is missing; its keypress/locator-press route is useful reproduction evidence but not the final product oracle.
  • Standard Playwright against the same http://localhost:3100 page produced Confidential quarterly planhello with ordered key presses.
  • green: bun test ./packages/slate/test/rooted-operation-contract.ts ./packages/slate/test/create-editor-value-contract.ts ./packages/slate/test/state-tx-public-api-contract.ts ./packages/slate/test/editor-runtime-view-contract.ts.
  • green: bun --filter slate typecheck.
  • green: bun typecheck:site.
  • green: bun lint:fix.
  • green after lint: PLAYWRIGHT_RETRIES=0 bun playwright playwright/integration/examples/multi-root-document.test.ts --project=chromium.

Implementation:

  • setCurrentSelection now normalizes selection roots. A rootless selection imported through the header view is stored as a header selection; main-root selections keep the legacy rootless point shape.
  • Added a core contract proving repeated view-local inserts after rootless selection import produce hello..., not olleh....
  • Updated the multi-root browser row to press ordered keys and assert the forbidden reversed string is absent.

Decision:

  • Do not fake typing-order regressions with direct reversed text insertion.
  • The invariant is selection-transform ownership: the active root must advance the caret after each root-local insert.
  • No fixed/improved issue count changes.

Cursor Selection Drift Architecture Pass - 2026-05-21

Status: current pass complete. Lane remains pending.

Hard verdict: no, the current cursor/selection architecture is not the absolute best drift-prevention architecture yet.

The direction is right: Slate v2 has rooted operations, root-bound views, transactional writes, event frames, model-vs-DOM ownership, repair traces, and browser rows that assert focus, DOM selection, model state, and follow-up typing. The problem is that selection authority is still assembled from several cooperating pieces instead of one hard runtime boundary. That is why the recent bugs were real even though the architecture looked mostly correct.

Intent and boundary:

  • intent: prevent recurring user-visible cursor/selection drift that only appears under real browser timing, native caret placement, root switching, undo/redo, or repair-induced selectionchange.
  • outcome: keep the existing Slate v2 direction, but add a focused selection-authority consolidation target before claiming the architecture is release-hard.
  • in scope: core selection root semantics, view-root selection scoping, DOM import/export, beforeinput/key/input repair, history focus policy, browser proof, issue-ledger classification, and tests.
  • non-goals: no implementation edit from this Ralplan pass, no pivot to ProseMirror/Lexical/Tiptap, no product-level Plate API, no exact new Fixes #... claim, and no raw-device mobile closure.
  • decision boundary: DOM selection is IO, not the canonical source. The model selection is canonical only after it is imported through an owned event frame.

Live current shape:

  • core operations carry optional root fields at .tmp/slate-v2/packages/slate/src/interfaces/operation.ts:13, and core stamps a default operation root at .tmp/slate-v2/packages/slate/src/core/public-state.ts:456.
  • root-bound views filter selection and stamp update root through createEditorView at .tmp/slate-v2/packages/slate/src/editor-runtime-view.ts:52.
  • the recent key-order fix normalizes rootless imported selections in setCurrentSelection at .tmp/slate-v2/packages/slate/src/core/public-state.ts:2079.
  • React selection import/export lives in .tmp/slate-v2/packages/slate-react/src/editable/selection-controller.ts:533 and .tmp/slate-v2/packages/slate-react/src/editable/selection-controller.ts:707.
  • runtime selectionchange provenance and repair-induced model ownership live in .tmp/slate-v2/packages/slate-react/src/editable/runtime-selection-engine.ts:30.
  • the editing kernel already models selection policy and repair policy at .tmp/slate-v2/packages/slate-react/src/editable/editing-kernel.ts:265, and selectionchange ownership at .tmp/slate-v2/packages/slate-react/src/editable/editing-kernel.ts:447.
  • current tests cover the exact rootless import bug in .tmp/slate-v2/packages/slate/test/editor-runtime-view-contract.ts:89 and the human click/ordered key path in .tmp/slate-v2/playwright/integration/examples/multi-root-document.test.ts:155.

Diagnosis:

  • Root identity is present, but still optional at too many internal edges. Range can enter core as rootless and rely on view context to stamp it. That preserved main-root ergonomics, but it is too forgiving for a runtime that now supports multiple roots.
  • Selection authority is spread across core state, editor views, React selection controller, runtime selection engine, beforeinput sync, DOM repair, browser handle, and example focus choreography.
  • The current architecture is traceable, but not fully illegal-state-proof. The Browser/Playwright rows caught a mismatch after the fact; the runtime did not make the mismatch impossible enough.
  • Tests are improving, but they are still mostly example-shaped. The real contract should be generated from event-family and root/view matrices, so a fix for header clicks automatically protects paste, undo, modifier keys, native selection movement, composition, and remote import paths.

Ecosystem comparison:

  • ProseMirror has the clean precedent: transactions track document changes, selection changes, stored marks, metadata, and UI event context together. Its transaction selection maps through steps, and its view has explicit selectionFromDOM / selectionToDOM owners. See ../prosemirror/state/src/transaction.ts:26, ../prosemirror/state/src/transaction.ts:67, ../prosemirror/view/src/selection.ts:9, and ../prosemirror/view/src/selection.ts:55.
  • Lexical proves the modern lifecycle side: EditorState contains selection, $setSelection mutates only inside the active update context, update tags can skip DOM selection/focus side effects, and reconciliation owns DOM selection export. See ../lexical/packages/lexical/src/LexicalEditorState.ts:105, ../lexical/packages/lexical/src/LexicalUtils.ts:633, ../lexical/packages/lexical/src/LexicalUpdateTags.ts:49, and ../lexical/packages/lexical/src/LexicalUpdates.ts:616.
  • React-ProseMirror is the missing React wrapper precedent. It is not a better selection engine than ProseMirror itself; its lesson is lifecycle authority: the EditorView produced by useEditor should only be accessed through event/effect hooks, layout effects run after the view has the latest state and decorations, event callbacks receive the current view, composition pauses React-driven selection updates, and commit effects own DOM selection correction. See ../react-prosemirror/src/hooks/useEditor.ts:33, ../react-prosemirror/src/hooks/useEditorEffect.ts:12, ../react-prosemirror/src/hooks/useEditorEventCallback.ts:25, ../react-prosemirror/src/ReactEditorView.ts:124, and ../react-prosemirror/src/ReactEditorView.ts:275.
  • Tiptap is not the architecture winner here. It mostly confirms DX pressure: extension storage and React selectors should be pleasant, while the engine selection discipline comes from ProseMirror. See ../tiptap/packages/core/src/ExtensionManager.ts:367 and ../tiptap/packages/react/src/useEditorState.ts:118.

Rewrite recommendation:

Do a targeted selection-authority rewrite. Do not rewrite the whole editor.

  1. Make runtime selection internally root-explicit. A rootless Range may exist only at public single-root API edges. Inside runtime state, commits, selection transforms, view reads, history batches, and collab/local replay, every point is a rooted point. Main-root elision is a serializer/DX layer, not an internal invariant.
  2. Introduce a single SelectionFrame or equivalent internal object for every event that can read, import, export, repair, or mutate selection: id, root, eventFamily, inputIntent, targetOwner, provenance, modelBefore, domBefore, policy, modelAfter, lifecycle phase, view/commit epoch, and side-effect policy for focus/scroll/DOM selection.
  3. Route all DOM selection imports through one runtime boundary: selectionRuntime.importDOM(frame) or equivalent. It returns a rooted model range or null, stamps provenance, and is the only place allowed to convert DOM selection into tx.selection.set(...).
  4. Route all DOM exports/repairs through the paired boundary: selectionRuntime.exportDOM(frame) or equivalent. Programmatic export, repair-induced selectionchange, shell-backed selection, skip-scroll, and external focus preservation become frame policy, not scattered flags.
  5. Add React lifecycle accessors around the runtime boundary. Event handlers that read/write/import selection should use a runtime-owned callback shape like useSlateEventCallback((editor, event) => ...). Layout, geometry, and DOM export work should use a post-commit layout effect shape like useSlateLayoutEffect((editor) => ...). Direct window.getSelection() plus tx.selection.set(...) from arbitrary React render/effect/userland code is forbidden because it can pair a stale React view with current DOM selection.
  6. Add dev/test invariants:
    • a content operation root and current model-selection root must match, or selection must be null/cross-root-explicit;
    • no root-bound view may commit rootless selection internally;
    • no DOM import may happen without a frame root and provenance;
    • no DOM import/export may run from a stale React render/effect phase or stale view/commit epoch;
    • repair-induced selectionchange cannot switch from model-owned to dom-current;
    • external controls/title inputs cannot focus or scroll the editor unless the frame explicitly asks for it.
  7. Add static authority inventory guards. The runtime should fail tests when code outside the selection authority directly pairs getSelection(...) / ReactEditor.resolveSlateRange(...) with tx.selection.set(...), exports DOM selection directly, toggles model-selection preference without a frame, or reads DOM selection from React component code outside the approved event callback / layout-effect accessors.
  8. Generate browser gauntlets from the matrix, not from one example. Each row asserts active element, DOM selection owner, model selection root/offset, commit root/provenance, rendered text, forbidden text, focus/scroll side effects, and follow-up typing.

Proposed minimum proof matrix:

SurfaceRequired proof
Core/rooted selectionrootless public selection imported through a root-bound view becomes internal rooted selection; root mismatch throws in dev/test; Point/Range transforms ignore unrelated roots.
Key/native movementnative vertical movement followed by model-owned horizontal movement imports the DOM selection first and does not snap back.
Beforeinput/textnative insertText, model fallback, and DOM repair all commit through one frame and keep ordered typing.
Undo/redo/historytitle input undo/redo, root-local undo/redo, and body undo/redo do not steal focus or replay stale selection roots.
Composition/IMEcomposition-owned selection cannot be overwritten by repair-induced selectionchange or root activation.
Clipboard/drag/dropselect-all/copy/paste/drop are root-local unless a cross-root selection is explicitly supported.
Multi-root UIheader/body/footer chrome clicks, text-surface clicks, modifier keys, placeholder state, and follow-up typing stay root-local.
Remote/importremote state or collab patches default to preserve DOM selection/focus/scroll unless explicit policy says otherwise.
React lifecycle accessrender-time and stale-effect selection reads cannot import DOM selection; event callbacks and post-commit layout effects see the current root/view/selection; composition pauses React-driven selection updates.
Browser prooffocused Playwright rows plus persistent-browser or Browser-plugin rows for the exact user surface, with ordered keypresses when typing order matters.
Static drift guardownership inventory fails if selection import/export or repair policy escapes the selection runtime.

Steelman objection:

"This is yet another abstraction. You just fixed the bug with one root normalizer; why add a bigger subsystem?"

Fair objection. The answer is that this is not a new product API and not a cosmetic refactor. The existing code already has the subsystem: event frames, selection source, selectionchange origin, selection policy, repair policy, model preference, root-bound views, and root stamping. The rewrite consolidates those facts behind one boundary so future fixes do not require remembering which of eight modules also needs the same root/provenance/focus rule.

Decision:

  • Keep Slate model + operations + editor.read / editor.update.
  • Keep rooted operations and multi-root views.
  • Revise selection internals so rooted selection is an invariant, not a repair.
  • Revise browser proof so event families are generated from contracts, not handwritten only when a user finds a video-class bug.
  • Do not claim absolute browser robustness until this rewrite and proof matrix land.

Score for current selection drift architecture:

DimensionScoreEvidence
React 19.2 runtime performance0.88runtime has selector dispatch, throttled/debounced selectionchange, and model-backed full-document selection guards; hot-path risk remains if richer frame objects allocate on every selectionchange.
Slate-close unopinionated DX0.86public rootless single-root ergonomics are good, but internal rootless tolerance leaked into multi-root behavior.
Plate/slate-yjs migration backbone0.87rooted operations and selection side-effect metadata help, but collab/remote import needs frame-level focus/scroll/DOM policy.
Regression-proof testing0.76current focused rows are strong, yet they are still example-local and missed the Browser-plugin key-order path until reported.
Research evidence completeness0.91ProseMirror, Lexical, Tiptap, research notes, solution notes, issue ledgers, and live source were re-read.
shadcn-style composability/minimalism0.85<SlateRuntime> / <Slate root> stay clean; the React lifecycle accessors keep selection authority internal instead of leaking a new product API, but internals still need fewer escape hatches and better owner boundaries.

Weighted current score: 0.86.

Target score after selection-authority consolidation, React lifecycle access gate, and generated browser gauntlets: 0.94.

Follow-up pass:

  • related-issue-discovery: classify the related selection/focus/history surface without new fixed/improved claims unless exact proof exists. Start cache-first from docs/slate-v2/ledgers/fork-issue-dossier.md, docs/slate-v2/ledgers/issue-coverage-matrix.md, docs/slate-issues/gitcrawl-v2-sync-ledger.md, and docs/slate-issues/gitcrawl-live-open-ledger.md. This pass is now recorded below.

Status: current pass complete. Lane remains pending.

This pass is cache-first and does not add a new issue-count claim. The current rewrite recommendation is supported by live issue pressure, but exact closure still belongs to future browser/device rows.

Reviewed sources:

  • docs/slate-v2/ledgers/fork-issue-dossier.md
  • docs/slate-v2/ledgers/issue-coverage-matrix.md
  • docs/slate-issues/gitcrawl-v2-sync-ledger.md
  • docs/slate-issues/gitcrawl-live-open-ledger.md

Issue pressure map:

SurfaceRelated rowsDecision
Multi-root/view ownership#5537, #5117, #6016Supports one shared runtime with root-bound views, view-local focus/input ownership, and view-local DOM state. No exact closure claim from this architecture pass.
Focus and scroll ownership#5867, #5826, #5538, #4995, #5088, #5473, #3893, #1769, #3412, #4376, #5171Fold into selection-frame focus/scroll policy. Programmatic focus, blur, external controls, and refocus autoscroll must be frame-owned, not scattered DOM side effects.
Selection gesture/native movement#5689, #5559, #5524, #5632, #5806, #5690, #5274, #3585Fold into generated browser gauntlets. These are not solved by one header-click row; they need event-family proof for click, shift-click, triple-click, vertical movement, and inline/void boundaries.
History and undo selection replay#5515, #3705, #3756, #3921, #5587, #5364, #3534, udecode/slate#9, udecode/slate#11, udecode/slate#12Keep existing exact claims narrow. The rewrite must make history focus/selection restore policy explicit so title inputs and root views do not steal focus on undo/redo.
Beforeinput, composition, and typed order#6022, #4232, #5398, #5433, #5883, #4400, #5653, #4543, #5371Treat as input-runtime and selection-frame interaction pressure. Android/iOS/IME rows need raw-device or browser proof before closure.
Clipboard, paste, drop, and external DOM#4888, #5749, #4806, #4268, #5479, #5376, #5328, #4857Clipboard/drop ingress should carry selection frame provenance when it mutates model selection or DOM ownership; existing clipboard fixes remain scoped.
Explicit non-goals#5550, #5551, #5924, #2558Do not broaden the rewrite into arbitrary Web Component selection drag, table-range selection modeling, or a public ignore-cursor API.

Already fixed or accounted rows stay narrow:

  • #6034: exact table-end ArrowDown caret row is already fixed in the current issue matrix.
  • #3534: undo after Enter with a multi-block selection is already fixed in the current issue matrix.
  • Existing udecode/slate history/undo rows remain limited to their current browser proof. They do not prove general multi-root/title-input focus policy.

Decision:

  • New fixed claims: 0.
  • New improved claims: 0.
  • Related issue matrix changes: 0.
  • Sync ledger note: added.
  • PR reference update: skipped because no claim count or release narrative changed.

Architecture impact:

The issue corpus strengthens the targeted rewrite. The selection runtime should own these cross-cutting facts in one place:

  1. active root and model selection root;
  2. DOM import provenance and event family;
  3. focus, scroll, and DOM export policy;
  4. history/undo selection restore ownership;
  5. composition/input ownership;
  6. browser proof row generation.

Next pass:

  • closure-score-final-gates-cursor-selection-drift: verify plan consistency, completion state, sync ledger note, and whether the Ralplan can close as Ralph-ready without implementation edits.

Cursor Selection Drift Final Gates - 2026-05-21

Status: complete. Lane is Ralph-ready.

Final gate audit:

GateResult
Plan top stateAt the original closeout: status: done, current_pass: closure-score-final-gates-cursor-selection-drift, next_pass: none, final_handoff_status: complete, ralplan_lane_status: complete. The later lifecycle amendment updates current_pass without reopening the lane.
Source edit boundaryNo .tmp/slate-v2 implementation edit belongs to this Ralplan closeout. The rewrite remains a later Ralph implementation target.
Architecture verdictCurrent architecture is good but not absolute-best. The accepted target is targeted selection-authority consolidation, not a whole-editor rewrite.
EvidenceLive source, local ecosystem source, regression rows, solution notes, and issue ledgers are recorded in the current-state and related-issue passes.
Issue accountingNew fixed claims: 0. New improved claims: 0. Existing issue classifications remain scoped.
PR referenceSkipped because no fixed/improved count or release narrative changed.
ScoreCurrent selection-drift architecture stays 0.86; target after implementation, React lifecycle access gate, and generated browser gauntlets is 0.94. The lower score is intentional and records the rewrite need.
HandoffExisting Ralph-ready handoff remains the implementation owner; this closeout adds the selection-authority rewrite as the next architecture target.
Completion hookScoped completion state is done and no pending pass remains.

Final decision:

  • Close this Ralplan lane.
  • Do not claim the selection-authority rewrite is implemented.
  • Do not claim absolute browser robustness yet.
  • The next concrete owner is Ralph execution for the targeted rewrite: rooted internal selection, single selection-frame import/export/repair boundary, React lifecycle access gate, dev/test invariants, authority inventory guards, and generated browser gauntlets.

Cursor Selection React Lifecycle Amendment - 2026-05-21

Status: complete. Lane stays closed.

This amendment updates the target after comparing ../react-prosemirror. It does not reopen implementation and does not add a fixed/improved issue claim.

Decision:

  • Keep the ProseMirror/Lexical-inspired selection authority rewrite.
  • Add React lifecycle authority as a first-class part of SelectionFrame.
  • Track lifecyclePhase and a view/commit epoch so stale React render/effect reads cannot import DOM selection into current model state.
  • Add internal React accessors equivalent to useSlateEventCallback and useSlateLayoutEffect; make them public only if userland truly needs direct lifecycle-safe editor/view access.
  • Forbid direct DOM selection import from arbitrary React render/effect code.

Why this matters:

React-ProseMirror's key lesson is boring and correct: React wrappers must make stale editor/view access hard. Slate v2 should copy that lifecycle discipline, not its architecture wholesale. Otherwise Playwright/browser-only drift bugs will keep slipping through whenever a component reads the DOM selection from one React phase and commits it in another.

Updated Ralph target:

  1. Rooted internal model selection.
  2. One SelectionFrame for import/export/repair/mutation.
  3. Frame-owned focus, scroll, DOM selection, provenance, and lifecycle policy.
  4. Runtime-owned event callback and post-commit layout-effect access.
  5. Static guards for DOM selection import/export outside the runtime boundary.
  6. Generated browser gauntlets that include React lifecycle and composition rows, not just the currently reported header/body example.

Final state:

  • status: done
  • current_pass: react-prosemirror-lifecycle-amendment
  • next_pass: none
  • score: 0.86
  • target_score: 0.94

Previous Store-Based Handoff

Historical only. Do not use this as Ralph authority; use the state-field handoff above.

Objective

Implement first-class non-node document state in Slate v2 without hidden nodes: document title/settings are persisted document stores; comments remain external anchored stores by default; header/footer/global regions stay deferred to a future multi-root model.

Scope Lock

Allowed implementation owners in .tmp/slate-v2:

  • packages/slate/src/interfaces/editor.ts
  • packages/slate/src/create-editor.ts
  • packages/slate/src/core/public-state.ts
  • packages/slate/src/core/editor-extension.ts only if descriptor registration needs extension setup support.
  • packages/slate-history/src/history-extension.ts
  • packages/slate-react/src/hooks/** and selector/subscription support.
  • focused tests and examples named in the proof plan.

Forbidden in the first Ralph pass:

  • hidden title/settings/header/footer nodes.
  • value: { children, stores }.
  • product comment/thread services in raw Slate.
  • shared node object identity across editors.
  • generic custom-operation escape hatches.
  • global dirtyScope: all for store writes.

Accepted Public Target

  • defineEditorStateStore(descriptor)
  • createEditor({ initialValue: children }) remains content-only.
  • createEditor({ initialDocument: { children, stores } }) is the broader document constructor.
  • EditorDocumentSnapshot = { children, stores?, version? }.
  • state.stores.get(descriptor).
  • tx.stores.set(descriptor, valueOrUpdater).
  • EditorCommit.statePatches.
  • EditorCommit.dirtyStateStoreKeys.
  • EditorCommitSource literal 'state-store'.
  • useEditorStateStore(descriptor, selector, options?).
  • optional typed extension aliases through existing state / tx extension groups, never core product verbs.

Implementation Slices

  1. Core document snapshot and descriptor registry.
    • Add initialDocument without changing initialValue.
    • Add persisted descriptor registry and store initialization.
    • First proof: bun test ./packages/slate/test/create-editor-document-contract.ts.
  2. Core state writes and commit dirtiness.
    • Add tx.stores, state patch capture, rollback, dirtyStateStoreKeys, and 'state-store' source publication.
    • First proof: bun test ./packages/slate/test/document-state-contract.ts.
  3. Patch policy and metadata.
    • Add whole-value patches for small stores and patch-hook guard for large historyable/shared stores.
    • Extend commit metadata tests for ordered mixed operation/state commits.
  4. History.
    • Teach history batches to invert/replay operations plus state patches.
    • Prove title push, title typing merge, preference skip, mixed undo/redo, and remote import skip.
  5. Collab substrate.
    • Export local shared state patches; suppress local/external policies; import remote patches with history skip and selection side-effect suppression.
  6. React selector locality.
    • Add useEditorStateStore.
    • Prove title edits wake exact store selectors only and body edits wake no store selectors.
  7. Comments and examples.
    • Keep comments external by default.
    • Add a document-state example only after core/history/react tests pass.
  8. Performance and release gates.
    • Run focused benchmarks, package typechecks, then bun check; run bun check:full only when browser/example behavior changed enough to claim release proof.

First Red Tests

Write one failing public contract at a time:

  1. packages/slate/test/create-editor-document-contract.ts
  2. packages/slate/test/document-state-contract.ts
  3. packages/slate/test/document-state-patch-contract.ts
  4. packages/slate-history/test/document-state-history-contract.ts
  5. packages/slate/test/collab-document-state-contract.ts
  6. packages/slate-react/test/editor-state-store-selector-contract.test.tsx
  7. playwright/integration/examples/document-state.test.ts

Do not write the whole suite upfront.

Required Commands

Focused gates from .tmp/slate-v2:

  • bun test ./packages/slate/test/create-editor-document-contract.ts
  • bun test ./packages/slate/test/document-state-contract.ts
  • bun test ./packages/slate/test/document-state-patch-contract.ts
  • bun test ./packages/slate/test/commit-metadata-contract.ts
  • bun --filter slate typecheck
  • bun test ./packages/slate-history/test/document-state-history-contract.ts
  • bun --filter slate-history typecheck
  • cd packages/slate-react && bunx vitest run --config ./vitest.config.mjs test/editor-state-store-selector-contract.test.tsx
  • bun --filter slate-react typecheck
  • playwright test playwright/integration/examples/document-state.test.ts --project=chromium

Broad gates from .tmp/slate-v2:

  • bun bench:core:editor-store:local
  • bun bench:core:history-retained-memory:local
  • bun bench:core:collab-readiness:local
  • bun bench:react:rerender-breadth:local
  • bun check
  • bun check:full when examples/browser behavior changed.

Issue And Reference Sync

  • No Fixes #... claim belongs to this plan.
  • Current related/non-fix issue surface is #4477/#4483/#5987/#3383/#5515/#3741/#3715/#4612/#3705/#3756/#3921/#6016/#3482.
  • Keep docs/slate-issues/gitcrawl-v2-sync-ledger.md as the current sync note owner for this architecture pass.
  • Keep docs/slate-v2/ledgers/issue-coverage-matrix.md unchanged unless Ralph implementation produces a new exact fixed/improved proof.
  • docs/slate-v2/references/pr-description.md is synced only as a non-claim future API note. Counts stay unchanged.

Stop Rules

  • If initialValue stops being content-only, stop and fix the API shape.
  • If store writes require dirtyScope: all, stop and redesign dirtiness.
  • If title typing rerenders body blocks, stop before examples.
  • If history/collab cannot invert/replay mixed commits, keep state stores internal/experimental and default history/collab to skip/local.
  • If browser state import steals focus or scroll, default imports to preserve DOM selection/focus/scroll and do not publish the example.

Revision/Handoff Hardening Pass - 2026-05-20

Status: complete.

Evidence read:

  • active plan top, proof sections, pass ledger, and previous continuation state.
  • docs/slate-v2/references/pr-description.md.
  • issue sync and coverage rows for the touched issue surface.
  • ralph skill handoff contract.

Plan deltas:

  • Added this Ralph-ready handoff with objective, scope lock, public target, implementation slices, first red tests, commands, issue/reference sync, and stop rules.
  • Locked initialDocument as the accepted constructor target for this plan.
  • Synced docs/slate-v2/references/pr-description.md as a non-claim future API note. Fixed/improved counts stay unchanged.
  • Raised the plan score to threshold because the remaining work is final closure audit, not architecture discovery.

Closure owner:

  • Closure-score/final-gates pass completed below.

Closure-Score/Final-Gates Pass - 2026-05-20

Status: complete.

Historical closure only. Superseded by the later latest-API refresh, which reopened the lane and replaced initialDocument with initialValue normalization plus canonical rooted Value.

Evidence read:

  • active plan top, verdict, API target, runtime target, policy sections, proof matrix, pass ledger, and Ralph-ready handoff.
  • active goal state.
  • active goal state.
  • docs/slate-v2/references/pr-description.md.
  • docs/slate-issues/gitcrawl-v2-sync-ledger.md.
  • docs/slate-v2/ledgers/issue-coverage-matrix.md.
  • relevant solution notes for history batch preconditions, source-scoped invalidation, and projection ownership. The optional docs/solutions/patterns/critical-patterns.md file is not present in this repo.

Requirement audit:

RequirementEvidenceResult
non-node editor state modelFinal Architecture Authority, Public API Target, and Internal Runtime Target define state fields, statePatches, dirtyStateKeys, and initialDocument.state while keeping children as content.complete
history opt-in/outHistory Policy Target, proof matrix, high-risk proof rows, and Ralph handoff define descriptor defaults plus transaction metadata overrides for push/merge/skip.complete
persistence and collaborationPersistence And Collaboration Target, EditorDocumentSnapshot, collab proof rows, and stop rules define serialized state fields and ordered mixed commit export/import.complete
related issue mappingIssue Ledger Accounting, the 2026-05-20 sync notes, coverage matrix references, and PR non-claim text cover the related surface without new Fixes #... claims.complete
other-editor comparison evidenceEcosystem Strategy and research refresh cite ProseMirror StateField/history metadata, Lexical read/update/tags/NodeState, Tiptap storage/selectors, compiled research, local raw source, and Context7 official-doc checks.complete
realistic examples and migration pressureproof matrix, browser stress rows, performance/DX/migration pass, and Ralph handoff require title/settings state-field examples, external comments split, old initialValue, and new initialDocument.state.complete
non-contiguous and shared-history architectureNon-Contiguous And Multi-Editor Proposal defers header/footer to root-id multi-root design and rejects shared node objects for multi-editor history.complete
Ralph readinessRalph-Ready Handoff gives objective, scope lock, public target, implementation slices, first red tests, required commands, issue sync, and stop rules.complete
boundary disciplineplan and completion state keep Slate Ralplan as docs/ledger/state only; .tmp/slate-v2 implementation belongs to a later explicit Ralph run.complete

Final decision:

  • The architecture review is ready for user review and later Ralph execution.
  • No Slate v2 implementation claim is made.
  • No issue fixed/improved counts change from this plan.
  • Closure verification remains limited to planning artifacts in plate-2; implementation proof must run from .tmp/slate-v2 during Ralph.

Previous Done Handoff, superseded by Jotai reopen:

  • public API: defineEditorStateStore, initialDocument, EditorDocumentSnapshot, state.stores, tx.stores, statePatches, dirtyStateStoreKeys, 'state-store', and useEditorStateStore.
  • history: descriptor defaults plus update metadata override.
  • persistence: children + stores snapshot, with initialValue still children-only.
  • collab: commit records include operations plus state patches.
  • comments: external anchored store default.
  • multi-root: deferred root-id path model for header/footer/global regions.
  • multi-editor: shared document runtime, not shared node objects.
  • issue accounting: related/not-fixed mapping only; no Fixes #... claims.
  • proof gates: core/history/react/collab/browser/benchmark rows are assigned to Ralph.

Closure-Score/Final-Gates State-Field Final - 2026-05-20

Status: complete.

Historical closure only. Superseded by the latest-API refresh. Its initialDocument, state.fields, and tx.fields wording is not current authority.

Evidence read:

  • active plan top, final authority, public API target, proof matrix, high-risk proof plan, maintainer objection ledger, pass ledger, and Ralph-ready handoff.
  • active goal state.
  • active goal state.
  • docs/slate-v2/references/pr-description.md.
  • docs/slate-issues/gitcrawl-v2-sync-ledger.md.
  • docs/slate-v2/ledgers/issue-coverage-matrix.md.

Final gate audit:

GateResult
pass schedulecomplete: every state-field follow-up row is complete, and this row closes the final gates.
public API authoritycomplete at the time: defineEditorStateField, initialDocument.state, state.fields, tx.fields, statePatches, dirtyStateKeys, source 'state', useEditorStateFieldValue, and useSetEditorStateField. Superseded by latest API authority.
terminologycomplete: current proof/handoff wording is field-based; store wording is external/source terminology or explicitly historical.
issue accountingcomplete: no Fixes #... claim, no fixed/improved count change, #4612 matrix note tightened, PR reference remains non-claim.
boundarycomplete: Slate Ralplan edited docs/ledgers/state only; .tmp/slate-v2 implementation belongs to explicit Ralph execution.
handoffcomplete: Ralph-ready handoff has objective, forbidden paths, slices, red tests, commands, issue sync, and stop rules.

Final decision:

  • The state-field architecture plan is ready for user review and later Ralph execution.
  • No Slate v2 implementation claim is made.
  • No issue fixed/improved counts change from this plan.
  • Closure verification is limited to planning artifacts in plate-2; implementation proof must run from .tmp/slate-v2 during Ralph.

Latest API Issue/Reference/Proof Sync Pass - 2026-05-20

Status: complete.

Evidence read:

  • active plan authority, proof matrix, high-risk proof plan, maintainer objection ledger, Ralph-ready handoff, and final summary.
  • docs/slate-v2/references/pr-description.md.
  • docs/slate-issues/gitcrawl-v2-sync-ledger.md.
  • docs/slate-v2/ledgers/issue-coverage-matrix.md.
  • active goal state.
  • active goal state.

Decisions:

  • keep zero new fixed issue claims.
  • keep zero new improved issue claims.
  • update the PR reference non-claim note to latest API names: defineStateField, canonical Value, InitialValue, state.getField, tx.setField, rooted operations, root-aware locations, runtime/view split, and React state-field hooks.
  • update the sync ledger note so it no longer teaches initialDocument, state.fields, or old hook names as current API.
  • update the #4612 coverage row so future document state fields are tied to initialValue normalization and tx.setField, not React controlled value.
  • keep the implementation proof boundary: no .tmp/slate-v2 source edits and no implementation closure claim from this Ralplan.

Closure-Score/Final-Gates Latest API Final - 2026-05-20

Status: complete.

Final gate audit:

GateResult
latest API authoritycomplete: active authority uses defineStateField, canonical Value, InitialValue, state.getField, tx.setField, rooted operations, root-aware Point/Range, runtime/view split, and state-field React hooks.
proof rowscomplete: proof matrix and Ralph handoff use value normalization, rooted operations, runtime/view, state field, history, collab, React selector, browser, and benchmark rows.
issue/reference synccomplete: PR reference, v2 sync ledger, and #4612 coverage row are latest-API non-claim rows with no count change.
historical driftcomplete: stale API names remain only in explicitly historical/superseded pass notes or previous handoff sections.
boundarycomplete: Slate Ralplan edited only plan/ledger/reference/state artifacts; .tmp/slate-v2 implementation belongs to explicit Ralph execution.
stop-hook statecomplete: completion state may be done because no runnable Slate Ralplan pass remains.

Final decision:

  • The latest API Ralplan is ready for user review and later Ralph execution.
  • No Slate v2 implementation claim is made.
  • No issue fixed/improved counts change from this plan.
  • Completion state is closed because remaining work is implementation by explicit [$ralph], not more Slate Ralplan review.

State-Field Policy Shorthand DX Pass - 2026-05-20

Status: complete.

Decision:

  • persist, history, and collab are universal state-field policy axes.
  • history and collab must be shorthand-first in public examples: history: 'push', history: 'skip', collab: 'shared', collab: 'local'.
  • Object policy forms such as history: { default: 'push' } and collab: { default: 'shared' } stay available only as escape hatches for future policy metadata.
  • Do not add more universal top-level policy axes now. Future migration, authorization, conflict resolution, or adapter-specific behavior should first fit through serialize, deserialize, collab, or extension-owned metadata.

Plan deltas:

  • updated active public examples to shorthand-first policy fields.
  • updated StateField<T> to accept shorthand or expanded policy objects.
  • added an explicit policy rule to the internal runtime target and history policy target.
  • updated Ralph handoff/final summary expectations.
  • no issue fixed/improved count changes.
  • no .tmp/slate-v2 source edits.

Closure-Score/Final-Gates State-Field Policy Shorthand - 2026-05-20

Status: complete.

Final gate audit:

GateResult
public examplescomplete: active examples use history: 'push', history: 'skip', collab: 'shared', and collab: 'local'.
descriptor typecomplete: StateField<T> accepts shorthand policies and object escape hatches.
scope controlcomplete: no new universal policy axis was added.
boundarycomplete: Slate Ralplan edited plan/state artifacts only; .tmp/slate-v2 implementation belongs to explicit Ralph execution.
stop-hook statecomplete: completion state may be done because no runnable Slate Ralplan pass remains.

Final User-Review Handoff Outline

Final handoff:

  • public API: defineStateField, canonical Value = { roots, state? }, ergonomic InitialValue, state.getField, tx.setField, statePatches, dirtyStateKeys, source 'state', useStateFieldValue, and useSetStateField.
  • construction: initialValue accepts Element[], { children, state? }, or { roots, state? }; runtime always normalizes to canonical rooted Value.
  • state-field policy DX: history and collab use string shorthands for the common path, with object policy forms reserved for extra metadata.
  • history: descriptor defaults plus update metadata override.
  • persistence: { roots, state? } snapshot. No version field in Slate core.
  • collab: commit records include operations plus state patches.
  • comments: external anchored store default.
  • multi-root: root-explicit content operations, root-aware Point/Range, and root-local numeric paths.
  • multi-editor: one shared document runtime with root-bound editor views, not shared node objects.
  • issue accounting: related/not-fixed matrix, no Fixes claims.
  • proof gates: core/history/react/collab/browser/benchmark rows.

Next Action

Wait for explicit [$ralph] before editing .tmp/slate-v2 source.