Back to Plate

Lexical history harvest rows need stack-law contracts

docs/solutions/best-practices/2026-05-09-lexical-history-harvest-rows-need-stack-law-contracts.md

53.0.63.6 KB
Original Source

Lexical history harvest rows need stack-law contracts

Problem

LexicalHistory tests are useful, but only after stripping away Lexical command notifications, shared parent editors, node keys, React harness setup, and nested editor extension wiring.

The portable behavior is the history stack law: what enters undo, what enters redo, what clears redo, and which document/selection state comes back.

Symptoms

  • A source row named LexicalHistory in sequence: change, undo, redo, undo, change looked like a CAN_UNDO/CAN_REDO command test, but the portable row was redo invalidation after a new edit.
  • A quote-node undo test looked like Lexical rich-text coverage, but the portable row was block property changes entering undo/redo as one history batch.
  • A TextNode leaf test looked like Lexical custom node dirty tracking, but the portable row was no-op updates staying out of history while property commits remain undoable.

What Didn't Work

  • Copying Lexical command payload assertions into Slate. Slate history exposes stacks and commands differently.
  • Treating nested parent/child shared history as a raw Slate invariant. That is framework/product integration until a Slate nested-editor owner accepts it.
  • Counting existing broad undo rows as enough. They did not explicitly prove the three stack laws this source file exposed.

Solution

Translate each LexicalHistory row to the narrow Slate history contract it actually exercises.

The accepted rows landed in packages/slate-history/test/history-contract.ts:

  • a new edit after undo clears redo history and later redo is a no-op
  • selected block property changes undo and redo cleanly
  • empty updates do not save history, while node property commits are undoable

The rejected rows stayed out:

  • CAN_UNDO/CAN_REDO command notification payloads
  • CLEAR_HISTORY command shape
  • shared history across parent and nested editors
  • Lexical node-key and NodeSelection internals
  • React test harness setup

Why This Works

History portability lives at the stack and committed-state level, not at the source framework's command surface.

Slate can share the same behavior without sharing Lexical APIs:

  • undo and redo stacks are observable
  • document and selection snapshots are observable
  • metadata and commit tags already own explicit grouping, merging, and skipping
  • nested editor shared history needs its own accepted owner before it becomes a Slate claim

Prevention

  • For external history tests, first classify rows as stack law, selection restoration, command API, nested-editor integration, or harness setup.
  • Add Slate rows only for stack laws and state restoration that current Slate owns.
  • Reject command payloads and shared-editor wiring unless a Slate public owner exists.
  • Keep history proof in slate-history package contracts unless the behavior needs browser/native transport.