docs/plans/2026-05-06-slate-v2-core-history-selection-undo-ralplan.md
The next cluster should be v2-core-engine, narrowed to history, undo, and
selection integrity.
Hard take: do not jump back to Android or another DOM bridge pass right now. Input runtime, React runtime, DOM selection, and clipboard already have completed local proof lanes. Android exact closure still needs raw-device evidence. The next runnable architecture owner is the core transaction/history path, because the issue corpus still has open undo selection rows and the current implementation already exposes the right live hooks to test them without browser hardware.
Chosen first slice:
history/undo selection integrity
+ move_node undo state
+ deleteFragment undo selection restoration
+ incomplete set_selection replay guards
+ transaction metadata and collaboration boundaries
Not chosen:
v2-core-engine rewrite in one pass;move_node, and partial set_selection
histories, without adding product-shaped public APIs.slate, slate-history, transaction commits, operation inverse,
refs/bookmarks, history grouping, collaboration history metadata, and issue
accounting for clusters 6 and 27 plus singleton #4559.normalizeSelection escape hatch.Fixes #... claims require red/green proof matching the
issue repro.Principles:
set_selection patches during replay.editor.update, tags, metadata, and
HistoryEditor helpers are enough unless proof says otherwise.Top drivers:
move_nodes wrong state.Viable options:
Chosen option: option 3.
Consequences:
Live owners:
.tmp/slate-v2/packages/slate-history/src/with-history.ts.tmp/slate-v2/packages/slate-history/test/history-contract.ts.tmp/slate-v2/packages/slate-history/test/integrity-contract.ts.tmp/slate-v2/packages/slate/src/core/apply.ts.tmp/slate-v2/packages/slate/src/core/public-state.ts.tmp/slate-v2/packages/slate/src/editor/bookmark.ts.tmp/slate-v2/packages/slate/test/collab-history-runtime-contract.ts.tmp/slate-v2/packages/slate/test/selection-rebase-contract.tsCurrent implementation facts:
withHistory captures previousSnapshot = Editor.getSnapshot(e) and stores
selectionBefore: previousSnapshot.selection in each new batch.batch.operations.map(Operation.inverse).reverse() and then
sets batch.selectionBefore.batch.selectionBefore before replaying the original operations.set_selection operations are not saved as history content operations.editor.update owns tags and metadata, including history, collab, and
selection.EditorCommit already records selectionBefore, selectionAfter,
selectionChanged, metadata, tags, dirty runtime ids, and operations.apply transforms path refs, point refs, range refs, bookmarks, and implicit
targets before applying each operation.Editor.bookmark exists as a live op-rebased range primitive.Current gap:
docs/research/sources/editor-architecture/lexical-read-update-extension-runtime.mdeditor.update, update tags, dirty sets,
command handlers inside update context.editor.update as the mutation boundary and use commit
tags/metadata for history grouping and collaboration import policy.$ helper API, and command-first mutation as the normal
app API.docs/research/sources/editor-architecture/prosemirror-transaction-view-dom-runtime.mddocs/research/sources/editor-architecture/tiptap-extension-command-react-dx.mdNo new public API in the first slice.
Keep:
editor.update((tx) => ...)history-push, history-merge, historic, collaborationmetadata.history, metadata.collab, metadata.selectionwithHistory(editor)HistoryEditor.withoutSaving, withoutMerging, withNewBatch, undo,
redoReject for this lane:
normalizeSelectionhistoryTransactionSelectionBookmarkIf exact proof shows raw range snapshots are insufficient, the target is an internal history selection bookmark, not a new public concept.
The implementation target is:
commit owns before/after selection
+ history batch owns replay policy
+ undo/redo replay emits complete selection state or null
+ structural inverse operations restore the exact document first
+ selection restoration happens after the restored document is valid
+ remote/collab imports cannot poison local undo stacks
Hard requirements:
move_node restores the exact original tree and selection;set_selection patch without a
current selection;set_selection-only commits stay out of undo stacks;history: { mode: 'skip' } stay out of
local undo stacks;Skipped for the first slice. This is not a React render API plan.
The only React-facing requirement is negative: React runtime must consume the resulting commits without depending on mutable editor fields or browser timing.
Plate needs:
This lane strengthens the substrate Plate builds on; it should not add Plate
policy to slate-history.
Slate-yjs needs:
This plan must keep local-only runtime ids out of serialized collaboration
operations. Current collab-history-runtime-contract.ts already proves runtime
ids are local and remote remove/move rebases them.
ClawSweeper status: complete for the core-history selection/undo surface.
Gitcrawl evidence read:
gitcrawl doctor --json: 659 open threads, 617 clusters,
last_sync_at=2026-05-04T14:58:11.123944Z.gitcrawl cluster-detail ... --id 6: #3705, #3756, #3921.gitcrawl cluster-detail ... --id 27: #3534, #3551.gitcrawl search ... "history undo selection set_selection move_nodes":
no additional hybrid hits.gitcrawl threads ... --numbers 3534,3551,3705,3756,3921,4559,1770,2288,3741,3752 --include-closed --json:
refreshed the exact issue bodies and states for this slice.gitcrawl neighbors ... --number 3534 and --number 3705: confirmed the
selection/undo and incomplete set_selection families overlap, but do not
prove the same root cause for every row.Fixed issues: none in the planning pass.
Candidate exact claims after execution, only if red/green proof lands:
move_nodes restores the exact original tree and selection.set_selection or selection-movement failure directly.Related but not fixed yet:
set_selection history replay family.set_selection history replay family.Improves through replace_children; keep out of new fixed
claims unless public range operation proof changes.Ledger sync status:
docs/slate-v2/ledgers/issue-coverage-matrix.md lists #3534, #3551,
#3705, #3756, #3921, and #4559 as Related, #2288 as Improves, and #1770,
#3741, and #3752 as related/non-claim pressure.docs/slate-v2/ledgers/fork-issue-dossier.md already has sections for
#3534, #3551, #3705, #3756, #3921, #4559, #1770, #2288, #3741, and #3752.docs/slate-v2/references/pr-description.md keeps fixed issue claims
unchanged and updates the improved/related/not-claimed count to 99.Fixes #... rows are added until execution proof lands.Remaining cluster backlog:
v2-core-engine bucket: 104 rows.| Contract | Required proof |
|---|---|
| #3534 multi-block edit undo | package test in slate-history or slate that reproduces selected multi-block edit, undo, exact selection restore |
| #4559 deleteFragment undo | package test that selects/deletes a fragment, undoes, and asserts restored fragment selection |
| #3551 move_nodes undo | package test that moves nodes via public transform/update, undoes, and asserts exact original tree plus selection |
incomplete set_selection replay | operation/history test proving replay either resolves against live selection or rejects safely before history corruption |
| selection-only commits | existing proof stays green: selection-only commits are not saved to history |
| collaboration import | existing proof stays green: remote imports can skip local history |
| replace_children undo | existing proof stays green: range delete stores one undoable batch and restores selection |
| refs/bookmarks | existing bookmark and selection-rebase tests stay green |
Fast local verification target for execution:
bun --filter slate-history typecheck
bun --filter slate typecheck
bun test ./packages/slate-history/test/history-contract.ts ./packages/slate-history/test/integrity-contract.ts ./packages/slate/test/collab-history-runtime-contract.ts ./packages/slate/test/selection-rebase-contract.ts
Broaden only if the changed files touch operation inverse, refs, or normalization:
bun test ./packages/slate/test/operations-contract.ts ./packages/slate/test/transaction-contract.ts ./packages/slate/test/delete-contract.ts
Package tests are the first owner. Browser proof is not required for the first red/green history slice unless the repro depends on native selection import or DOM focus.
If a browser row is needed later:
| Skill | Status | Reason |
|---|---|---|
clawsweeper | applied | clusters 6 and 27 plus #4559/#1770/#2288/#3741/#3752 reviewed and synced into fork dossier, coverage matrix, and PR count |
tdd | required next | execution must start with red package tests |
performance | applied as non-claim boundary | #3752 is classified as memory benchmark pressure, not correctness closure |
performance-oracle | skipped until execution | apply if implementation changes operation count, stack retention, or ref transforms |
vercel-react-best-practices | skipped | no React render surface in first slice |
react-useeffect | skipped | no React effect surface |
shadcn | skipped | no UI surface |
high-risk-deliberate-pass | applied | risk matrix blocks public/API changes and exact issue claims until red/green proof lands |
Failure modes:
move_node inverse is correct for content but not for refs/bookmarks;Mitigation:
normalizeSelection.set_selection history-saveable by default.| Objection | Answer | Status |
|---|---|---|
| "History is already closed in the roadmap." | Support-package closure is not exact issue closure. The issue matrix still marks #3534/#3551/#4559 as related. | accepted |
| "This is old Slate issue debt." | Old does not mean stale when the same transaction/history class remains central to v2 and can be tested locally. | accepted |
| "Why not Android next?" | Android exact closure needs raw-device proof. This core lane is runnable now and strengthens collaboration/history. | accepted |
| "Selection bookmarks are a ProseMirror idea." | The mechanism is useful; the public position model is not. Slate can keep JSON paths and internal op-rebased bookmarks. | accepted |
| "Do not add new public history API." | Agreed. First slice is internal proof only. | accepted |
| Pass | Status | Evidence added | Plan delta | Open issues | Next owner |
|---|---|---|---|---|---|
| current-state read and initial score | complete | recent completion files, full issue ledger plan, gitcrawl clusters 6/27, live with-history, core transaction, bookmark, and history tests | selected v2-core-engine-history-selection-undo as next plan | none for planning | ralph |
| related issue discovery | complete | refreshed gitcrawl threads for #3534/#3551/#3705/#3756/#3921/#4559/#1770/#2288/#3741/#3752 plus neighbors for #3534/#3705 | #3705/#3756/#3921/#3741/#3752 added to coverage matrix accounting | none for planning | ralph |
| issue-ledger pass | complete | coverage matrix, fork dossier, PR count, and full issue-ledger execution row synced | fixed claims unchanged; improved/related/not-claimed count moves to 99 | no new closure claims | ralph |
| research/source refresh | complete | live Slate v2 history/core/bookmark/collab files refreshed; local Lexical/ProseMirror/Tiptap source mechanisms checked | plan now uses source-backed tags/bookmarks/selection mapping/command-chain decisions | none for planning | ralph |
| high-risk deliberate pass | complete | premortem and exact-claim gates require red/green issue proof before promotion | implementation must start with package tests, not API changes | none for planning | ralph |
| closure score | complete | scorecard raised above gate with no dimension below 0.85 | plan is ready for execution, not issue closure | execution proof still needed | ralph |
Fixes rows.Phase 1: red issue-shaped package tests.
set_selection replay guard if reproducible without
DOM refocus.Phase 2: smallest core/history fix.
slate-history first; touch operation inverse/ref layers only if the
red test proves the bug lives there.Phase 3: preserve existing contracts.
Phase 4: ledger and PR sync.
Must pass before implementation handoff:
bun test ./packages/slate-history/test/history-contract.ts ./packages/slate-history/test/integrity-contract.ts
bun test ./packages/slate/test/collab-history-runtime-contract.ts ./packages/slate/test/selection-rebase-contract.ts
bun --filter slate-history typecheck
bun --filter slate typecheck
bun lint:fix
Add operations-contract.ts, transaction-contract.ts, and delete-contract.ts
when operation inverse or range delete code changes.
| Dimension | Score | Evidence |
|---|---|---|
| React 19.2 runtime performance | 0.92 | no React hot path planned; commits already carry dirty runtime ids in public-state.ts; React consumption is a negative constraint only |
| Slate-close unopinionated DX | 0.92 | no new public API; keeps editor.update, metadata, tags, and HistoryEditor |
| Plate and slate-yjs migration backbone | 0.93 | commit metadata, remote history skip, runtime-id locality, and collaboration tests are current source owners |
| Regression-proof testing strategy | 0.94 | exact issue-shaped package tests are named before any fix; fixed claims stay blocked until red/green proof lands |
| Research evidence completeness | 0.92 | live Slate v2 source plus local Lexical/ProseMirror/Tiptap mechanisms were refreshed for this pass |
| shadcn-style composability and hook minimalism | 0.91 | React/UI not in scope; plan avoids new hooks/components and keeps extension ergonomics out of the engine proof |
Weighted score: 0.93.
Status: done. The plan is closure-ready for Ralph execution. It is not an
issue-fix claim and adds no public API.
Status: execution slice complete.
.tmp/slate-v2/packages/slate-history/test/history-contract.ts.Fixes claims.Improves claims for the model-level partial
set_selection guard.Related because exact closure still needs matching repro
proof.Verification:
cd .tmp/slate-v2 && bun test ./packages/slate-history/test/history-contract.ts ./packages/slate-history/test/integrity-contract.ts
cd .tmp/slate-v2 && bun test ./packages/slate/test/collab-history-runtime-contract.ts ./packages/slate/test/selection-rebase-contract.ts
cd .tmp/slate-v2 && bun --filter slate-history typecheck
cd .tmp/slate-v2 && bun --filter slate typecheck
cd .tmp/slate-v2 && bun lint:fix
All gates passed after lint. The execution checkpoint is
.tmp/completion-checks/slate-v2-core-history-selection-undo-execution.md.
>= 0.92, no dimension below 0.85.