docs/plans/2026-05-08-slate-v2-new-tests-architecture-cleanup-ralplan.md
status: done
score: 0.94
date: 2026-05-08
target: .tmp/slate-v2
source plan: docs/editor-test-harvester/lexical/report.md
skill: slate-ralplan
Do not rewrite Slate v2 for this. The new Lexical-derived tests are a useful pressure pass, and the selected lane is green, but the cleanup target is small:
state.marks.get().ClipboardEvent, semantic handles, and raw device proof are different
claims.The architecture is basically right. The current rough edge is example-local typing and organization, not core/runtime design.
Implemented the approved cleanup in .tmp/slate-v2:
site/examples/ts/mark-utils.ts;site/examples/ts/custom-types.d.ts so CustomTextKey is derived
from boolean leaf fields and excludes style-only attrs like fontSize;state.marks.get() casts from richtext.tsx,
iframe.tsx, and hovering-toolbar.tsx;site/examples/ts/paste-html-import.ts;site/examples/ts/paste-html.tsx focused on rendering and editor
composition.No core, runtime, public API, issue claim, or table-selection model changed.
.tmp/slate-v2/site/examples/ts/paste-html.tsx.tmp/slate-v2/site/examples/ts/custom-types.d.tsrichtext.tsx, iframe.tsx, and
hovering-toolbar.tsxslate,
slate-dom, or slate-react.Pressure test: if the fix starts adding Google Docs, Google Sheets, Word, or table-selection policy to Slate core, it is going the wrong way.
Principles:
Top drivers:
paste-html.tsx now mixes UI, parser policy, transport handling, and leaf
rendering.CustomTextKey now excludes fontSize, but callers still cast marks because
boolean marks and style attributes share one broad leaf type.Viable options:
| Option | Verdict | Why |
|---|---|---|
| Leave as-is | reject | Green, but repeated casts and a swollen example file will keep getting worse as more source-app paste corpus rows land. |
| Move paste parsing to core | reject | This would make raw Slate opinionated about Google Docs/Sheets/Word policy. Bad trade. |
| Extract example-local helpers and split mark/style typing | choose | Keeps core clean, preserves proof, and removes the actual maintenance debt. |
| Build Lexical-style whole-table selection now | reject for this lane | Current tests do not prove that model and Slate v2 does not own it yet. |
Consequences:
| Surface | Current owner | Finding |
|---|---|---|
| HTML paste parser | .tmp/slate-v2/site/examples/ts/paste-html.tsx:32-231 | Element/text tag maps, font-size normalization, styled text import, fragment normalization, and deserialize live inside the component file. |
| Paste transport | .tmp/slate-v2/site/examples/ts/paste-html.tsx:263-305 | dom.clipboard.insertData remains the right extension point; iOS plain-text prediction is app policy. |
| Paste leaf style | .tmp/slate-v2/site/examples/ts/paste-html.tsx:406-434 | fontSize rendering is leaf style policy, not a toolbar mark. |
| Custom leaf type | .tmp/slate-v2/site/examples/ts/custom-types.d.ts:161-177 | CustomText includes boolean marks plus fontSize; CustomTextKey excludes fontSize. |
| Mark casts | .tmp/slate-v2/site/examples/ts/richtext.tsx:159-164, iframe.tsx:81-86, hovering-toolbar.tsx:62-67 | Three examples repeat the same cast around state.marks.get(). |
| Table containment proof | .tmp/slate-v2/playwright/integration/examples/tables.test.ts:129-179 | Tests lock triple-click and drag containment without claiming whole-table selection. |
| Browser transport proof | .tmp/slate-v2/playwright/integration/examples/plaintext.test.ts:23-77, editable-voids.test.ts:48-71 | execCommand, synthetic paste, and native input paste are intentionally separate rows. |
| IME helper | .tmp/slate-v2/packages/slate-browser/src/playwright/ime.ts:22-95 | Synthetic composition clones the DOM range before mutation; native Chromium CDP remains the stronger path when available. |
| IME/history proof | .tmp/slate-v2/playwright/stress/generated-editing.test.ts:1070-1179, .tmp/slate-v2/packages/slate-history/test/history-contract.ts:259-305 | Composition-adjacent rows and history unit rows exist and should stay unchanged. |
| Source | Mechanism | Slate target | Verdict |
|---|---|---|---|
Lexical harvested tests: ../lexical/packages/lexical-playground/__tests__/e2e/Composition.spec.mjs, History.spec.mjs, CopyAndPaste/*, Selection.spec.mjs, Extensions.spec.mjs | Issue-shaped browser rows for IME, paste corpus, browser transport, and table selection. | Keep behavior rows, not Lexical node classes, commands, or table model. | partial |
Lexical table package tests: ../lexical/packages/lexical-table/src/__tests__/unit/LexicalTableExtension.test.ts | Dedicated table selection / nested-table policy. | Defer whole-table selection until Slate v2 owns a table-selection model. | diverge |
ProseMirror composition tests: ../prosemirror/view/test/webtest-composition.ts:87-305 | Heavy composition matrix around marks, cursor wrappers, widgets, overlap, and cross-paragraph changes. | Keep growing browser/composition coverage while isolating runtime behavior from app paste policy. | agree |
ProseMirror composition runtime: ../prosemirror/view/src/input.ts:435-565 | Runtime tracks active composition, Safari/Android quirks, pending DOM records, and composition node ownership. | Keep IME/runtime policy in slate-react / slate-browser, not in examples. | agree |
ProseMirror clipboard tests: ../prosemirror/view/test/webtest-clipboard.ts:44-123 | External HTML parsing stays parser/schema-owned with transformation hooks. | Slate exposes capability hooks; app examples choose source-specific parsing. | partial |
No public Slate API change for this cleanup.
Accepted target:
dom.clipboard.insertData remains the app-owned rich paste extension point.fontSize stays a leaf attribute in the paste-html example.Rejected target:
editor.clipboard as a new public namespace.parseExternalHtmlFromGoogleDocsOrSheets.TableSelection without a real Slate table-selection design.No runtime rewrite. The existing runtime tests are good because they isolate transport classes:
slate-browser helper.slate-dom unit tests.The next implementation should avoid touching slate-react runtime unless a
test fails after pure helper extraction.
Implement a tiny example-local helper layer:
site/examples/ts/mark-utils.ts
type BooleanCustomTextKey = ...toggleBooleanMark(editor, key)isBooleanMarkActive(editor, key)fontSize out of toolbar mark controls.For paste HTML:
paste-html.tsx into an example-local file,
likely site/examples/ts/paste-html-import.ts.paste-html.tsx.No new adapter work. The cleanup preserves the important migration shape:
This is friendly to Plate and slate-yjs because it does not bake product import rules into the core substrate.
ClawSweeper related-issue pass: skipped.
Reason: the implementation is a behavior-neutral helper extraction/type cleanup. It does not change public API, runtime behavior, browser behavior, examples' observable output, issue claims, or PR narrative. Existing issue-ledger rows and PR references stay unchanged.
Known nearby issue refs from existing ledgers:
| Issue | Cluster | Claim | Why | Proof route | Live ledger sync | PR line |
|---|---|---|---|---|---|---|
| #6034 | DOM selection / table edge | no new claim | Existing PR reference already claims ArrowDown-at-last-table-cell. This cleanup does not widen it. | existing table Playwright row | unchanged | unchanged |
| Mobile/IME macro rows | mobile/IME/input | no new claim | Current plan explicitly refuses raw mobile closure without device proof. | none in this cleanup | unchanged | unchanged |
| Clipboard corpus rows | clipboard/paste | no new claim | Existing clipboard execution lane already owns claims; helper extraction should be behavior-neutral. | existing unit/browser gates | unchanged | unchanged |
Required after implementation:
cd /Users/zbeyens/git/slate-v2
bunx playwright test playwright/integration/examples/paste-html.test.ts --project=chromium
bunx playwright test playwright/integration/examples/plaintext.test.ts --project=chromium
bunx playwright test playwright/integration/examples/plaintext.test.ts --project=firefox
bunx playwright test playwright/integration/examples/editable-voids.test.ts --project=chromium -g "keeps native paste inside editable void input"
bunx playwright test playwright/integration/examples/editable-voids.test.ts --project=firefox -g "keeps native paste inside editable void input"
PLAYWRIGHT_RETRIES=0 bunx playwright test playwright/integration/examples/tables.test.ts --project=chromium
STRESS_FAMILIES=ime-composition-inline-void-boundary,ime-composition-undo,paste-html-image-void PLAYWRIGHT_RETRIES=0 bunx playwright test playwright/stress/generated-editing.test.ts --project=chromium
bun check
Run Playwright commands sequentially. The Next webserver build can collide when parallelized.
Fresh planning-pass verification, run from /Users/zbeyens/git/slate-v2 on
2026-05-08:
| Command | Result |
|---|---|
bun check | passed |
bunx playwright test playwright/integration/examples/paste-html.test.ts --project=chromium | passed, 8 tests |
PLAYWRIGHT_RETRIES=0 bunx playwright test playwright/integration/examples/tables.test.ts --project=chromium | passed, 9 tests |
bunx playwright test playwright/integration/examples/plaintext.test.ts --project=chromium | passed, 3 tests |
bunx playwright test playwright/integration/examples/plaintext.test.ts --project=firefox | passed, 2 tests, 1 expected skip for blocked synthetic clipboard data |
bunx playwright test playwright/integration/examples/editable-voids.test.ts --project=chromium -g "keeps native paste inside editable void input" | passed, 1 test |
bunx playwright test playwright/integration/examples/editable-voids.test.ts --project=firefox -g "keeps native paste inside editable void input" | passed, 1 test |
STRESS_FAMILIES=ime-composition-inline-void-boundary,ime-composition-undo,paste-html-image-void PLAYWRIGHT_RETRIES=0 bunx playwright test playwright/stress/generated-editing.test.ts --project=chromium | passed, 3 tests |
These proved the current new-test lane was green before cleanup.
Fresh implementation verification, run from /Users/zbeyens/git/slate-v2 after
cleanup:
| Command | Result |
|---|---|
bun check | passed |
bunx playwright test playwright/integration/examples/paste-html.test.ts --project=chromium | passed, 8 tests |
bunx playwright test playwright/integration/examples/richtext.test.ts --project=chromium -g "mark|toolbar bold" | passed, 12 tests |
bunx playwright test playwright/integration/examples/iframe.test.ts --project=chromium | passed, 2 tests |
bunx playwright test playwright/integration/examples/hovering-toolbar.test.ts --project=chromium | passed, 4 tests |
| Lens | Status | Result |
|---|---|---|
| intent-boundary-pass | applied | Scope is helper extraction/type cleanup; no runtime rewrite. |
| tdd | applied | Existing harvested tests are the behavior lock. This cleanup should not add new behavior tests unless a refactor exposes a gap. |
| vercel-react-best-practices | applied | Avoid new render subscriptions; helper extraction must keep selectors stable and no inline component churn. |
| performance-oracle | applied | Parser helpers are linear in DOM nodes; no new global caches or repeated DOM walks. |
| steelman-pass | applied | Strongest objection is that cleanup churn risks breaking green proof for little payoff. Chosen plan wins because it removes repeated casts and file bloat without behavior change. |
| high-risk-deliberate-pass | applied | Browser-sensitive proof exists; any runtime change is out of scope. |
fontSize and breaks formatted paste.
Rollback answer: revert helper extraction first; do not patch runtime unless a focused test proves runtime drift.
dom.clipboard.insertData capability shape.slate-browser transport distinction.| Objection | Answer | Verdict |
|---|---|---|
| "Why touch green code?" | Because the green code introduced repeated casts and a swollen example parser. The fix is organization-only and test-locked. | keep |
| "Why not solve all table selection now?" | That is a new table model, not cleanup. Shipping fake proof would be worse. | keep defer |
| "Why not make paste HTML first-class?" | Slate should expose hooks; apps choose import policy. ProseMirror and Slate both support transformation/parser boundaries without hard-coding source apps. | keep |
| Pass | Status | Evidence added | Plan delta | Open issues | Next owner |
|---|---|---|---|---|---|
| Current-state read and initial score | complete | Live source/test reads listed above; Lexical and ProseMirror evidence sampled; fresh .tmp/slate-v2 bun check and focused browser rows passed. | Created cleanup plan; chose helper extraction over rewrite. | none | implementation |
| Related issue discovery | complete | Existing nearby refs identified only. | Skipped ClawSweeper because implementation stayed behavior-neutral and claim-neutral. | none | none |
| Issue ledger pass | complete | No claim/API/runtime/browser behavior changed. | Ledgers unchanged by design. | none | none |
| Implementation cleanup | complete | Mark helper extraction and paste-html parser helper extraction landed in .tmp/slate-v2. | Removed repeated casts and shrank paste-html.tsx. | none | verification |
| Closure score | complete | bun check, paste-html, richtext mark, iframe, and hovering-toolbar Playwright rows passed after cleanup. | Status set to done. | none | none |
None for this cleanup. Future table selection needs a separate API/model plan.
state.marks.get() casts.insertHtmlData wired through dom.clipboard.insertData.fontSize rendering behavior unchanged.bun check./Users/zbeyens/git/slate-v2.bun check passes from /Users/zbeyens/git/slate-v2.bun run completion-check passes from /Users/zbeyens/git/plate-2.Current state is done because the approved cleanup was implemented under an
execution lane after the Ralplan pass, with no claim-changing issue surface and
fresh .tmp/slate-v2 verification.