docs/plans/2026-05-03-slate-v2-dom-coverage-full-execution-ralplan.md
status: done-full-execution-complete
created: 2026-05-03
skill: .agents/skills/slate-ralplan/SKILL.md
performance lens: .agents/skills/performance/SKILL.md
source repo: /Users/zbeyens/git/slate-v2
supersedes:
docs/plans/2026-05-02-slate-v2-hidden-subtree-first-class-ralplan.mddocs/plans/2026-05-03-slate-v2-staged-rendering-strategy-phase-6-plan.mdUse one Slate-owned primitive for every model-present / DOM-incomplete region:
DOMCoverageBoundary
Do not split this into separate systems for collapsed content, rendering strategy staging, shell strategy, and viewport virtualization. That would multiply the exact DOM lookup failure class Slate v2 is trying to kill.
The final shape is:
cheap staged default
+ DOM coverage registry and bridge
+ unstable Boundary authoring adapter
+ reason-specific policy engines
+ stress-only virtualization prototype
+ production perf/RUM visibility
No phase is left as a vague future bucket. Some phases have strict release gates, but they are in the plan and owned.
Hard take:
DOM coverage is the primitive.
Collapse is one policy.
Large-doc staging is one policy.
Virtualization is one policy.
Shell is one explicit aggressive policy.
Intent:
Desired outcome:
slots.unstableBoundary to stable slots.Boundary
only after lifecycle, browser, stress, and DX gates pass.In scope:
Non-goals:
Activity as a replacement for editor DOM coverage.Decision boundaries:
Current live source already contains the internal primitive and an unstable adapter:
DOMCoverageBoundary states/reasons/policies exist in
/Users/zbeyens/git/slate-v2/packages/slate-dom/src/plugin/dom-coverage.ts:25.app-collapse, app-hidden, rendering-staged,
viewport-virtualization, shell-aggressive, and runtime-atom in
/Users/zbeyens/git/slate-v2/packages/slate-dom/src/plugin/dom-coverage.ts:32./Users/zbeyens/git/slate-v2/packages/slate-dom/src/plugin/dom-coverage.ts:134.DOMCoverageBoundaryRange registers child-range coverage in
/Users/zbeyens/git/slate-v2/packages/slate-react/src/components/dom-coverage-boundary.tsx:32.DOMCoverageSelfBoundary registers self coverage in
/Users/zbeyens/git/slate-v2/packages/slate-react/src/components/dom-coverage-boundary.tsx:106./Users/zbeyens/git/slate-v2/packages/slate-react/src/components/dom-coverage-boundary.tsx:81
and :139.renderElement props expose slots.unstableBoundary in
/Users/zbeyens/git/slate-v2/packages/slate-react/src/components/editable-text-blocks.tsx:320.slots.unstableBoundary supports scope: self | children, mounted,
policies, and placeholder materialization in
/Users/zbeyens/git/slate-v2/packages/slate-react/src/components/editable-text-blocks.tsx:333.renderElement still receives mandatory children and slots in
/Users/zbeyens/git/slate-v2/packages/slate-react/src/components/editable-text-blocks.tsx:419./Users/zbeyens/git/slate-v2/packages/slate-react/src/components/editable-text-blocks.tsx:258./Users/zbeyens/git/slate-v2/packages/slate-react/src/editable/selection-controller.ts:206
and :619.interactiveReadyAt and nativeSurfaceCompleteAt
in
/Users/zbeyens/git/slate-v2/scripts/benchmarks/browser/react/huge-document-legacy-compare.mjs:861
and :919./Users/zbeyens/git/slate-v2/scripts/benchmarks/browser/react/huge-document-legacy-compare.mjs:930.Current proof already exists for:
/Users/zbeyens/git/slate-v2/packages/slate-react/test/dom-coverage-boundary-contract.tsx:70/Users/zbeyens/git/slate-v2/packages/slate-react/test/dom-coverage-boundary-contract.tsx:238/Users/zbeyens/git/slate-v2/packages/slate-react/test/dom-coverage-boundary-contract.tsx:295/Users/zbeyens/git/slate-v2/packages/slate-react/test/dom-coverage-boundary-contract.tsx:409/Users/zbeyens/git/slate-v2/packages/slate-react/test/dom-coverage-boundary-contract.tsx:523/Users/zbeyens/git/slate-v2/packages/slate-react/test/dom-coverage-boundary-contract.tsx:705/Users/zbeyens/git/slate-v2/packages/slate-react/test/dom-coverage-boundary-contract.tsx:785Gap:
Principles:
Top drivers:
Options:
| Option | Verdict | Why |
|---|---|---|
| Keep children mandatory, no hidden support | reject | Leaves the known legacy limitation unsolved. |
| Let renderers omit children freely | reject | Recreates missing DOM crashes. |
| CSS-hide everything | reject | Native find, a11y, copy, stale DOM, and IME become accidental. |
| Treat collapse as void/atom only | partial | Fine for cards, wrong for editable hidden sections. |
| Separate collapse/staging/virtualization systems | reject | Duplicate DOM bridge risk. |
| One DOM coverage primitive with reason-specific policy | choose | Correct substrate and future-proof. |
Consequences:
Current accepted public surface:
renderElement={({ children, element, slots }) => {
if (element.type === 'section') {
return (
<EditableElement>
{React.Children.toArray(children)[0]}
<slots.unstableBoundary
boundaryId="section-body"
mounted={!collapsed}
reason="app-collapse"
scope={{ type: 'children', from: 1 }}
selectionPolicy="materialize"
copyPolicy="include-model"
findPolicy="not-native-until-mounted"
>
Collapsed body
</slots.unstableBoundary>
</EditableElement>
)
}
return <EditableElement>{children}</EditableElement>
}}
Stable target after gates:
<slots.Boundary
boundaryId="section-body"
mounted={!collapsed}
reason="app-collapse"
scope={{ type: "children", from: 1, to: 3 }}
selectionPolicy="materialize"
copyPolicy="include-model"
findPolicy="not-native-until-mounted"
renderPlaceholder={({ materialize }) => (
<button type="button" onClick={materialize}>
Show content
</button>
)}
/>
Whole-element target:
<slots.Boundary
boundaryId="hidden-header"
mounted={!hidden}
reason="app-hidden"
scope={{ type: "self" }}
selectionPolicy="boundary"
copyPolicy="exclude"
findPolicy="not-native-until-mounted"
/>
Hard API cuts:
HiddenRange.HiddenSelf.SelfBoundary private or sugar only; the public concept is one
Boundary with scope.mounted over hidden.Element-spec target after slots stabilize:
schema.define({
type: "header",
domCoverage: {
scope: "self",
mountedWhen: (element) => !element.hidden,
reason: "app-hidden",
selectionPolicy: "boundary",
copyPolicy: "exclude",
},
});
Slots own custom layout. Element specs own stable node-type behavior.
One registry stores coverage records:
type DOMCoverageBoundary = {
boundaryId: string;
ownerRuntimeId: RuntimeId | null;
ownerPath: Path;
coveredPathRanges: readonly PathRange[];
coveredRuntimeRanges: readonly RuntimeIdRange[];
state:
| "mounted"
| "intentionally-hidden"
| "pending-mount"
| "virtualized"
| "atom-boundary";
reason:
| "app-collapse"
| "app-hidden"
| "rendering-staged"
| "viewport-virtualization"
| "shell-aggressive"
| "runtime-atom";
anchor:
| { type: "owner" }
| { type: "summary-slot"; runtimeId: RuntimeId }
| { type: "placeholder"; runtimeId?: RuntimeId };
selectionPolicy: "materialize" | "boundary" | "model-backed";
copyPolicy: "include-model" | "summary-only" | "exclude" | "materialize";
findPolicy: "native" | "not-native-until-mounted" | "custom";
version: number;
};
Runtime rules:
| Reason | Cohort | Selection | Copy/paste | Find | A11y | Default? |
|---|---|---|---|---|---|---|
app-collapse | any doc with user collapse | materialize or boundary by app policy | include model for select-all; local policy explicit | not native until mounted | placeholder announces collapsed | yes as feature |
app-hidden | hidden headers/footers/chrome-like doc nodes | boundary | exclude by default | not native until mounted | hidden/summary policy | yes as feature |
rendering-staged | 2000-10000+ blocks | materialize target | model-backed or materialize for spans | native after surface complete | complete after surface complete | yes for default large-document default |
shell-aggressive | explicit perf escape hatch | shell-specific | model-backed where needed | explicit limitation | explicit limitation | no, opt-in |
viewport-virtualization | stress/pathological only | materialize caret target | model-backed spans | custom or limitation | strategy required | no, research/prototype |
runtime-atom | void/atom nodes | boundary | serialized node policy | native around atom | atom semantics | yes when node spec says atom |
GitHub lesson applied:
Make the repeated unit 50-75% cheaper before relying on virtualization.
GitHub tricks pulled into Slate:
data-* hit routing;client-event-listenersclient-passive-event-listenersrerender-defer-readsrerender-derived-statererender-derived-state-no-effectrerender-move-effect-to-eventrerender-memorerender-use-ref-transient-valuesjs-index-mapsjs-set-map-lookupsjs-combine-iterationsjs-length-check-firstjs-early-exitjs-request-idle-callback with max-latency fallback onlyrendering-content-visibility for staged non-editor chrome onlyrendering-activity for non-editor panels, not editor body coveragecohort-segmentationrepeated-unit-budgetrare-state-isolationevent-delegation-budgeteffect-subscription-budgetcss-layout-hotpathinteraction-inp-matrixmemory-dom-taggingdegradation-contractstaged-readinessreact-19-runtime-proofbrowser-trace-cwv-proofproduction-rum-dashboardeditor-native-behavior-proofActivity: allowed for side panels/inspectors/previews, not editor body DOM
coverage;useEffectEvent: only for effect-fired external subscription callbacks;nativeSurfaceComplete;Slate-close authoring:
renderElement remains the entrypoint.children stays mandatory for normal content.slots.Boundary is the future stable adapter.slots.unstableBoundary remains until browser/native/perf gates pass.scope, mounted, reason, selectionPolicy,
copyPolicy, findPolicy, renderPlaceholder.DX guardrails:
Plate/plugin maintainer answer:
slots.Boundary for custom layout and element specs for
stable node behavior.slate-yjs/collab maintainer answer:
| Behavior | app-collapse | app-hidden | rendering-staged | shell-aggressive | viewport-virtualization |
|---|---|---|---|---|---|
| browser find | not native until mounted | not native until mounted | native after surface complete | explicit limitation | custom or limitation |
| screen reader | summary/placeholder | hidden/summary | complete after surface complete | explicit limitation | strategy required |
| click/caret | boundary or materialize | boundary | materialize target | promote shell | materialize target |
| native selection | boundary/materialize | boundary | materialize/model-backed | model-backed shell | materialize/model-backed |
| select-all | include model by policy | exclude by default | model-backed full doc | model-backed | model-backed |
| copy | include/model-backed by policy | exclude by default | model-backed/materialize | model-backed | model-backed |
| paste | no stale DOM | no stale DOM | model-backed/materialize | model-backed | materialize target or model-backed |
| IME | freeze toggles | freeze toggles | target mounted first | shell promotes first | target mounted first |
| mobile touch | materialize or clamp | clamp | materialize target | explicit proof | materialize target |
| undo/history | explicit ownership | explicit ownership | mount state excluded | mount state excluded | mount state excluded |
| collab | boundary dirties only | boundary dirties only | group dirties only | shell dirties only | window/group dirties only |
Unit/runtime:
React:
children or coverage;Browser:
nativeSurfaceComplete;Performance/stress:
Owner: .tmp/slate-v2 Slate React runtime and benchmark harness.
Work:
:has, broad selectors, scroll
geometry, and overlay movement.Gate:
Phase 1 budget snapshot:
Source: /Users/zbeyens/git/slate-v2/scripts/benchmarks/browser/react/huge-document-legacy-compare.mjs
with REACT_HUGE_COMPARE_MODE=current-only,
REACT_HUGE_COMPARE_READY_ONLY=1, REACT_HUGE_COMPARE_SKIP_BUILD=1,
REACT_HUGE_COMPARE_PROFILE=1, and one iteration. This is iteration evidence,
not release-grade benchmark evidence.
| Cohort | Blocks | Surface | DOM nodes | DOM/block | Editable descendants | Render proxy | Selector subscriptions | Active listeners | Root groups mounted/pending | Coverage boundaries | Heap MB |
|---|---|---|---|---|---|---|---|---|---|---|---|
| normal | 100 | v2Off | 401 | 4.01 | 200 | not sampled | not sampled | 148 | 0/0 | 0 | 29.88 |
| medium | 1000 | v2Off | 4001 | 4.00 | 2000 | text 2000 / leaf 2000 | runtime 2000 / global 7 | 148 | 0/0 | 0 | 146.93 |
| medium | 1000 | v2DefaultRenderOff | 4001 | 4.00 | 2000 | text 2000 / leaf 2000 | runtime 1000 / global 7 | 148 | 0/0 | 0 | sampled separately |
| medium | 1000 | v2AutoExplicit | 203 | 0.20 | 100 | text 100 / leaf 100 / group 1 | runtime 100 / global 7 | 148 | 1/1 | 1 | 119.84 |
| medium | 1000 | v2DefaultRenderAuto | 203 | 0.20 | 100 | text 100 / leaf 100 | runtime 50 / global 7 | 148 | 1/1 | 1 | sampled separately |
| medium | 1000 | v2ShellExplicitRadius0 | 437 | 0.44 | 200 | text 200 / leaf 200 | not sampled | 148 | 0/0 | 0 | 150.76 |
| large | 5000 | v2Off | 20001 | 4.00 | 10000 | not sampled | not sampled | 148 | 0/0 | 0 | 586.98 |
| large | 5000 | v2AutoExplicit | 203 | 0.04 | 100 | not sampled | not sampled | 148 | 1/1 | 1 | 473.61 |
| stress | 10000 | v2AutoExplicit | 203 | 0.02 | 100 | not sampled | not sampled | 148 | 1/1 | 1 | 102.34 |
| stress | 10000 | v2ShellExplicitRadius1 | 1193 | 0.12 | 400 | not sampled | not sampled | 148 | 0/0 | 0 | 203.98 |
Budget read:
renderElement keeps the generic child selector path by design.:has(...) usage in Slate React/DOM
runtime. Layout reads are concentrated in selection/caret repair and
placeholder measurement, not per repeated block. Shell segments already use
contain: layout style paint.SlateText + SlateLeaf wrapper pair can be safely collapsed later.
That needs a DOM mapping proof and should not block Phase 2.Owner: slate-dom and slate-react selection/clipboard/runtime bridge.
Work:
toDOMPoint, toDOMRange, toSlatePoint, and
clipboard bridge.Gate:
Owner: hidden subtree feature lane.
Work:
DOMCoverageBoundaryRange and DOMCoverageSelfBoundary private.slots.unstableBoundary public-unstable only.Gate:
Owner: rendering strategy default strategy.
Work:
auto as staged default.staged force strategy.full benchmark/control strategy.shell explicit aggressive strategy.interactiveReady and nativeSurfaceComplete separate.Gate:
nativeSurfaceComplete measured and bounded.Iteration evidence:
copyPolicy: materialize now requests DOMCoverage.materializeBoundary with
reason copy and the active model range before writing model-backed clipboard
data. This keeps the current copy event synchronous and avoids stale DOM while
still waking the staged group.paste before
applying insert-data, so editing into a pending staged group does not
stay invisible by policy accident./Users/zbeyens/git/slate-v2/packages/slate-react/test/dom-coverage-native-bridge-contract.test.ts
cover copy and paste over a rendering-staged pending boundary and
assert stale DOM is not copied or mutated.bunx biome check packages/slate-dom/src/plugin/dom-clipboard-runtime.ts packages/slate-react/src/editable/clipboard-input-strategy.ts packages/slate-react/test/dom-coverage-native-bridge-contract.test.ts --fix,
focused native bridge Vitest, bunx turbo build --filter=./packages/slate-dom --filter=./packages/slate-react --force,
bunx turbo test --filter=./packages/slate-dom --filter=./packages/slate-react,
and bunx turbo typecheck --filter=./packages/slate-dom --filter=./packages/slate-react.v2DefaultRenderAuto: ready mean 21.21 ms versus legacy chunk-on
302.84 ms; select/type/full-replace/insert-fragment rows beat chunk-on; stale
group count stayed 0; nativeSurfaceComplete mean was 936.59 ms.v2DefaultRenderAuto
honest on startup and background completion: ready mean 36.21 ms,
nativeSurfaceComplete mean 1963.65 ms, pending groups at ready 199, stale
group count 0.v2DefaultRenderAuto-only run beat legacy chunk-on in
most rows but still missed the strict gate on
middleBlockSelectThenTypeMs by 3.18 ms.middleBlockSelectMs was 21.77 ms versus legacy chunk-on 1.17 ms, while
middleBlockTypeAfterSelectMs was 71.60 ms versus legacy 82.00 ms. The
blocker is far selection/materialization cost, not generic typing./Users/zbeyens/git/slate-v2/packages/slate-react/src/components/editable-text-blocks.tsx
made the 10000-row benchmark worse/outlier-heavy and was reverted./Users/zbeyens/git/slate-v2/packages/slate-react/src/components/editable-text-blocks.tsx
from 50 to 25 blocks. Background batch size moved from 8 to 16 groups so
smaller urgent units do not slow native-surface completion just by increasing
group count.v2DefaultRenderAuto:
ready 23.38 ms versus legacy chunk-on 332.60 ms; start type 32.13 versus
67.17; start select+type 31.18 versus 77.77; middle type 23.21 versus 64.17;
middle select+type 41.03 versus 73.18; full replace 12.83 versus 113.66;
insert fragment 9.26 versus 130.89; stale group count 0;
nativeSurfaceComplete 939.43 ms.v2DefaultRenderAuto:
ready 36.20 ms versus legacy chunk-on 593.05 ms; start type 100.37 versus
104.73; start select+type 56.55 versus 74.38; middle type 54.01 versus
65.87; middle select+type 72.66 versus 80.55; full replace 21.22 versus
256.31; insert fragment 23.85 versus 269.83; stale group count 0;
nativeSurfaceComplete 2136.26 ms.middleBlockPromoteThenTypeMs is 77.62 ms
versus legacy chunk-on 65.39 ms. This is not one of the Phase 4 staged
default gates; keep it visible for the later native event/path pass instead
of using it to claim a clean sweep.Owner: Slate React API/DX.
Work:
slots.Boundary;domCoverage;scope: self before accepting public
SelfBoundary sugar.Gate:
Hidden* names;Iteration evidence:
slots.unstableBoundary, from renderElement props in
/Users/zbeyens/git/slate-v2/packages/slate-react/src/components/editable-text-blocks.tsx./Users/zbeyens/git/slate-v2/packages/slate-react/test/dom-coverage-boundary-contract.tsx.scope={{ type: 'self' }} for hidden
header/footer and scope={{ type: 'children', from, to }} for collapsed
section bodies:
/Users/zbeyens/git/slate-v2/docs/libraries/slate-react/editable.md and
/Users/zbeyens/git/slate-v2/site/examples/ts/dom-coverage-boundaries.tsx.slots.Boundary is the leading stable target after remaining gates.slots.SelfBoundary should not ship first; scope: self is clear enough
and keeps the public model unified.HiddenRange and HiddenSelf stay rejected because they describe product
UI instead of DOM coverage.domCoverage is a later convenience for stable node-type
behavior, not the first authoring API for custom child layouts.slots.unstableBoundary until lifecycle, browser/native, stress, and DX gates
are closed.Owner: shell explicit strategy.
Work:
shell-aggressive coverage where bridge/debug needs
it.Gate:
auto.Owner: benchmark/perf lane.
Work:
staged, full, legacy chunk-on/off, and explicit
shell.Gate:
Owner: stress/pathological research and prototype lane.
Work:
viewport-virtualization policy on the same DOM
coverage registry.Gate:
Owner: instrumentation/RUM lane.
Work:
Gate:
Owner: Slate maintainer review.
Work:
slots.Boundary only if Phase 2-9 gates pass.slots.unstableBoundary if stable gates are not met.Gate:
| Change | Objection | Antithesis | Answer | Verdict |
|---|---|---|---|---|
| DOM coverage primitive | This is too much machinery for collapse. | Plate can render CSS-hidden children. | CSS hiding leaves native behavior accidental; DOM coverage centralizes bridge policy. | keep |
Stable slots.Boundary | Public JSX slots may create lifecycle races. | Keep only internal API. | Stabilize only after lifecycle/browser/perf gates; current API remains unstableBoundary. | revise |
scope: self instead of SelfBoundary | Self hiding is a different concept. | Separate component is clearer. | One Boundary concept avoids public API split; sugar can be added if examples prove need. | keep |
| Virtualization in same plan | Virtualization has different semantics. | Separate project avoids pollution. | Same missing-DOM bridge, different policy engine. Separate bridge would be worse. | keep |
| Model-backed copy | Visual and copied payload may diverge. | Always materialize before copy. | Use explicit policy; select-all can include model, local ranges can be product-specific. | keep |
| staged staging | Missing far DOM hurts native find. | Render all DOM always. | Track nativeSurfaceComplete; absent far DOM during warmup is allowed, stale far DOM is not. | keep |
| Shell strategy | Shell is faster, make it default. | Use the fastest path. | Rich text native behavior makes shell an escape hatch, not default. | keep |
| Performance budget pass | This delays architecture work. | Just finish coverage. | GitHub's diff work shows repeated-unit bloat can dominate; skip this and staging hides bad units. | keep |
| Dimension | Score | Evidence |
|---|---|---|
| React 19.2 runtime performance | 0.94 | Performance phase, React primitive limits, benchmark trace owners |
| Slate-close unopinionated DX | 0.93 | renderElement + slots.unstableBoundary, Plate owns product UI |
| Plate and slate-yjs migration backbone | 0.91 | runtime-vs-document state split, plugin/collab answers |
| Regression-proof testing strategy | 0.95 | unit/react/browser/perf/native matrix and current test owners |
| Research evidence completeness | 0.92 | prior Lexical/ProseMirror evidence plus GitHub performance article |
| shadcn-style composability/minimalism | 0.91 | one Boundary adapter, minimal props, no product API in core |
Weighted total: 0.93.
Ready for execution review: yes.
| Pass | Status | Evidence added | Plan delta | Next owner |
|---|---|---|---|---|
| current-state read | complete | live source/test/benchmark pointers | unstable adapter treated as current, not hypothetical | done |
| intent/boundary | complete | explicit scope and non-goals | no vague future bucket | done |
| decision brief | complete | options/rejections/consequences | one primitive, many policies | done |
| performance pass | complete | performance rule files and GitHub tricks | hot-surface audit and RUM phases added | done |
| native behavior pass | complete | per-strategy matrix | virtualization proof owned | done |
| migration pass | complete | Plate/slate-yjs backbone answers | mount state excluded from history/collab | done |
| high-risk pass | complete | public API, browser, IME/mobile, virtualization gates | stable API gated, not assumed | done |
| closure | complete | score >= 0.92 and no dimension below 0.85 | ready for ralph execution | user review |
| Time | Phase | Status | Evidence | Next |
|---|---|---|---|---|
| 2026-05-03 | Phase 1: Hot Surface Audit And Budget Cuts | in_progress | ralph activated the plan; active goal state set to pending; active goal state refreshed; first slice scoped to benchmark/runtime instrumentation for missing hot-surface tags. | Add/verify surface-weight tags, then close or continue Phase 1 budget cuts. |
| 2026-05-03 | Phase 1 activation cleanup | stopped | User interrupted the benchmark run on purpose while asking an explanatory question; orphaned benchmark processes were stopped. Existing live source already contains the first-slice instrumentation tags: event listener stats, DOM coverage boundary count, root group counts, and process heap tags in /Users/zbeyens/git/slate-v2/scripts/benchmarks/browser/react/huge-document-legacy-compare.mjs:419 and :1057. | No active execution owner. Reopen Phase 1 with ralph when execution should resume. |
| 2026-05-03 | Phase 1 hot-surface audit | complete | Added JSDOM event-listener counting and ready surface-weight tags for active/added listeners, selected listener types, DOM coverage boundary count, mounted/pending root group counts, and process heap in /Users/zbeyens/git/slate-v2/scripts/benchmarks/browser/react/huge-document-legacy-compare.mjs. Verification: node --check, targeted Biome check, and generated current-runner profile emitted the new keys. Full compare wrapper was SIGTERM'd locally, so this is iteration proof, not release-grade benchmark evidence. | Continue Phase 1 budget cuts: produce the repeated-unit budget table, then cut measured wrapper/listener/effect/state overhead in the default staged path. |
| 2026-05-03 | Phase 1 budget table | in_progress | Added current-only, ready-only, skip-build, and surface-filter benchmark strategies; recorded normal/medium/large/stress ready-surface budget snapshot; added selector subscription profiler tags in /Users/zbeyens/git/slate-v2/packages/slate-react/src/hooks/use-editor-selector.tsx. Verification: benchmark syntax, targeted Biome, bunx turbo build --filter=./packages/slate-react --force, bunx turbo typecheck --filter=./packages/slate-react, bunx turbo test --filter=./packages/slate-react, and current-only ready profiler rows. | Continue runtime cuts against default render component/proxy and selector subscription pressure. |
| 2026-05-03 | Phase 1 default direct text-child cut | complete | Default no-custom element rendering now renders direct text children from the parent selector when no projection store or custom text/leaf/segment renderer is active. Custom renderers and projection-backed text stay on the generic child selector path. Benchmark proof at 1000 blocks: v2DefaultRenderOff runtime selector subscriptions 1000 vs v2Off 2000; v2DefaultRenderAuto 50 vs v2AutoExplicit 100. Verification: targeted Biome, node --check, bunx turbo build --filter=./packages/slate-react --force, bunx turbo typecheck --filter=./packages/slate-react, bunx turbo test --filter=./packages/slate-react, and current-only ready benchmark rows. | Continue Phase 1 with remaining default render proxy/DOM shape and CSS/layout hot-path review. |
| 2026-05-03 | Phase 1 closure review | complete | CSS/layout scan found no :has(...) runtime selectors; repeated DOM listeners are flat; selector pressure is measured and reduced for default no-custom rendering; rare state remains in projection/annotation/widget stores instead of block props; staged ready surface keeps mounted descendants and subscriptions bounded. | Move to Phase 2 DOM Coverage Bridge Closure. Keep the SlateText/SlateLeaf DOM-shape collapse as a later measured risk, not a Phase 1 blocker. |
| 2026-05-03 | Phase 2 DOM coverage bridge closure | complete | Fixed editor-owned unmapped DOM target probing in /Users/zbeyens/git/slate-v2/packages/slate-dom/src/plugin/dom-editor.ts so iframe-shaped paragraph targets return non-void instead of throwing; added native bridge tests in /Users/zbeyens/git/slate-v2/packages/slate-react/test/dom-coverage-native-bridge-contract.test.ts proving copy, paste, and drag-start over hidden boundaries use model-backed data and avoid stale DOM; guarded DOMCoverage.materializeBoundary during active composition in /Users/zbeyens/git/slate-v2/packages/slate-dom/src/plugin/dom-coverage.ts; extended boundary tests with first/last self-boundary DOM point import smoke in /Users/zbeyens/git/slate-v2/packages/slate-react/test/dom-coverage-boundary-contract.tsx. Verification: targeted Biome, bun test ./packages/slate-dom/test/bridge.ts ./packages/slate-dom/test/dom-coverage.ts --bail=1, bun test ./packages/slate-react/test/dom-coverage-boundary-contract.tsx --bail=1, focused native bridge vitest, bunx turbo build --filter=./packages/slate-dom --filter=./packages/slate-react --force, bunx turbo typecheck --filter=./packages/slate-dom --filter=./packages/slate-react, and bunx turbo test --filter=./packages/slate-react. | Move to Phase 3 Hidden/Collapsed Runtime Release Gate: keep private harness private, keep slots unstable, add comprehensive example/docs/debug proof, then browser/a11y/stress rows. |
| 2026-05-03 | Phase 3 hidden/collapsed runtime release gate | complete | Converted /Users/zbeyens/git/slate-v2/site/examples/ts/dom-coverage-boundaries.tsx from direct private boundary component imports to slots.unstableBoundary, added the deep-section-body depth-3 boundary, kept header/footer self boundaries and hidden model update/copy/debug controls, and documented slots.unstableBoundary plus native find/a11y/copy policy behavior in /Users/zbeyens/git/slate-v2/docs/libraries/slate-react/editable.md. Verification: targeted Biome, site typecheck, slate-react typecheck, managed browser proof at /examples/dom-coverage-boundaries with Outer then Nested toggles showing deep-section-body and Deep body collapsed while Deep hidden body stayed absent. | Move to Phase 4 DOM-Present Large-Doc Default Closure: selection/copy/paste over staged coverage, stale DOM prevention, readiness, and benchmark matrix. |
| 2026-05-03 | Phase 4 staged clipboard bridge | complete | Added materialization requests for copyPolicy: materialize in /Users/zbeyens/git/slate-v2/packages/slate-dom/src/plugin/dom-clipboard-runtime.ts and paste target materialization in /Users/zbeyens/git/slate-v2/packages/slate-react/src/editable/clipboard-input-strategy.ts. Added regression rows in /Users/zbeyens/git/slate-v2/packages/slate-react/test/dom-coverage-native-bridge-contract.test.ts proving copy and paste over a rendering-staged pending boundary request materialization, use model data, and avoid stale DOM. Verification: targeted Biome, focused native bridge Vitest, slate-dom+slate-react build, slate-react package tests, and slate-dom+slate-react typecheck. | Continue Phase 4 with stale-DOM/readiness proof and 5000/10000 benchmark matrix. |
| 2026-05-03 | Phase 4 benchmark split | complete | 5000-block full compare was green for default auto against legacy chunk-on, and 10000-block readiness/full-replace/stale-DOM rows were green. Strict 10000 interaction closure first missed middleBlockSelectThenTypeMs by 3.18 ms, and split-selection proof showed the real owner was far pending root-group selection/materialization (middleBlockSelectMs 21.77 ms versus legacy chunk-on 1.17 ms) while typing after selection was faster than legacy. A speculative active-group persistence cut was reverted after worse/outlier-heavy results. | Reduce urgent root-group materialization cost without weakening staged correctness. |
| 2026-05-03 | Phase 4 staged closure | complete | Reduced staged root group size from 50 to 25 and increased background batch size from 8 to 16. Fixed the direct text-child fast path so full-document replace refreshes visible first-group text even when runtime ids are reused. Verification: targeted Biome; staged Bun contract rows (7 pass); slate-react package Vitest (20 files / 146 tests); slate-react typecheck; slate-react build; 5000 and 10000 block, 5-iteration legacy-compare matrices. Final 5000 and 10000 default auto rows beat legacy chunk-on for ready, select-all, start/middle type, start/middle select+type, full replace, and insert-fragment; stale group count stayed 0; native completion was measured at 939.43 ms and 2136.26 ms. Residual 10000 middleBlockPromoteThenTypeMs remains recorded for the later event/path pass. | Move to Phase 5 API bake-off without stabilizing slots.Boundary yet. |
| 2026-05-03 | Phase 5 boundary API bake-off | complete | Re-read live source, tests, docs, and example for slots.unstableBoundary, private boundary components, and DOM coverage docs. The current unified slot adapter already covers child ranges and self coverage without raw runtime ids. Verdict: keep slots.unstableBoundary public-unstable; keep slots.Boundary as the eventual stable target; reject public HiddenRange / HiddenSelf; do not ship public SelfBoundary before proving scope: self is insufficient; keep element-spec domCoverage and low-level registration as later/internal shapes. | Move to Phase 6 shell policy integration. |
| 2026-05-03 | Phase 6 shell policy integration | complete | Shell segments now register shell-aggressive DOM coverage boundaries in /Users/zbeyens/git/slate-v2/packages/slate-react/src/rendering-strategy/segment-shell.tsx, using state: virtualized, selectionPolicy: model-backed, copyPolicy: include-model, and findPolicy: not-native-until-mounted. Shell placeholder DOM carries DOM coverage attributes for bridge/debug import. Regression rows in /Users/zbeyens/git/slate-v2/packages/slate-react/test/rendering-strategy-and-scroll.tsx verify shell boundary ids, path/runtime coverage, policy, DOM attributes, cleanup after promotion, and neighboring shell boundary retention. Verification: targeted Biome; shell-focused direct Bun test (11 pass); slate-react typecheck; slate-react package Vitest (20 files / 146 tests); slate-react build with the known is-hotkey external warning. | Move to Phase 7 release-grade benchmark closure; keep shell evidence separated from default auto claims. |
| 2026-05-03 | Phase 7 release-grade benchmark closure | complete | Added isolated current-surface benchmark strategy in /Users/zbeyens/git/slate-v2/scripts/benchmarks/browser/react/huge-document-legacy-compare.mjs after all-in-one surface runs showed process contamination. Moved staged background mounting into React.startTransition, reduced root group size from 25 to 16, and moved first background mount from 250 ms to 500 ms in /Users/zbeyens/git/slate-v2/packages/slate-react/src/components/editable-text-blocks.tsx, preserving urgent typing before native completion. Final isolated artifacts: /Users/zbeyens/git/slate-v2/tmp/phase7-final-isolated-5000.json and /Users/zbeyens/git/slate-v2/tmp/phase7-final-isolated-10000.json. At 10000 blocks, v2DefaultRenderAuto mean rows were: ready 34.09 ms, select-all 0.25 ms, start type 68.37 ms, start select+type 51.05 ms, middle type 53.93 ms, middle select+type 74.54 ms, middle promote+type 68.22 ms, full replace 19.51 ms, insert fragment 12.83 ms, native completion 2573.97 ms, stale groups 0. Residual default miss: middle promote+type is +2.42 ms versus legacy chunk-on mean. Shell rows remain explicit and slower for middle select/promote, so they cannot support default claims. Profile artifact /Users/zbeyens/git/slate-v2/tmp/phase7-profile-10000.json records ready surface tags: default auto 67 DOM nodes, 32 editable descendants, 1 coverage boundary, 148 active listeners, 88.46 MB heap; shell radius 0 797 DOM nodes, 200 editable descendants, 99 boundaries, 148 listeners, 148.51 MB heap. Verification: benchmark syntax; targeted Biome; staged direct Bun rows (7 pass); slate-react typecheck; slate-react package Vitest (20 files / 146 tests); slate-react build with known is-hotkey external warning; 5000/10000 isolated 5-iteration matrices. | Move to Phase 8 viewport virtualization prototype. |
| 2026-05-03 | Phase 8 viewport virtualization prototype | complete-experimental | Added explicit renderingStrategy={{ type: 'virtualized' }} support in /Users/zbeyens/git/slate-v2/packages/slate-react/src/rendering-strategy/create-segment-plan.ts, /Users/zbeyens/git/slate-v2/packages/slate-react/src/components/editable-text-blocks.tsx, and /Users/zbeyens/git/slate-v2/packages/slate-react/src/rendering-strategy/segment-shell.tsx. Far segments now register viewport-virtualization DOM coverage boundaries with state: virtualized, selectionPolicy: materialize, copyPolicy: include-model, and findPolicy: not-native-until-mounted; broad selections stay model-backed through the existing shell-backed selection lane. Docs in /Users/zbeyens/git/slate-v2/docs/libraries/slate-react/editable.md state browser find and screen-reader limits. Stress artifacts: /Users/zbeyens/git/slate-v2/tmp/phase8-virtualized-25000.json, /Users/zbeyens/git/slate-v2/tmp/phase8-virtualized-50000.json, and /Users/zbeyens/git/slate-v2/tmp/phase8-virtualized-profile-50000.json. At 25000 blocks, virtualized ready mean was 137.03 ms, select-all 0.27 ms, middle promote+type 360.48 ms, full replace 40.46 ms, stale groups 0. At 50000 blocks, ready mean was 249.39 ms, select-all 0.28 ms, middle promote+type 908.59 ms, full replace 155.73 ms, stale groups 0. The 50000 profile row records 2397 DOM nodes, 200 editable descendants, 499 coverage boundaries, 148 active listeners, and 357.35 MB mean heap. Verdict: prototype works as a stress lane, but 50k edit interactions are too slow for stable/default claims. Verification: targeted Biome; virtualized/shell direct Bun rows; slate-react typecheck; slate-react package Vitest (20 files / 146 tests); slate-react build with known is-hotkey external warning; 25000/50000 current-only stress rows. | Move to Phase 9 production observability; keep virtualization experimental. |
| 2026-05-03 | Phase 9 production observability | complete | Added onRenderingStrategyMetrics to <Editable> in /Users/zbeyens/git/slate-v2/packages/slate-react/src/components/editable.tsx and /Users/zbeyens/git/slate-v2/packages/slate-react/src/components/editable-text-blocks.tsx, exporting EditableRenderingStrategyMetrics from /Users/zbeyens/git/slate-v2/packages/slate-react/src/index.ts. The callback runs after commit and reports cohort, document size, requested/effective strategy, mounted/pending groups and top-level counts, shell count, native surface completeness, DOM coverage boundary counts by reason, DOM boundary element count, visible DOM node count, and editable descendant count. Tests in /Users/zbeyens/git/slate-v2/packages/slate-react/test/rendering-strategy-and-scroll.tsx cover virtualized and staged metrics. Docs now show the Datadog/RUM action shape and required dashboard tags: interaction, cohort, document size, strategy, boundary count, visible DOM count, editable descendant count, custom renderer flag, browser, mobile/desktop, IME, and release. Verification: bunx biome check ... --fix; focused metrics/virtualized Bun rows (4 pass); bunx turbo typecheck --filter=./packages/slate-react; bunx turbo test --filter=./packages/slate-react (20 files / 146 tests); bunx turbo build --filter=./packages/slate-react --force with known is-hotkey external warning; bun lint:fix. | Move to Phase 10 stable release decision. |
| 2026-05-03 | Phase 10 stable release decision | complete | Maintainer decision: keep default renderingStrategy as auto staged rendering; keep slots.unstableBoundary documented but unstable; do not stabilize slots.Boundary; keep renderingStrategy.type = "virtualized" experimental because 50000-block edit lanes are still hundreds of milliseconds to about one second; keep shell as explicit aggressive strategy only. This satisfies the full-plan requirement without lying: the primitive is implemented and measured, the production observability hook exists, and the unstable/experimental surfaces stay labeled honestly. Final fast gate in /Users/zbeyens/git/slate-v2: bun check passed lint, all package/site/root typechecks, Bun tests (1007 pass, 95 skip), and slate-react Vitest (20 files / 146 tests). | Full execution complete. |
| 2026-05-03 | Rendering strategy API rename | complete | Renamed the public large-document rendering controls to renderingStrategy, with string values auto, staged, full, shell, and virtualized, object option key type, metrics fields requestedStrategy and effectiveStrategy, and export EditableRenderingStrategyMetrics. Renamed live source/test/example files from large-document to rendering-strategy, updated browser contract route families, docs, walkthroughs, active plan state, continuation state, and the RUM solution note. Kept the unrelated editable-island void kind intact. Verification: targeted Biome; node --check for the legacy compare benchmark; focused rendering-strategy-and-scroll and provider hook Bun tests; bunx turbo typecheck --filter=./packages/slate-react; bunx turbo test --filter=./packages/slate-react; bunx turbo build --filter=./packages/slate-react --force with the known is-hotkey external warning; bunx turbo typecheck --filter=./packages/slate-browser; bun typecheck:root; bun typecheck:site; bun lint:fix; old API reference sweep; focused Playwright route row (rendering-strategy-runtime, Chromium) passed. | Full execution complete with renamed API. |
The plan is ready only because it does not claim implementation completion. Execution starts with Phase 1.
Before stable release:
bun check green in /Users/zbeyens/git/slate-v2;