docs/plans/2026-05-04-slate-v2-clawsweeper-v2-clipboard-serialization-ralplan.md
v2-clipboard-serialization RalplanThe next cluster is v2-clipboard-serialization.
Do not skip to core yet. The full issue-ledger order is input, DOM selection, React runtime, clipboard/import-export, core, API/DX, then performance. Input and React runtime are closed for their current local proof lanes. Clipboard is the next honest browser/runtime boundary.
Hard take: clipboard is not "the HTML parser". That framing is how Slate gets a browser junk drawer. The right target is narrower:
core fragment meaning
+ DOM DataTransfer transport
+ schema-safe internal payloads
+ app-owned rich HTML adapters
+ model-backed copy for DOM-incomplete ranges
+ cut/delete behavior that matches model selection
Current Slate Ralplan state: done. This pass selected the bucket, refreshed live source owners, ran the ClawSweeper related-issue pass for representative clipboard rows, synced the PR narrative and fork ledgers, and produced a first RALPH execution slice.
Intent:
Desired outcome:
slate owns fragment extraction/insertion semantics;slate-dom owns browser clipboard transport, MIME keys, HTML scraping,
plain-text fallback, DOM coverage policy, and payload validation;slate-react owns browser event delegation, cut/drop/paste dispatch, and DOM
coverage materialization before editable browser interactions;In scope:
.tmp/slate-v2/packages/slate/src fragment extraction/insertion contracts;.tmp/slate-v2/packages/slate-dom/src/plugin/dom-clipboard-runtime.ts;.tmp/slate-v2/packages/slate-react/src/editable/clipboard-input-strategy.ts;.tmp/slate-v2/packages/slate-dom/test/clipboard-boundary.ts;.tmp/slate-v2/packages/slate/test/clipboard-contract.ts;Non-goals:
text/html and text/plain payloads;editor.clipboard.Decision boundaries:
Fixes #... needs a current repro and focused proof.Unresolved user-decision points:
28 v2-clipboard-serialization rows, including
#5630, #5616, #5429, #5328, #5233, #5151, #3857, #3801, #3557, #3486,
#3469, #3155, #5089, #5005, #4906, #4888, #4857, #4810, #4806, #4802,
#4716, #4613, #4567, #4542, #4440, #4104, #2694, and #1024.docs/slate-issues/requirements-from-issues.md routes clipboard and
serialization pressure to explicit internal fragment format ownership,
HTML/plain-text import/export boundaries, foreign-editor isolation, and
configurable payload keys.gitcrawl --json threads ianstormtaylor/slate --numbers 5328,4857,5233,3486,4542,5151,4802,4806 --include-closed confirms the
representative rows are still open and current in the local gitcrawl corpus.gitcrawl --json cluster-detail ianstormtaylor/slate --id 21 confirms #4802
and #4806 are one active inline-void clipboard family..tmp/slate-v2/packages/slate-dom/src/plugin/dom-clipboard-runtime.ts:51
writes model-backed Slate fragments, :69 exports DOM selection data, :88
checks DOM coverage boundaries before copy, :107 falls back to model-backed
copy when DOM range export fails, :183 runs extension-owned
dom.clipboard.insertData handlers, and :202 imports Slate fragment data..tmp/slate-v2/packages/slate-dom/src/plugin/dom-clipboard-runtime.ts:211
decodes and parses the embedded fragment payload directly. A malformed or
foreign data-slate-fragment can still throw before the fallback path has a
chance to handle plain text or app-owned HTML..tmp/slate-v2/packages/slate-dom/test/clipboard-boundary.ts:186 proves DOM
clipboard APIs stay under editor.dom.clipboard, :196 proves selected
fragment round-trip and target replacement, :241 proves custom MIME keys,
:282 proves embedded HTML fragment fallback, :316 proves plain-text
fallback, :338 proves decorated DOM export strips render-only wrappers,
:367 proves multiline plain text preserves target block type, and :401
proves expanded selection replacement with every pasted line..tmp/slate-v2/packages/slate/test/clipboard-contract.ts:18 proves core
fragment extraction and insertion, including mixed inline extraction and
expanded replacement..tmp/slate-v2/packages/slate-react/src/editable/clipboard-input-strategy.ts:157
delegates copy to editor.dom.clipboard.writeSelection, :181 writes and
deletes expanded cut selections, :141 materializes DOM coverage paste
targets, and :288 delegates paste to editor.dom.clipboard.insertData..tmp/slate-v2/packages/slate-react/src/plugin/with-react.ts:17 exposes
withReact(editor, clipboardFormatKey?), preserving value generics and
passing the format key to withDOM.Current-state read result:
slate owns
fragment semantics, slate-dom owns DataTransfer transport, and slate-react
delegates events.data-slate-fragment false positives.Principles:
Drivers:
28 rows into this bucket.Options:
| Option | Verdict | Why |
|---|---|---|
| Put a generic HTML serializer/deserializer in raw Slate | reject | That turns raw Slate into product policy and will still fail real Word/Docs cases. |
Keep trusting data-slate-fragment | reject | Clipboard data is foreign input. A regex hit is not proof of a valid Slate fragment. |
| Remove embedded HTML fragments and rely only on custom MIME | reject | Safari/cross-browser flows need the HTML carrier. |
Keep editor.dom.clipboard plus fail-closed payload validation | choose | Matches live source and fixes the real trust-boundary gap. |
Public editor.clipboard namespace | reject | The existing test explicitly proves clipboard is a DOM host capability, not a core editor namespace. |
Chosen shape:
copy/cut:
model selection -> fragment
DOM coverage policy decides native DOM export vs model-backed export
DataTransfer receives custom application/<format-key>, text/html, text/plain
paste/drop:
extension-owned dom.clipboard.insertData handlers first
safe internal fragment decode/validate next
app-owned HTML handlers when registered
plain-text fallback last
Consequences:
data-slate-fragment, then a fail-closed import helper.withReact(editor, clipboardFormatKey?) can stay as the current minimal API;
a future options object is an API/DX topic, not required for this fix.Fixes after Slice 5 keyed embedded-fragment proof.
#4569 also moves to Fixes because the current insertData docs now state
capability order and fallback behavior.Reviewed with ClawSweeper:
| Issue | Current classification | Decision |
|---|---|---|
| #5328 | cluster-synced | First execution slice. Malformed or text-node data-slate-fragment must fail closed. |
| #4857 | cluster-synced | Same trust-boundary family, but exact NYTimes/select-all repro needs browser proof before Fixes. |
| #5233 | fixes-claimed | Slice 5 proves custom fragment format keys isolate MIME payloads and embedded HTML fallback fragments. |
| #3486 | fixes-claimed | Older duplicate-family wording for custom setData id; covered with #5233. |
| #4542 | cluster-synced | Fragment insertion shape and empty-block behavior need separate model/browser proof. |
| #5151 | improves-claimed | Rich fragment target-block preservation is covered for the selected single text-block replacement case; exact browser repro closure is not claimed. |
| #4802 | improves-claimed | Selected inline void export keeps the Slate fragment payload without assuming block-void spacer DOM; no external-editor closure without browser payload proof. |
| #4806 | improves-claimed | Selected inline void copy/paste/cut ordering is covered through the DOM clipboard contract; exact browser repro closure is not claimed. |
New fixed issue claims from Slice 5:
The PR auto-close count is now 6:
Owner:
.tmp/slate-v2/packages/slate-dom/src/plugin/dom-clipboard-runtime.ts.tmp/slate-v2/packages/slate-dom/test/clipboard-boundary.tsGoal:
data-slate-fragment payloads must not throw;false without mutating the document.Representative issues:
Tests first:
bun test packages/slate-dom/test/clipboard-boundary.ts
Acceptance:
Owner:
.tmp/slate-v2/packages/slate/test/clipboard-contract.ts.tmp/slate-v2/packages/slate-dom/test/clipboard-boundary.tsGoal:
Representative issues:
Owner:
.tmp/slate-v2/packages/slate-react/src/editable/clipboard-input-strategy.tsGoal:
text/plain/text/html output deterministic;Representative issues:
Owner:
.tmp/slate-v2/packages/slate-react/src/editable/clipboard-input-strategy.ts.tmp/slate-v2/packages/slate/src delete/fragment contractsGoal:
Representative issues:
Owner:
withReact(editor, options?)withDOM(editor, options?)dom.clipboard.insertData capabilitiesGoal:
editor.dom.clipboard;slate-dom.Representative issues:
Status: complete for current-state, decision brief, implementation, docs, and focused verification.
Verdict: revise Slice 5 before ralph. The current direction is right, but
the old withReact(editor, clipboardFormatKey?) signature should not survive
the v2 cut. A lone positional string is cramped API design: it solves one issue
while reserving no sane place for later React/DOM adapter options.
Hard cut target:
type DOMEditorOptions = {
/**
* Bare DataTransfer subtype used for Slate's internal fragment payload.
* Slate writes and reads `application/${clipboardFormatKey}`.
*/
clipboardFormatKey?: string
}
type ReactEditorOptions = DOMEditorOptions
withDOM(editor, options?: DOMEditorOptions)
withReact(editor, options?: ReactEditorOptions)
No compatibility overload for withReact(editor, 'x-proof-fragment'). Common
usage stays unchanged:
withReact(createEditor());
Custom usage becomes explicit:
withReact(createEditor(), { clipboardFormatKey: "x-proof-fragment" });
Pre-Slice-5 live source evidence:
.tmp/slate-v2/packages/slate-react/src/plugin/with-react.ts:17 still exposes
clipboardFormatKey?: string..tmp/slate-v2/packages/slate-dom/src/plugin/with-dom.ts:37 still takes the
same positional string and forwards it to the DOM clipboard runtime..tmp/slate-v2/packages/slate-dom/src/plugin/dom-clipboard-runtime.ts:35
stores the key in a WeakMap; :82 writes
application/${clipboardFormatKey}; :247 reads that MIME key..tmp/slate-v2/packages/slate-dom/src/plugin/dom-clipboard-runtime.ts:249
still falls back to unkeyed embedded HTML data-slate-fragment..tmp/slate-v2/packages/slate-dom/test/clipboard-boundary.ts:386 proves the
custom MIME payload path, but did not prove cross-schema safety before Slice
5 because the embedded HTML fallback was still unkeyed.Critical correction from the review pass:
#5233 was not just docs. The custom MIME key was custom, but text/html
still carried data-slate-fragment without a format marker. A receiving
default editor could still import schema-private JSON through the HTML fallback.
Slice 5 keys the embedded fallback too.
Target embedded-fragment rule:
data-slate-fragment plus a fragment-format marker, for example
data-slate-fragment-format="x-proof-fragment";clipboardFormatKey;x-slate-fragment key, preserving default Slate-to-Slate paste;Extension surface target:
editor.dom.clipboard;slate-dom, not from the private runtime
file:type DOMClipboardInsertDataHandler = (
editor: DOMEditor,
data: DataTransfer,
) => boolean | void;
dom.clipboard.insertData capability;true to stop the default Slate fragment/text
fallback.Issue accounting from this pass:
| Issue | Status for Slice 5 | Reason |
|---|---|---|
| #5233 | Fixes | Exact ask is custom fragment format isolation; Slice 5 covers options API plus keyed HTML fallback proof. |
| #3486 | Fixes with #5233 | Older custom setData id request; same root requirement. |
| #1024 | Improves | MIME identity discussion maps to keyed payloads and keyed embedded fragments, but not a full document MIME system. |
| #4613 | Improves | Existing dom.clipboard.insertData capability is now publicly typed and documented, but broad override closure is not claimed. |
| #4569 | Fixes | Docs now state insertData capability order, handler return semantics, and fallback behavior. |
| #4440 | Related, not fixed | Output customization for plain text/HTML needs a later writeSelection/serializer capability. Do not cram it into Slice 5. |
| #3557 | Related, not fixed | General method overriding is handled by v2 extension/transform APIs, not clipboard-specific enough for this slice. |
Decision brief:
Rejected alternatives:
editor.clipboard: rejected because the low-level DOM transport belongs
under editor.dom.clipboard.Implementation acceptance criteria:
withReact and withDOM use options objects with preserved value generics;{ clipboardFormatKey };editor.clipboard namespace appears;DOMClipboardInsertDataHandler is exported from a public slate-dom surface;unknown;Score for this pass:
withReact(createEditor()) remains tiny; custom key
call becomes self-documenting.gitcrawl checked #5233, #3486, #1024,
#4613, #4569, #4440, and #3557.slate-dom.Total: 0.90. Slice 5 is complete; next pass is large payload performance.
Slice 5 implementation evidence:
withReact(editor, clipboardFormatKey?) and
withDOM(editor, clipboardFormatKey?) to options-object APIs;data-slate-fragment-format;DOMClipboardInsertDataHandler from public slate-dom;clipboardFormatKey, capability order, handler return semantics,
and app-owned rich HTML fallback policy;Owner:
.tmp/slate-v2/scripts/benchmarks/core/current/clipboard-large-payload.mjs.tmp/slate-v2/scripts/benchmarks/slate/5945-large-plaintext-paste.mjs.tmp/slate-v2/scripts/benchmarks/README.md.tmp/slate-v2/package.jsonGoal:
Representative issues:
Hard cut:
Fixes #4056, Fixes #5945, or Fixes #5992 until the benchmark lane
reproduces the original workload and shows an accepted improvement.Current source evidence:
.tmp/slate-v2/package.json has benchmark commands for core, React, history,
and #6038, but no clipboard large-payload command..tmp/slate-v2/scripts/benchmarks/README.md says new lanes belong under
scripts/benchmarks/**, artifacts belong in .tmp/, and public bench:*
command names are the contract..tmp/slate-v2/packages/slate-dom/src/plugin/dom-clipboard-runtime.ts currently
pays clipboard costs in these places: model-backed fragment JSON.stringify
and btoa, DOM range cloneContents, temporary div.innerHTML, plain text
extraction, decodeURIComponent/atob/JSON.parse, text.split, per-line
insertText, and fragment insertion..tmp/slate-v2/packages/slate/src/transforms-text/insert-fragment.ts currently
walks fragment nodes, builds starts/middles/ends, splits, inserts, and
normalizes. That is likely correct architecture; Slice 6 must measure it
before changing it.docs/slate-issues/benchmark-candidate-map.md already marks #5945 as
ready-now, #5992 as ready-with-minor-setup, and #4056 as
ready-with-minor-setup.gitcrawl threads ianstormtaylor/slate --numbers 4056,5945,5992 --include-closed --json confirms all three rows are open in the local live
corpus.Benchmark cohorts:
| Cohort | Payload/document shape | Purpose |
|---|---|---|
| small | 10 lines/blocks | catch fixed overhead and correctness drift |
| normal | 100 lines/blocks | default editor sanity |
| large | 1,000 lines/blocks | first real #5945-like pressure |
| stress | 10,000 lines/blocks | exact generated #5945 workload |
| pathological | 50,000 existing blocks plus two-node cut | #5992-shaped huge-document cut |
Required lanes:
| Lane | Measures | Issue pressure |
|---|---|---|
plainTextSplitMs | text/plain newline split and line-count scaling | #5945, #4056 |
plainTextInsertMs | public insert-data path for many newline blocks | #5945, #4056 |
fragmentEncodeMs | model-backed fragment JSON/base64/text/html payload creation | #4056, #5992 |
fragmentDecodeMs | custom MIME and embedded HTML fragment decode/parse | #4056 |
fragmentInsertMs | core fragment insertion into selected range | #5945, #4056 |
cutTwoBlocksMs | copy payload creation plus selected range delete in huge document | #5992 |
fullSelectionCopyMs | model-backed copy for large selected text | #4056 |
Metrics:
text/plain, text/html, and
application/${clipboardFormatKey};.tmp/slate-v2/tmp/slate-clipboard-large-payload-benchmark.json.Decision rule:
Not claimed until
the benchmark row reproduces the original workload and the improvement is
documented.slate.slate-dom.Candidate implementation moves after baseline:
innerHTML work for model-backed copy when DOM coverage
already proves the selection is model-owned;split/insert behavior current unless the benchmark proves it is the
bottleneck.Performance review:
js-combine-iterations, js-length-check-first,
js-set-map-lookups, js-cache-property-access only after baseline points
to a repeated JavaScript loop.Fast gates:
cd .tmp/slate-v2
bun run bench:slate:5945:local
bun test ./packages/slate-dom/test/clipboard-boundary.ts
bun test ./packages/slate/test/clipboard-contract.ts
bun --filter slate-dom typecheck
bun --filter slate-react typecheck
bun lint:fix
Broader gate only if browser-visible behavior changes:
cd .tmp/slate-v2
PLAYWRIGHT_RETRIES=0 bunx playwright test playwright/stress/generated-editing.test.ts -g "paste-normalize-undo" --project=chromium
Baseline execution result:
.tmp/slate-v2/scripts/benchmarks/core/current/clipboard-large-payload.mjs.bun run bench:slate:5945:local..tmp/slate-v2/tmp/slate-clipboard-large-payload-benchmark.json.SLATE_CLIPBOARD_BENCH_STRESS_LINES=10000 SLATE_CLIPBOARD_BENCH_HUGE_CUT_BLOCKS=50000.930.39ms, 1,000-node fragment insert mean 1196.83ms,
full selection copy mean 1.65ms, and plain split mean 0.03ms.3545.36ms, 2,000-node fragment insert mean
4615.83ms, full selection copy mean 5.25ms, and plain split mean
0.05ms.368.96ms.1000 lines around 1316ms, 2000
lines around 5412ms) because insertFragment is slower than the existing
split/insert path for this payload. The candidate was removed.Baseline verdict:
Not claimed because this slice recorded a
bounded benchmark plus one cleanup, not an accepted issue-size improvement.First slice gates:
cd .tmp/slate-v2
bun test packages/slate-dom/test/clipboard-boundary.ts
bun --filter slate-dom typecheck
bun lint:fix
Broader gates when the touched behavior expands:
cd .tmp/slate-v2
bun test packages/slate/test/clipboard-contract.ts
bun --filter slate-react test:vitest -- dom-coverage-native-bridge-contract
bun --filter slate-react test:vitest -- editing-kernel-contract
bun test:integration-local --grep "richtext|highlighted-text|rendering-strategy-runtime|mentions|inlines"
Use browser proof only when the slice changes browser-visible copy/paste/cut behavior. Use benchmark proof before large-paste performance claims.
data-slate-fragment just be ignored?"Yes, unless the valid custom MIME payload is present. Clipboard import is a trust boundary. Bad embedded fragments should never throw or block plain-text fallback.
clipboardFormatKey enough for #5233?"Mostly, but do not auto-close yet. The live API exists, but the exact issue asked for safe cross-schema behavior. We need public docs/JSDoc plus proof that only the configured MIME key is treated as authoritative.
slate-dom?"No. slate-dom should carry data across the browser boundary. Apps decide how
to map foreign HTML into their schema.
Yes. That is still safer than stale DOM. Model-backed copy must be explicit in policy and proof.
Only as Improves. The focused DOM contract now proves selected inline void
copy/paste/cut ordering and fragment preservation. Exact Fixes still needs
browser replay of #4802/#4806 and exported payload inspection.
Added in this pass:
v2-clipboard-serialization as the next bucket;slate-dom, slate-react, and slate clipboard owners;Dropped:
Unchanged:
clawsweeper: applied. Related issue pass ran through the active issue
matrix, live gitcrawl representative threads, and fork ledger updates.goal workflow: applied through docs/plans, checkpoint, and
active goal state.learnings-researcher: applied. Relevant solution notes confirm the split:
core fragment meaning, DOM transport, React delegation, app-owned rich HTML.
The expected docs/solutions/patterns/critical-patterns.md file is absent in
this repo.tdd: applied through Slice 1 malformed-fragment tests and Slice 2 rich
fragment target-block preservation tests.performance: applied for Slice 6 planning. The next slice must add a
benchmark lane before any large paste/copy/cut performance claim.| Pass | Status | Owner | Evidence | Next |
|---|---|---|---|---|
fail-closed-internal-fragment-import | complete | .tmp/slate-v2/packages/slate-dom clipboard runtime | Added focused malformed-fragment tests in .tmp/slate-v2/packages/slate-dom/test/clipboard-boundary.ts; added safe fragment decode/parse in .tmp/slate-v2/packages/slate-dom/src/plugin/dom-clipboard-runtime.ts; verified with focused clipboard test, slate-dom typecheck, and lint fix. | Continue with Slice 2: fragment insertion shape and selection placement. |
fragment-insertion-shape-and-selection-placement | complete for #5151-shaped slice | .tmp/slate-v2/packages/slate fragment insertion | Added focused core and DOM clipboard tests proving a rich single text-block fragment preserves the receiving text-block type and post-insert selection; implemented the selected target-block ownership path in .tmp/slate-v2/packages/slate/src/transforms-text/insert-fragment.ts; #5151 moved to Improves; fixed issue claims unchanged. | Continue with Slice 3: inline void copy/cut/paste. |
inline-void-copy-cut-paste | complete for package-level DOM proof | .tmp/slate-v2/packages/slate-dom clipboard runtime | Added a selected inline void clipboard regression proving no block-void spacer DOM assumption, preserved Slate fragment payload, FEFF-free external text output, paste round-trip, and cut-shaped delete ordering; implemented a safe attachment fallback in .tmp/slate-v2/packages/slate-dom/src/plugin/dom-clipboard-runtime.ts; #4802/#4806 moved to Improves; fixed issue claims unchanged. | Continue with Slice 4: structural cut/delete. |
structural-cut-delete | complete for package-level model/React proof | .tmp/slate-v2/packages/slate-react clipboard cut dispatch and .tmp/slate-v2/packages/slate fragment/delete contracts | Added a selected block void cut regression proving model-backed clipboard data, single void removal, and model-owned DOM repair; added core list-fragment/delete proofs for whole-list wrapper extraction and deletion across a list without orphan list-item; #3857/#3801/#3469 moved to Improves; #4716 remains Related; #2694 remains Not claimed. | Continue with Slice 5: API and extension surface. |
api-and-extension-surface-ralplan | complete for current-state and decision brief | .tmp/slate-v2/packages/slate-react/src/plugin/with-react.ts, .tmp/slate-v2/packages/slate-dom/src/plugin/with-dom.ts, .tmp/slate-v2/packages/slate-dom/src/plugin/dom-clipboard-runtime.ts | Pre-Slice-5 source showed custom MIME support existed, but the positional string API was weak and embedded HTML fallback was unkeyed. Plan target changed to withReact(editor, options?) / withDOM(editor, options?), keyed embedded fragments, public insertData handler type, and docs/example proof. | Slice 5 executed; keep as planning evidence. |
api-and-extension-surface | complete | .tmp/slate-v2/packages/slate-dom, .tmp/slate-v2/packages/slate-react, .tmp/slate-v2/site/examples, .tmp/slate-v2/docs/libraries/slate-react | Added focused custom-key embedded HTML acceptance/rejection tests; hard-cut withReact/withDOM to options objects; keyed embedded HTML fallback fragments; exported DOMClipboardInsertDataHandler; typed rich HTML/image example handlers; synced docs and issue claims. #5233/#3486/#4569 moved to Fixes; #1024/#4613 moved to Improves; #4440/#3557 remain Related. | Continue with Slice 6: large payload performance. |
large-payload-performance-ralplan | complete for benchmark-first plan | .tmp/slate-v2/scripts/benchmarks, .tmp/slate-v2/packages/slate-dom, .tmp/slate-v2/packages/slate | Current source has no large clipboard payload benchmark command. Plan now defines cohorts, lanes, metrics, artifact path, no-claim policy, and fast gates for #4056/#5945/#5992. | RALPH should execute Slice 6 baseline benchmark. |
large-payload-performance-baseline | complete for bounded local baseline plus one cleanup | .tmp/slate-v2/scripts/benchmarks/core/current/clipboard-large-payload.mjs, .tmp/slate-v2/scripts/benchmarks/slate/5945-large-plaintext-paste.mjs, .tmp/slate-v2/package.json, .tmp/slate-v2/packages/slate-dom/src/plugin/dom-clipboard-runtime.ts | Added benchmark command and artifact. Latest bounded run shows insertion still dominates: 2,000-line plain text insert mean 3545.36ms, 2,000-node fragment insert mean 4615.83ms, full selection copy mean 5.25ms, split mean 0.05ms, and 10,000-block two-node cut mean 368.96ms. Exact issue-size gate stays env-controlled because the first 10,000-line/50,000-block attempt exceeded roughly 150s. Rejected fragment-materialization candidate because it worsened the lane; kept transform-registry caching only. | Continue with deeper split/insert normalization optimization; fixed issue claims unchanged. |
Verdict: keep executing the clipboard program. Slice 6 is now ready for RALPH: add the large clipboard payload benchmark first, record baseline, and optimize only the measured bottleneck.