docs/plans/2026-05-22-slate-v2-pretext-layout-rendering-architecture-ralplan.md
status: complete created: 2026-05-22 completion_id: 019e46be-4ec4-7d11-bc6e-9fcf033a8803 current_pass: layout-rendering-architecture-closure current_pass_status: complete next_pass: moved-to-root-location-authority-cleanup-plan target_score: 0.94 score: 0.91
The root-location authority cleanup has moved to
docs/plans/2026-05-22-slate-v2-root-location-authority-cleanup-ralplan.md.
This Pretext/layout plan remains the owner for layout, DOM strategy, virtualization, and pagination architecture. The later root-location appendix is kept as historical context only and is superseded by the standalone root-location plan.
Hard take: current Slate v2 can support Pretext-backed layout, but not in the absolute-best shape yet.
The substrate is right:
slate has runtime ids, rooted state, state fields, commits, and snapshots.slate-react has DOM coverage, shell-backed selection policy, staged
rendering, metrics, and experimental virtualization.slate-layout has page settings, page geometry, projections, hit rects,
run-scoped decoration rects, and a React PagedEditable.But the architecture still needs a boundary rewrite before beta if Pretext is supposed to become the future performance/layout engine instead of a pagination demo dependency.
The current bad shape:
slate-layout is page-first, not a generic continuous layout service.block.textStyle,
then measures placed runs afterward. That fixes visual spacing, but line
breaking is not truly rich-inline/run-owned yet.renderingStrategy is too broad a public name. It sounds like layout,
rendering, pagination, shelling, and virtualization live in one prop.virtualized is top-level-index/estimated-height driven, not layout driven.Final call: rewrite the boundary now. Do not rewrite Slate core. Do rewrite
slate-layout and slate-react render/mount APIs around a first-class derived
layout service.
Intent: make Pretext a clean long-term layout/performance engine for Slate v2,
including non-paginated editors, without dirty hacks to fit slate-react.
Desired outcome: users can opt into layout-aware Slate once and get continuous layout, page layout, line maps, block heights, hit rects, scroll anchoring, virtualization inputs, and overlays from the same derived layout service.
In scope:
slate-layout generic continuous and paged layout APIs.slate-react public render/mount strategy naming.Non-goals:
slate core.Decision boundaries:
slate-react may depend on a layout interface, not on Pretext directly.slate-layout as the default layout engine. Do not
expose public engine selection until a real second engine exists.renderingStrategy is hard-cut to domStrategy before beta because the old
name prevents the right mental model.Principles:
Top drivers:
renderingStrategy is creating the wrong abstraction boundary.Viable options:
slate-layout plus renderingStrategy.
slate-react.
slate-layout a generic derived layout service, fold Pretext into it
as the built-in engine, and rename/split renderingStrategy into DOM
materialization policy.
Consequence: slate-react gets a stronger integration point with layout, but
still works without layout for small/simple editors.
Layering:
| Layer | Owner |
|---|---|
slate | document value, roots, state fields, operations, transactions, history, collab |
slate-layout | derived layout store, continuous layout, paged layout, built-in Pretext text measurement, block heights, line/box geometry, hit rects, range projection |
slate-dom | DOM coverage, materialization, selection/copy/find policies for missing DOM |
slate-react | native editable DOM, event bridge, selection import/export, render/mount strategy, layout consumption |
| Plate/apps | Markdown, tables, comments, diagnostics, product UI, export/import |
Public target:
const layout = useSlateLayout(editor, {
typography,
})
<Editable layout={layout} domStrategy="auto" />
Paged target:
const layout = useSlateLayout(editor, {
page: { margins: 96, preset: 'a4' },
typography,
})
<PagedEditable
layout={layout}
domStrategy="staged"
pageView={{ gap: 24, mode: 'facing' }}
/>
Advanced persisted/collaborative page settings:
const pageSettings = defineStateField<PageSettings>({
key: 'layout.pageSettings',
collab: 'shared',
history: 'push',
initial: () => ({ margins: 96, preset: 'a4' }),
persist: true,
})
const layout = useSlateLayout(editor, {
page: pageSettings,
typography,
})
Naming target:
layout: what geometry exists.page: document-owned page settings. Presence selects paged layout; absence
means continuous layout.root: defaults to the current editor/view root. Pass it only for multi-root
editors.typography: deterministic measurement config/resolver for Pretext, not
product styling. Omit it only when the runtime can safely derive typography
from the mounted editable.pageView: viewport/display policy such as facing pages and page gap. It is
not part of the derived layout snapshot.domStrategy: what DOM is mounted.PagedEditable: page viewport renderer over a layout.Editable: native editing surface that can consume layout.renderingStrategy is hard-cut to domStrategy before beta. mountStrategy is
rejected because it sounds like a React implementation detail instead of a
native DOM availability contract.
slate-layoutCurrent source:
.tmp/slate-v2/packages/slate-layout/src/index.ts exposes
SlatePageLayout* types and createSlatePageLayout..tmp/slate-v2/packages/slate-layout/src/react.tsx exposes
useSlatePageLayout, useSlatePageLayoutSnapshot, and PagedEditable.Target:
SlateLayoutSnapshot.Current source:
.tmp/slate-v2/packages/slate-layout-pretext/src/index.ts calls
layoutWithLines(prepared, input.page.content.width, block.lineHeight) using
block.textStyle, then creates run widths afterward.Problem:
Target:
prepareRichInline, layoutNextRichInlineLineRange, and
walkRichInlineLineRanges.break: 'never' and caller-owned extraWidth.whiteSpace: 'pre-wrap' remains the editable default.slate-layout as the built-in Pretext engine. Keep any
engine interface internal until a second production engine exists.renderingStrategyCurrent source:
.tmp/slate-v2/packages/slate-react/src/rendering-strategy/create-segment-plan.ts
defines RenderingStrategyOptions with auto, full, staged, shell,
and object-only virtualized..tmp/slate-v2/packages/slate-react/src/components/editable-text-blocks.tsx
normalizes that into staged root groups, shell segments, or virtualized plan.Problem:
Target:
type EditableDOMStrategy =
| 'auto'
| 'full'
| 'staged'
| {
type: 'virtualized'
overscan?: number
}
Rules:
auto: full DOM for normal docs, staged DOM-present for large docs, no
virtualized mode unless explicitly requested.staged: DOM-present materialization with pending DOM coverage boundaries.virtualized: viewport DOM only, layout-driven sizes, materialize-first
native behavior.EditableDOMStrategy. Preview shells
are internal or app-owned surfaces, not a raw Slate editable rendering mode.domStrategy.DOM coverage is the bridge contract between document truth, layout truth, and mounted DOM. It is not an island API and not a public rendering strategy.
Target:
mounted: native DOM exists for the range.pending: Slate knows the range and can materialize before selection, copy,
paste, browser-find assist, or scroll-to-selection.virtualized: the range is intentionally outside the mounted viewport and
must materialize before native behavior is claimed.structural: layout/debug/table/page-frame DOM exists, but it is not an
editable descendant and must be excluded from Slate node resolution.Rules:
slate-dom owns the coverage registry and missing-DOM policy.slate-react publishes coverage from the mount plan.slate-layout provides rects and hit targets; it does not decide native DOM
availability.Current source:
.tmp/slate-v2/packages/slate-react/src/rendering-strategy/use-virtualized-root-plan.ts
uses estimatedBlockSize and top-level runtime indexes.Problem:
previewChars still exists on virtualized options even though viewport
virtualization does not render previews.Target:
layout.virtualItems or block/fragment rects.EditableDOMStrategy.Current source:
createSegmentPlan groups top-level runtime ids by fixed segment size.Target:
SlateMountPlan is derived from runtime state + layout state:
| System | Source | Mechanism | Avoids | Steal | Reject | Slate target | Verdict |
|---|---|---|---|---|---|---|---|
| Pretext | ../pretext/README.md, ../pretext/src/rich-inline.ts | prepare/cache text, then run cheap line layout and rich-inline ranges | DOM reflow measurement and guessed heights | rich-inline line fitting and prepared cache | editor semantics in layout engine | built-in slate-layout engine behind an internal boundary | agree |
| Premirror | ../premirror/docs/design-proposal.md | document truth -> snapshot -> measure -> compose -> render/mapping | page nodes and duplicated editing truth | composer/mapping split | ProseMirror position model | Slate path/root/run/box mapping | agree |
| TanStack Virtual | docs/research/sources/editor-architecture/tanstack-virtual-and-github-large-surface-virtualization.md | headless viewport range engine | too many DOM nodes in tail cohorts | range extraction and runtime-id keys | owning editor semantics | adapter under layout-aware domStrategy | partial |
| Current Slate v2 | live .tmp/slate-v2 sources listed above | runtime ids, DOM coverage, staged/shell/virtualized modes, page layout package | child-count chunking | substrate and metrics | vague renderingStrategy boundary | generic layout + DOM strategy split | revise |
Cohorts:
| Cohort | Size | Default |
|---|---|---|
| normal | <1000 top-level blocks | full or plain native DOM |
| medium | 1000-5000 | staged DOM-present with layout-aware offscreen estimates |
| large | 5000-10000 | staged DOM-present plus active corridor and occlusion |
| stress | 10000-50000 | explicit virtualized only |
| pathological | >50000 | explicit app-owned preview/collapse surface with RUM/degradation tags, outside EditableDOMStrategy |
Budgets:
<16ms normal/medium, <50ms large, <120ms stress;<500ms;Degradation contract:
full and completed staged: native find/selection/copy/paste/a11y.staged: materialize-first for far selection/copy/find.virtualized: viewport native, far content materialize-first.domStrategy.Unit:
slate-layout: continuous layout snapshot, page layout snapshot, root-bound
projection, hit rects, table/box geometry, line maps, built-in Pretext
rich-inline line breaks, trailing spaces, hard breaks, mixed fonts, inline
atoms, letter spacing, cache invalidation.slate-react: layout + domStrategy normalization, mount plan, DOM
coverage boundary reasons.Browser:
site/examples/ts/virtualization.tsx route
using layout + explicit domStrategy={{ type: 'virtualized' }}; far scroll
materializes target, typing works after materialization, visible range
select/copy works, IME is guarded from missing DOM, and browser find
limitations are tested instead of hidden.Fixture matrix:
RUM:
onRenderingStrategyMetrics to onDOMStrategyMetrics, and rename
payload fields to DOM strategy language.| Change | Objection | Answer | Verdict |
|---|---|---|---|
Rename/split renderingStrategy | Public churn right before beta. | The current name is already misleading. Better to break now than teach pagination/virtualization through the wrong slot. | keep |
Add generic slate-layout | Raw Slate is becoming a document layout engine. | Raw Slate remains Pretext-free; layout is optional derived view data. Apps can ignore it. | keep |
Fold Pretext into slate-layout instead of shipping slate-layout-pretext | Adds an opinionated dependency to layout. | Pretext is the only planned production engine, so a public engine package is fake configurability. slate, basic slate-react, and no-layout Editable stay clean; slate-layout keeps an internal engine boundary for tests/workers/future engines. | keep |
| Keep active editing DOM-native | Pretext/canvas could be the future. | True, but not until IME/selection/copy/find/a11y/mobile proof exists. Native active corridor is the sane bridge. | keep |
| Virtualization as explicit stress mode | Perf users want it automatic. | Silent degraded native behavior is worse than slower startup. Auto can choose staged, not virtualized, until proof closes. | keep |
Cut public shell / preview-shell | Existing examples and tests may already teach shell behavior. | That is exactly why to cut it before beta. Shell-style previews are app-owned or internal materialization UI, not a second public DOM strategy beside virtualization. | keep |
| Put page settings in state fields | Layout settings look product-specific. | Persisted page setup is document-owned metadata. View-only controls stay local or collab: 'local', history: 'skip', persist: false. | keep |
Add layout prop to Editable | This could make slate-react depend on layout package semantics. | Editable accepts a tiny structural layout protocol. Pretext and page composition stay in slate-layout packages. | revise: protocol-only |
Source ledgers read:
docs/slate-issues/gitcrawl-live-open-ledger.mddocs/slate-issues/gitcrawl-v2-sync-ledger.mddocs/slate-issues/open-issues-ledger.mddocs/slate-issues/benchmark-candidate-map.mddocs/slate-v2/ledgers/issue-coverage-matrix.mddocs/slate-v2/ledgers/fork-issue-dossier.mddocs/slate-v2/references/pr-description.mdThis pass adds no Fixes #... and no new Improves #... claim. The plan is
architecture pressure only until a later ralph implementation adds package,
browser, and benchmark proof.
| Issue | Current ledger state | Relation to this plan | Decision |
|---|---|---|---|
#790 dynamic rendering | cluster-synced / proof-route backlog | Core pressure for large-document mounting, explicit virtualization, and startup latency. | Related only. Layout-driven domStrategy can become proof, but not before large-doc mount/edit/scroll benchmarks pass. |
#4141 nested block rerendering | Improves already claimed by rerender breadth benchmark | Existing selector/runtime proof stays relevant; this plan must not regress it while adding layout subscriptions. | Preserve existing Improves; no promotion. Add layout invalidation breadth proof before closure. |
#5944 stable per-line pagination | issue-reviewed / needs repro | Directly overlaps the paged layout target and Premirror-style page composition. | Related only. Stable pagination needs current repro-shaped browser rows around line/page boundary flicker. |
#5924 structural DOM exclusion | Not claimed in coverage matrix; stale/triage-closed in sync ledger | Structural page frames, tables, overlays, and debug boxes need cursor-safe DOM boundaries. | Keep not claimed. The better answer is DOM coverage + mount-plan policy, not a public ignore-cursor escape hatch yet. |
#3892 custom editor surface and layout engine | cluster-synced / policy non-claim | Strong ecosystem pressure for a clean custom surface/layout substrate. | Keep not fixed. Generic slate-layout answers the substrate, not an app/product editor surface. |
#2572 accessibility | triage-closed / policy non-claim | A11y is a hard release guard for virtualized and app-owned preview missing-DOM behavior. | Keep not fixed. Closure requires explicit a11y/browser proof and degraded-mode docs. |
#5131, #2051 render/subscription breadth | not claimed or existing macro rows | Layout snapshots and mount plans can accidentally widen subscriptions. | Related guardrails. Add tests proving layout changes update only affected blocks/pages. |
Ledger accounting result for this pass:
0.0.issue-ledger-accounting
pass. This review amendment adds no new issue claim and needs no new ledger
row.Existing synced artifacts for this lane:
docs/slate-issues/gitcrawl-v2-sync-ledger.mddocs/slate-v2/ledgers/issue-coverage-matrix.mddocs/slate-v2/ledgers/fork-issue-dossier.mddocs/slate-v2/references/pr-description.mdThe preview-shell/DOM coverage amendment changes API boundaries only. It
does not add a fixed, improved, or newly related issue claim, so the existing
ledger sections remain the source of truth without another row.
Claim result:
0.0.Improves, Related, Not claimed, issue-reviewed, and
proof-route backlog statuses preserved.Accounting decision:
#790: related proof-route backlog for layout-driven virtualization.#4141: existing Improves preserved; layout work must not regress render
breadth.#5944: related issue-reviewed pagination pressure.#5924: not claimed; no public ignore-cursor API.#3892: policy non-claim; generic layout substrate only.#2572: policy non-claim; a11y is a release gate.#5131, #2051: unchanged subscription/performance guardrails.Live source pressure:
.tmp/slate-v2/packages/slate-layout/src/index.ts already accepts
settings?: EditorStateField<TSettings> and refreshes on
dirtyStateKeys, so page presets and margins already fit the state-field
direction..tmp/slate-v2/packages/slate-layout/src/react.tsx still exposes only
useSlatePageLayout, useSlatePageLayoutSnapshot, and PagedEditable..tmp/slate-v2/packages/slate-react/src/components/editable.tsx and
editable-text-blocks.tsx still expose renderingStrategy and
onRenderingStrategyMetrics..tmp/slate-v2/packages/slate-react/src/rendering-strategy/use-virtualized-root-plan.ts
still virtualizes top-level runtime ids with an estimated block size..tmp/slate-v2/packages/slate-react/src/editable/runtime-root-engine.ts and
keyboard-input-strategy.ts still treat shell and virtualized modes through
the same shell-backed selection policy.Final API decision:
domStrategy, not mountStrategy.SlateMountPlan.renderingStrategy before beta. A temporary internal alias may
exist only inside the implementation branch and must not appear in public
examples, docs, or exported reference surfaces.onDOMStrategyMetrics and metric fields from
renderingStrategy* to domStrategy*.Why domStrategy wins:
mountStrategy sounds like a React implementation detail and hides the native
behavior contract.renderingStrategy is too vague and keeps inviting layout, pagination,
virtualization, and shell preview policy into one prop.Target public surface:
const layout = useSlateLayout(editor, {
typography,
})
<Editable domStrategy="auto" layout={layout} />
const pageSettings = defineStateField({
key: 'layout.pageSettings',
collab: 'shared',
history: 'push',
initial: () => ({ margins: 96, preset: 'a4' }),
persist: true,
})
const layout = useSlateLayout(editor, {
page: pageSettings,
typography,
})
<PagedEditable
domStrategy="staged"
layout={layout}
pageView={{ gap: 24, mode: 'facing' }}
/>
Inline settings for local-only examples:
const layout = useSlateLayout(editor, {
page: { margins: 96, preset: 'a4' },
typography,
})
Rules:
mode: 'continuous': continuous layout is the default when page is
absent.mode: 'paged': page is the semantic switch because it carries the
document-owned page settings.root: 'main' in the happy path: layout uses the current editor/view root.
Explicit roots are multi-root escape hatches.pageView owns facing/spread display and page gaps; those are viewport
policy, not document layout truth.Layout protocol:
slate-react should depend on a small structural EditableLayout protocol,
not on Pretext.slate-layout implements that protocol for continuous and paged snapshots.slate-layout owns the built-in Pretext engine. Engine selection is not
public until there is a real second production engine.Editable without layout stays DOM-native and Pretext-free.PagedEditable remains in slate-layout/react as a composed viewport over
Editable, not a separate editing runtime.DOM strategy target:
type EditableDOMStrategy =
| 'auto'
| 'full'
| 'staged'
| {
overscan?: number
type: 'virtualized'
}
Rules:
auto: full DOM for normal docs, staged DOM-present for large docs, never
virtualized.full: native DOM for everything.staged: DOM-present target with pending DOM coverage boundaries; native
behavior materializes before far selection/copy/find.virtualized: viewport DOM only, driven by layout rects, with explicit
native-behavior limitations.shell and preview-shell are cut from EditableDOMStrategy; they
are too easy to mistake for normal editable render modes. Product preview,
collapse, or read-only summary surfaces can be built above Slate by consulting
layout and DOM coverage.DOM coverage target:
slate-dom owns coverage state for mounted, pending, virtualized, and
structural ranges.slate-react registers coverage from the mount plan and materializes before
native-sensitive operations.slate-layout owns geometry only; it never pretends missing DOM is native.State-field policy:
collab: 'local',
history: 'skip', persist: false.dirtyStateKeys, root changes, and
affected block/runtime ids. A body text edit must not wake all page settings
subscribers; a page settings change may recompute the affected layout root.Performance shape:
Editable with no layout engine, no Pretext bundle,
and no virtualization code in the hot path;domStrategy, layoutMode, layoutEngine, document cohort,
native surface completeness, degradation mode.React 19.2 / Slate React rules applied:
useSyncExternalStore is the right subscription primitive for layout
snapshots and existing state-field hooks.useMemo option objects for stable behavior.Activity can preserve rare panels or inspectors, not editor body DOM.content-visibility can help DOM-present long pages, but cannot replace DOM
coverage or virtualized native-behavior policy.Migration backbone:
useSlateLayout and domStrategy without changing its
product plugins: Markdown, tables, comments, and diagnostics remain Plate/app
features over raw layout/projection primitives.Test pressure:
slate-layout: continuous snapshots, paged snapshots, state-field settings,
dirty-range recomposition, rich-inline run wrapping, table/box geometry.slate-react: domStrategy normalization, alias rejection, layout protocol,
mount plan, DOM coverage boundaries, state-field locality, selector locality.site/examples/ts/virtualization.tsx as the canonical
degraded-mode example. It must show native editing as the default posture,
then explicit virtualization as a stress-mode toggle backed by layout rects,
not estimated block size as the primary model.Trigger:
slate-react, slate-layout, and
slate-dom.Blast radius:
slate-react, slate-layout, slate-dom, plus examples and
Playwright proof routes.Three-scenario pre-mortem:
| Scenario | Failure | Guard |
|---|---|---|
| API alias rot | renderingStrategy stays beside domStrategy, examples keep using both, and agents/users learn the wrong abstraction. | Hard-cut public examples to domStrategy; no public docs for the old prop; temporary alias only as an implementation bridge before beta. |
| Hidden Pretext tax | Basic Editable pulls Pretext/layout code or waits on layout snapshots even when the user did not opt in. | layout is optional; no-layout Editable path has no Pretext import, no layout subscription, no virtualizer setup. Add bundle/source boundary proof. |
| Dirty native behavior | virtualized looks fast but breaks IME, screen readers, native selection, copy, paste, browser find, or far caret placement. | auto never chooses virtualization; virtualized mode exposes native-behavior limits in metrics and tests; browser rows must cover materialize-first behavior. |
| Subscription blowup | Layout snapshots wake every block/page on each keystroke and erase existing rerender-breadth wins. | Layout snapshots publish dirty ranges and versions; slate-react consumes runtime id/path/field-key selectors, not a broad layout object per block. |
| State-field misuse | Page settings become a dumping ground for app UI state, presence, comments, or layout caches. | Only persisted document-owned settings use state fields. View/debug/presence/cache state stays local, projection-owned, or collab: 'local' + history: 'skip' + persist: false. |
Steelman challenges:
| Decision | Best objection | Alternative | Chosen answer | Verdict |
|---|---|---|---|---|
domStrategy hard cut | The current renderingStrategy name already exists and churn costs time. | Keep name and document that it only means DOM mounting. | Documentation cannot fix a wrong noun. The prop is specifically about native DOM availability, so domStrategy is the cleaner beta API. | keep |
Cut public shell / preview-shell | Shell previews can be useful for product surfaces. | Keep object-only preview-shell beside virtualization. | No. Two public degraded DOM strategies is muddy. Product preview/collapse surfaces can use layout plus DOM coverage outside EditableDOMStrategy. | keep cut |
Layout prop on Editable | Editable becomes too opinionated and drags layout into raw Slate. | Keep pagination in PagedEditable only. | Raw Editable accepts only a structural layout protocol. This lets continuous layout and virtualization work without making Slate core or basic React depend on Pretext. | keep, protocol-only |
| State-field page settings | Page settings can be app/product policy. | Keep all page settings in React state. | Persisted, collaborative document layout settings are document state. Local view settings stay local. | keep |
| Layout-driven virtualization | Building layout before virtualization may cost more than estimated heights. | Keep TanStack estimated-size path. | Estimated heights are the dirty hack. Layout gives deterministic scroll anchoring, hit testing, and page/table fragments. Estimated size remains fallback only. | keep |
Expanded proof plan:
| Layer | Required proof |
|---|---|
| Unit | slate-layout continuous/paged snapshots; state-field settings changes; dirty-range recomposition; rich-inline line breaks; table/box geometry; projection/hit rects. |
| React/package | Editable domStrategy normalization; no public renderingStrategy type; no public shell or preview-shell; EditableLayout protocol contract; mount plan from layout; DOM coverage contract; selector locality for layout/state fields. |
| Browser | Continuous layout typing/Enter/Backspace/trailing spaces; pagination page-boundary caret/hit testing; virtualization.tsx far scroll/type/select; materialize-first copy/find; IME guards; browser find limitation rows; a11y rows for missing-DOM modes. |
| Performance | Large-doc mount/edit/scroll cohorts; layout recomposition dirty-range timings; DOM node counts; mounted item counts; cache hit rate; retained memory after document replacement. |
| Migration | Examples use only layout + domStrategy; virtualization example replaces public renderingStrategy naming; Plate can layer Markdown/tables/comments without raw Slate product APIs; slate-yjs syncs operations/state patches, not layout snapshots. |
| Docs/reference | PR reference and issue ledgers keep zero new Fixes/Improves until implementation proof exists; examples avoid old prop names. |
Rollback / remediation:
Editable imports Pretext/layout: block release and split the
protocol boundary.auto.domStrategy rollout
until selector locality passes.Verdict:
domStrategy only.preview-shell; shell-style preview is internal/app-owned only.| Dimension | Score | Evidence |
|---|---|---|
| React 19.2 runtime performance | 0.90 | live slate-react staged/shell/virtualized source; useSyncExternalStore state-field hooks; plan now requires layout-driven virtualization, selector locality, and no-layout fast path |
| Slate-close unopinionated DX | 0.94 | raw Slate and no-layout Editable stay Pretext-free; layout-aware users get the built-in Pretext path without fake engine boilerplate; final public name is domStrategy; public shell is cut |
| Plate/slate-yjs migration backbone | 0.89 | layout snapshots are derived, state-field settings are document-owned, content roots stay roots, and collab syncs operations/state patches only |
| Regression-proof testing | 0.88 | high-risk pass adds unit, React/package, browser, performance, migration, and docs/reference proof gates |
| Research evidence completeness | 0.93 | Pretext/Premirror/TanStack/local docs plus related issue ledgers and high-risk steelman rows reviewed |
| shadcn-style composability/minimalism | 0.94 | small layout + domStrategy call site with no public engine, mode, or default-root boilerplate; page is the paged-layout switch; PagedEditable owns viewport display policy |
Total: 0.94.
Status: complete. All Slate Ralplan closure gates pass for planning review.
| Pass | Status | Evidence added | Plan delta | Open issues | Next owner |
|---|---|---|---|---|---|
| current-state-read | complete | live .tmp/slate-v2 layout, Pretext, rendering strategy, virtualization, DOM coverage, pagination example, and docs/solutions reads | chose boundary rewrite: generic layout + domStrategy split + layout-driven virtualization | final API naming closed by performance/DX/migration pass | done |
| related-issue-discovery | complete | live/open issue ledgers, benchmark map, issue coverage matrix, fork dossier, and PR reference reviewed for #790, #4141, #5944, #5924, #3892, #2572, #5131, #2051 | added no-claim related issue matrix for layout/domStrategy plan | external ledger sync closed by issue-ledger-accounting pass | done |
| issue-ledger-accounting | complete | sync ledger, issue coverage matrix, fork dossier, and PR reference updated with architecture-only rows | preserved zero new fixed/improved issue claims and recorded no-claim boundaries | performance/DX/migration pressure closed by later pass | done |
| intent-decision-brief | complete | explicit boundary and decision brief | none | none | done |
| research-ecosystem-synthesis | complete | Pretext, Premirror, TanStack, current Slate v2 synthesis | none | optional deeper Lexical/ProseMirror/Tiptap pass if closure claims broader runtime law | done |
| performance-dx-migration-pressure | complete | live state-field, layout, rendering strategy, virtualization, shell-backed selection, and React subscription sources reviewed | chose domStrategy, cut public shell, folded Pretext into slate-layout, made SlateMountPlan internal, state-fielded document layout settings, kept no-layout Editable Pretext-free, and added perf/RUM/test pressure | high-risk pass must challenge the public rename and degraded native behavior contract | Slate Ralplan |
| high-risk-deliberate-pass | complete | high-risk trigger, blast radius, pre-mortem, steelman rows, expanded proof plan, and rollback policy | kept architecture; revised alias policy to public domStrategy only; cut public preview-shell; protected no-layout fast path | closure verified by final gates | done |
| closure-final-gates | complete | threshold audit, implementation-skill matrix, plan deltas, Done Handoff, Ralph proof gaps | marked ralplan lane complete for planning; implementation remains for later Ralph | no planning passes remain | done |
renderingStrategy to domStrategy;useSlateLayout;useSlatePageLayout as wrapper or example-only compatibility;slate-layout:
site/examples/ts/virtualization.tsx;virtualization.tsx the canonical explicit degraded-mode proof:
native/default posture first, explicit virtualized stress mode second,
layout rects as primary item sizing, materialize-first far selection/copy/
find, IME guard, and fixture rows for tables, voids, long words, trailing
spaces, and mixed inline marks.Closure status: complete.
Completion threshold audit:
| Gate | Result | Evidence |
|---|---|---|
Total score >= 0.92 | pass | closure score 0.94 |
No dimension below 0.85 | pass | final scorecard dimensions: 0.88 minimum |
| Pass schedule complete | pass | all pass-state rows complete or done |
| Issue discovery/accounting complete | pass | related issue discovery, sync ledger, coverage matrix, fork dossier, and PR reference updated |
| Public API maybe-language removed | pass | final target is domStrategy; public renderingStrategy, shell, and preview-shell are hard cuts before beta |
| Intent/decision boundaries explicit | pass | intent boundary and decision brief sections present |
| Ecosystem strategy complete | pass | Pretext, Premirror, TanStack, and current Slate v2 mechanisms mapped to Slate targets |
| High-risk deliberate mode complete | pass | trigger, blast radius, pre-mortem, steelman rows, proof plan, and rollback policy recorded |
| Verification workspace gate | pass for planning | no .tmp/slate-v2 implementation changed; live source was read for current-state evidence; implementation gates are named for Ralph execution |
| Final handoff in plan | pass | Done Handoff section below |
Applicable implementation-skill review matrix:
| Lens | Result | Evidence |
|---|---|---|
| Vercel React best practices | applied | useSyncExternalStore, primitive option deps, transition limits, no hidden Pretext import for no-layout Editable |
| performance-oracle | applied | repeated-unit budget, O(changed block plus dependent flow/page range), dirty-range recomposition, memory/RUM tags |
| performance | applied | cohorts, degradation contract, native behavior proof, DOM/memory tags, production metrics |
| tdd | applied as execution guidance | vertical proof slices named for layout, React, browser, performance, migration, and docs/reference |
| shadcn | skipped | no shadcn UI component or design-system API changed by this architecture plan |
| react-useeffect | skipped | no hook implementation changed; React subscription/effect constraints are recorded for Ralph execution |
Plan deltas from review:
slate-layout.slate-layout as the built-in layout engine while
keeping slate and no-layout slate-react Pretext-free.mode/default root layout boilerplate with page as the
paged-layout switch, implicit current-view root, and pageView for facing/gap
viewport policy.domStrategy naming and rejected mountStrategy.shell and preview-shell; preview/collapse surfaces are product
UI above Slate, not raw editable DOM strategies.SlateMountPlan internal.Fixes / Improves issue claims until implementation
proof exists.Done Handoff:
Before:
slate-layout is page-first.slate-react exposes renderingStrategy.After target:
slate-layout is a generic derived layout service with continuous and paged
snapshots.slate-layout includes the built-in Pretext engine and uses rich-inline line
breaking.Editable accepts optional layout and public domStrategy.PagedEditable composes over Editable from slate-layout/react.SlateMountPlan derives mounted ranges, active corridor, semantic islands,
materialization ranges, and DOM coverage boundaries from runtime plus layout.auto never chooses degraded virtualized.virtualized consumes layout rects/items.Ralph execution target:
renderingStrategy to domStrategy, remove public shell
and preview-shell, and rename metrics to DOM-strategy language.useSlateLayout and EditableLayout protocol.Editable free of Pretext/layout imports and subscriptions.Open proof gaps for Ralph, not this planning lane:
.tmp/slate-v2 implementation..tmp/slate-v2 gates below
before claiming behavior.domStrategy public API sliceChanged scope:
.tmp/slate-v2/packages/slate-react/src/components/editable-text-blocks.tsx.tmp/slate-v2/packages/slate-react/src/components/editable.tsx.tmp/slate-v2/packages/slate-react/src/index.ts.tmp/slate-v2/packages/slate-react/test/surface-contract.tsx.tmp/slate-v2/packages/slate-react/test/rendering-strategy-and-scroll.tsx.tmp/slate-v2/site/examples/ts/huge-document.tsx.tmp/slate-v2/site/examples/ts/pagination.tsx.tmp/slate-v2/docs/** DOM-strategy wording referencesResult:
Editable surface exposes domStrategy and onDOMStrategyMetrics.EditableDOMStrategy*.renderingStrategy names remain only in internal root/kernel wiring and
internal source assertions.Review findings:
EditableDOMRoot/keyboard/runtime wiring still uses
renderingStrategy language until the deeper mount-plan/layout owner is
rewritten.Verification:
# cwd: /Users/zbeyens/git/plate-2/.tmp/slate-v2
bun --filter slate-react test -- surface-contract provider-hooks-contract rendering-strategy-and-scroll
bun --filter slate-react typecheck
bun typecheck:site
bun lint:fix
Browser proof:
http://localhost:3100/examples/pagination reload shows the control label
DOM strategy, no old Rendering label, and the pagination viewport exists.http://localhost:3100/examples/huge-document?blocks=2 shows
DOM strategy, Requested DOM strategy, and Effective DOM strategy; old
requested/effective rendering-strategy labels are absent.slate-layout API sliceChanged scope:
.tmp/slate-v2/packages/slate-layout/src/index.ts.tmp/slate-v2/packages/slate-layout/src/react.tsx.tmp/slate-v2/packages/slate-layout/test/page-layout-contract.test.tsResult:
createSlateLayout, SlateLayout, SlateLayoutSnapshot,
SlateLayoutOptions, useSlateLayout, and useSlateLayoutSnapshot.page and hides the engine:
createSlateLayout(editor, () => ({ page: { margins: 72, preset: 'letter' } })).Review findings:
page was initially optional, which would imply
continuous layout before continuous geometry exists. It is required for this
paged subset until the continuous owner lands.Verification:
# cwd: /Users/zbeyens/git/plate-2/.tmp/slate-v2
bun --filter slate-layout test
bun --filter slate-layout typecheck
bun lint:fix
Changed scope:
.tmp/slate-v2/packages/slate-layout/package.json.tmp/slate-v2/packages/slate-layout/src/index.ts.tmp/slate-v2/packages/slate-layout/src/react.tsx.tmp/slate-v2/packages/slate-layout/test/page-layout-contract.test.ts.tmp/slate-v2/packages/slate-layout-pretext/package.json.tmp/slate-v2/packages/slate-layout-pretext/src/index.ts.tmp/slate-v2/site/examples/ts/pagination.tsx.tmp/slate-v2/site/tsconfig.json.tmp/slate-v2/bun.lockResult:
slate-layout owns pretextPageLayoutEngine and uses it as the built-in
engine behind createSlateLayout.slate-layout-pretext is only a compatibility re-export for the existing
package surface.slate-layout/react and calls
useSlateLayout(editor, { page, root, typography }); no public engine
setup appears at the example call site.{ preset, margins } object does not retrigger layout composition.Review findings:
useSlateLayout hook test that rerenders equivalent inline page
settings and verifies composition does not rerun.@chenglou/pretext dependency and site
path alias from slate-layout-pretext.slate-layout-pretext still exists as a compatibility
package. A later hard-cut can delete it if we decide beta should expose only
slate-layout.Verification:
# cwd: /Users/zbeyens/git/plate-2/.tmp/slate-v2
bun --filter slate-layout test
bun --filter slate-layout-pretext test
bun --filter slate-layout typecheck
bun --filter slate-layout-pretext typecheck
bun typecheck:site
bun lint:fix
Browser proof:
http://localhost:3100/examples/pagination reloads in the in-app Browser,
shows DOM strategy, exposes the pagination viewport, reports pages metrics,
and renders the Premirror fixture text.Changed scope:
.tmp/slate-v2/packages/slate-layout/package.json.tmp/slate-v2/packages/slate-layout/src/index.ts.tmp/slate-v2/packages/slate-layout/test/page-layout-contract.test.ts.tmp/slate-v2/bun.lockResult:
whiteSpace: 'normal' rich-inline path inside
pretextPageLayoutEngine.whiteSpace: 'pre-wrap' remains on the existing path to preserve editable
trailing spaces and hard-break fidelity until Pretext has a rich pre-wrap
primitive.slate-layout declares its React DOM test dependencies explicitly.Review findings:
whiteSpace: 'normal' and preserved the pre-wrap fallback.Verification:
# cwd: /Users/zbeyens/git/plate-2/.tmp/slate-v2
bun install
bun --filter slate-layout test
bun --filter slate-layout-pretext test
bun --filter slate-layout typecheck
bun --filter slate-layout-pretext typecheck
bun typecheck:site
bun lint:fix
Browser proof:
http://localhost:3100/examples/pagination reloads in the in-app Browser
after the engine change, shows DOM strategy, exposes the pagination
viewport, reports pages metrics, renders the Premirror fixture text, and
includes the mixed rich-text block.Changed scope:
.tmp/slate-v2/packages/slate-react/src/components/editable-text-blocks.tsx.tmp/slate-v2/packages/slate-react/src/index.ts.tmp/slate-v2/packages/slate-react/src/rendering-strategy/use-virtualized-root-plan.ts.tmp/slate-v2/packages/slate-react/test/rendering-strategy-and-scroll.tsx.tmp/slate-v2/packages/slate-layout/src/react.tsxResult:
EditableLayout protocol for layout-owned virtualized
top-level item geometry.size/start when provided,
including retained selected rows, total scroll size, and layout-backed
scrollToTopLevelIndex.PagedEditable passes a layout adapter derived from
getSlatePageLayoutProjection, so paged layout can feed virtualized DOM
materialization without slate-react importing slate-layout.minHeight and translateY instead of only the estimated block
size.Review findings:
Verification:
# cwd: /Users/zbeyens/git/plate-2/.tmp/slate-v2
bun --filter slate-react test -- rendering-strategy-and-scroll surface-contract
bun --filter slate-react typecheck
bun --filter slate-layout typecheck
bun typecheck:site
bun lint:fix
Browser proof:
http://localhost:3100/examples/pagination still renders DOM strategy,
pages metrics, the pagination viewport, and the Premirror fixture.http://localhost:3100/examples/huge-document?...strategy=virtualized...
mounts [data-slate-rendering-strategy-virtualizer="true"] with DOM coverage
boundaries in the in-app Browser.Changed scope:
.tmp/slate-v2/packages/slate-react/src/components/editable.tsx.tmp/slate-v2/packages/slate-react/src/components/editable-text-blocks.tsx.tmp/slate-v2/packages/slate-react/src/editable/root-selector-sources.ts.tmp/slate-v2/packages/slate-react/src/rendering-strategy/create-segment-plan.ts.tmp/slate-v2/packages/slate-react/test/rendering-strategy-and-scroll.tsx.tmp/slate-v2/packages/slate-react/test/surface-contract.tsx.tmp/slate-v2/site/examples/ts/huge-document.tsx.tmp/slate-v2/docs/libraries/slate-react/editable.md.tmp/slate-v2/docs/walkthroughs/09-performance.mdResult:
DOMStrategyType is only auto | full | staged; virtualized
remains explicit object-only and experimental.shell / preview-shell docs, huge-document controls, URL params,
and metrics rows are gone.shellCount,
shellAggressiveBoundaryCount, or 'shell' as an effective/degraded strategy
type. Internal segment coverage reports as partial-dom and keeps its
implementation boundary private.Editable bridge uses domStrategy* names for public-facing
plumbing and test contracts. Private shell coverage tests use a single
internal test helper instead of widening the public prop type.Review findings:
renderingStrategyVirtualizedOverscan, keeping old naming alive in the
high-level bridge. Renamed the bridge to domStrategy*.shell-aggressive remain internal. They are not documented or exported, and
the next owner can decide whether the private implementation vocabulary is
worth a hard file rename.Verification:
# cwd: /Users/zbeyens/git/plate-2/.tmp/slate-v2
bun --filter slate-react test -- surface-contract rendering-strategy-and-scroll
bun --filter slate-react typecheck
bun typecheck:site
bun lint:fix
Browser proof:
http://localhost:3100/examples/huge-document?blocks=1000&strategy=virtualized&threshold=1&estimated_block_size=24&overscan=0
reloads in the in-app Browser with DOM strategy options
auto, full, staged, virtualized.requested: virtualized, effective: virtualized,
boundaryCount: 1, viewportBoundaryCount: 1, and the virtualizer is
mounted with no horizontal page overflow.Knowledge capture:
docs/solutions/performance-issues/2026-05-03-slate-rendering-strategy-needs-production-rum-metrics.md
to the current DOM strategy API and recorded the metrics-leak prevention
rule for public API hard-cuts.Changed scope:
.tmp/slate-v2/packages/slate-react/src/dom-strategy/**.tmp/slate-v2/packages/slate-react/src/components/editable.tsx.tmp/slate-v2/packages/slate-react/src/components/editable-text-blocks.tsx.tmp/slate-v2/packages/slate-react/src/editable/**.tmp/slate-v2/packages/slate-react/src/index.ts.tmp/slate-v2/packages/slate-react/test/dom-strategy-and-scroll*.tsx.tmp/slate-v2/packages/slate-react/test/create-segment-plan-contract.test.ts.tmp/slate-v2/packages/slate-react/test/editing-kernel-contract.ts.tmp/slate-v2/packages/slate-react/test/keyboard-input-strategy-contract.test.ts.tmp/slate-v2/packages/slate-react/test/provider-hooks-contract.tsx.tmp/slate-v2/packages/slate-react/test/surface-contract.tsx.tmp/slate-v2/docs/general/docs-proof-map.md.tmp/slate-v2/docs/libraries/slate-react/experimental-virtualized-rendering.mdResult:
rendering-strategy to
dom-strategy, so internal file paths, imports, exports, DOM attrs, and test
files match the public domStrategy API boundary.segment-placeholder.tsx and data-slate-dom-strategy-placeholder.domStrategyRuntime names instead of
renderingStrategy* names.renderingStrategy only in negative public-surface
tests that prove the old prop/export names do not exist.Review findings:
../dom-strategy/segment-shell import; focused tests caught it and the import
now points at segment-placeholder.data-slate-rendering-strategy-* owner; renamed them to
data-slate-dom-strategy-*.shell-backed remain as the
existing model-selection vocabulary. They are behavior-bearing internals, not
public DOM strategy API names, so they should move only in a separate
selection-contract pass.Verification:
# cwd: /Users/zbeyens/git/plate-2/.tmp/slate-v2
bun --filter slate-react test -- surface-contract dom-strategy-and-scroll create-segment-plan-contract keyboard-input-strategy-contract editing-kernel-contract provider-hooks-contract
bun --filter slate-react typecheck
bun typecheck:site
bun lint:fix
bun --filter slate-react test -- surface-contract dom-strategy-and-scroll create-segment-plan-contract keyboard-input-strategy-contract editing-kernel-contract provider-hooks-contract
bun --filter slate-react typecheck
bun typecheck:site
bun lint:fix
bun --filter slate-react test
bun --filter slate-react typecheck
bun --filter slate-layout test
bun --filter slate-layout typecheck
bun typecheck:site
bun lint:fix
Browser proof:
http://localhost:3100/examples/huge-document?blocks=1000&strategy=virtualized&threshold=1&estimated_block_size=24&overscan=0
reloads in the in-app Browser with DOM strategy options
auto, full, staged, virtualized, no Shell option, no shell metric row,
requested: virtualized, effective: virtualized, one DOM coverage boundary,
one viewport boundary, mounted virtualizer rows, and no horizontal overflow.http://localhost:3100/examples/pagination reloads in the in-app Browser
with DOM strategy, five pages, no horizontal overflow, no old
data-slate-rendering-strategy-* attrs, and the Premirror plus rich Markdown
pagination fixture visible.Ralph execution status: complete. Remaining owners: none for this lane.
ralplan_lane_status: complete final_handoff_status: complete
Planning gate:
# cwd: /Users/zbeyens/git/plate-2
node tooling/scripts/completion-check.mjs --id 019e46be-4ec4-7d11-bc6e-9fcf033a8803
Implementation gates after Ralph edits .tmp/slate-v2:
# cwd: /Users/zbeyens/git/plate-2/.tmp/slate-v2
bun --filter slate-layout test
bun --filter slate-react test
bun --filter slate-layout typecheck
bun --filter slate-react typecheck
bun typecheck:site
PLAYWRIGHT_RETRIES=0 PLAYWRIGHT_WORKERS=1 bun run playwright playwright/integration/examples/pagination.test.ts --project=chromium
PLAYWRIGHT_RETRIES=0 PLAYWRIGHT_WORKERS=1 bun run playwright playwright/integration/examples/virtualization.test.ts --project=chromium
PLAYWRIGHT_RETRIES=0 PLAYWRIGHT_WORKERS=1 bun run playwright playwright/integration/examples/huge-document.test.ts --project=chromium
bun lint:fix
bun check
Reopened on 2026-05-22 for the root-location authority cleanup discovered by Codex review after the layout/rendering lane closed.
status: pending implementation current_pass: root-location-authority-amendment current_pass_status: complete next_pass: ralph-root-location-cleanup-execution score: 0.91
Hard take: the review fix is behaviorally right, but not the absolute best architecture yet.
The live .tmp/slate-v2 diff proves the correct bug class:
PointRef / RangeRef created inside a non-main view must bind to
the invoking view root;OperationApi.inverse(set_selection) must restore the root of the selection
being restored, not keep the root of the selection being undone.The remaining bad shape is duplication and metadata leakage:
MAIN_ROOT_KEY, getOperationRoot, point-root inference, range-root
inference, and selection-patch-root inference are repeated across
point.ts, path-ref.ts, point-ref.ts, range-ref.ts,
operation.ts, and transforms/general.ts.PointRefApi.transform and RangeRefApi.transform need ad hoc
__explicitRoot, __explicitAnchorRoot, and __explicitFocusRoot casts.Final call: keep the behavior, rewrite the authority boundary. Do not rewrite runtime/view. Do add one internal root-location module and test it directly.
Intent: make multi-root operations, refs, selection inverses, history replay, and view-scoped updates use one root authority rule.
Desired outcome: every root-aware API answers the same question the same way: which root owns this point/range/path/operation, and should that root be visible to the public caller?
In scope:
PathRef, PointRef, RangeRef, PointApi.transform,
RangeApi.transform, OperationApi.inverse, and replay/apply routing.Non-goals:
Point / Range shape again.Path.Decision boundary: this is a core Slate data-model/runtime cleanup. It belongs
in .tmp/slate-v2/packages/slate, not slate-react, slate-layout, or Plate.
Principles:
Path segment.Options:
__explicit*Root fields.
PointApi / RangeApi.
Add an internal module, preferably:
packages/slate/src/internal/root-location.ts
Export internal helpers only:
export const MAIN_ROOT_KEY = 'main'
export type RootVisibility = 'explicit' | 'implicit'
export type PointRootMeta = {
root: string
visibility: RootVisibility
}
export type RangeRootMeta = {
anchor: PointRootMeta
focus: PointRootMeta
root: string | null
}
export function getOperationRoot(operation: Operation): string
export function getPointRoot(point: Point, fallback?: string): PointRootMeta
export function getRangeRoot(range: Range, fallback?: string): RangeRootMeta
export function getSelectionPatchRoot(
patch: Partial<Range> | Range | null
): string | undefined
export function withImplicitPointRoot(point: Point, root: string): Point
export function withImplicitRangeRoot(range: Range, root: string): Range
export function stripImplicitPointRoot(point: Point, meta: PointRootMeta): Point
export function stripImplicitRangeRoots(range: Range, meta: RangeRootMeta): Range
Implementation rules:
PointApi.transform imports getPointRoot / getOperationRoot and returns
the original point unchanged for sibling-root operations.PathRefApi, PointRefApi, and RangeRefApi use the same
getOperationRoot.OperationApi.inverse(set_selection) uses getSelectionPatchRoot.PointRef / RangeRef creation records root visibility in internal WeakMaps
or a single internal metadata type, not scattered __explicit*Root casts.current and unref() preserve input shape:
Preferred metadata owner:
const POINT_REF_ROOT = new WeakMap<PointRef, PointRootMeta>()
const RANGE_REF_ROOT = new WeakMap<RangeRef, RangeRootMeta>()
This is cleaner than storing __explicitRoot fields on the ref object because
the public ref interface stays honest and the implementation has one authority
for visibility.
Add or tighten these tests in .tmp/slate-v2:
| File | Required coverage |
|---|---|
packages/slate/test/root-location-contract.ts | getOperationRoot, getPointRoot, getRangeRoot, getSelectionPatchRoot, implicit root injection, implicit root stripping, mismatched range roots |
packages/slate/test/editor-runtime-view-contract.ts | rootless pointRef and rangeRef created inside a header view shift on header ops and ignore main ops |
packages/slate/test/editor-runtime-view-contract.ts | rootless multi-block delete from a header view merges/deletes only header content |
packages/slate/test/rooted-operation-contract.ts | inverse set_selection from main -> header replays into main |
packages/slate/test/rooted-operation-contract.ts | inverse set_selection from header -> null and null -> header preserves the correct root behavior |
packages/slate/test/range-ref-contract.ts | public rangeRef.current, draft publication, and unref() preserve explicit/rootless input shape |
packages/slate/test/range-ref-contract.ts | public and internal range refs are removed only when matching-root operations delete the range |
packages/slate/test/transaction-contract.ts | committed root-scoped set_selection operation carries the active root, while command middleware payload stays caller-shaped |
packages/slate-history/test/document-state-history-contract.ts or new history row | undo/redo restores multi-root selection and root-scoped refs after text edits |
packages/slate/test/interfaces-contract.ts | PointApi.equals, PointApi.compare, RangeApi.equals, and RangeApi.intersection keep root-aware semantics |
Coverage rejects:
internal/root-location.ts and direct unit tests.point.ts, path-ref.ts,
point-ref.ts, range-ref.ts, operation.ts, and transforms/general.ts.codex review --uncommitted; accept only findings that beat this
plan's architecture boundary.| Lens | Decision |
|---|---|
tdd | applied: this is a behavior regression class; helper contract and root-scoped ref tests are mandatory |
performance-oracle | applied: root checks stay O(1), WeakMap metadata avoids extra point/range cloning except transform-time injection/strip |
vercel-react-best-practices | skipped: no React render/subscription surface changes |
performance | skipped: no RUM/cohort surface changes |
react-useeffect | skipped: no effects |
shadcn | skipped: no UI |
| Change | Objection | Answer | Verdict |
|---|---|---|---|
| Add internal root-location helper | This is extra abstraction for five files. | The duplicated root rules already produced a real review bug. One internal helper prevents path/ref/operation drift without public API churn. | keep |
| Use WeakMap metadata for ref root visibility | Hidden metadata is harder to inspect. | It is less dirty than public __explicit*Root fields and keeps PointRef / RangeRef shape close to Slate. Tests prove public current / unref() behavior. | keep |
Keep root on Point / Range, not Path | Root-aware compare changes legacy mental model. | Multi-root needs a root dimension. Putting it in Path would poison every path algorithm; point/range metadata is the least bad shape. | keep |
Focused gates:
# cwd: /Users/zbeyens/git/plate-2/.tmp/slate-v2
bun test ./packages/slate/test/root-location-contract.ts ./packages/slate/test/editor-runtime-view-contract.ts ./packages/slate/test/rooted-operation-contract.ts ./packages/slate/test/range-ref-contract.ts ./packages/slate/test/transaction-contract.ts ./packages/slate/test/interfaces-contract.ts ./packages/slate-history/test/document-state-history-contract.ts
bun typecheck:packages
bun lint:fix
codex review --uncommitted
Broad gate before closeout:
# cwd: /Users/zbeyens/git/plate-2/.tmp/slate-v2
bun test:bun
bun typecheck:packages
bun lint
Planning gate:
# cwd: /Users/zbeyens/git/plate-2
node tooling/scripts/completion-check.mjs
Added a new runnable owner after the previous closed layout lane:
ralph-root-location-cleanup-execution.ralplan_lane_status: pending final_handoff_status: pending
Changed scope:
active goal stateactive goal statedocs/plans/2026-05-22-slate-v2-pretext-layout-rendering-architecture-ralplan.mdResult:
blocked with
blocked_reason: user explicitly paused after Ralph handoff generation
instead of pending, preventing the stop hook from immediately resuming work.pending before editing.Review finding to fix next:
codex review --uncommitted:
packages/slate/src/core/public-state.ts treats only explicit
history: 'push' fields as needing replayable patch hooks, but omitted
history fields are still saved by shouldSaveStatePatch because only
history: 'skip' is excluded. Large default-history values can therefore
enter undo history as full snapshots without the intended 32KB guard.Current next owner:
state-field-large-patch-policy-fix.tmp/slate-v2/packages/slate/src/core/public-state.ts.tmp/slate-v2/packages/slate/test/collab-document-state-contract.ts or a
nearby state-field/history contract file.Resume gates:
# cwd: /Users/zbeyens/git/plate-2/.tmp/slate-v2
bun test ./packages/slate/test/collab-document-state-contract.ts ./packages/slate/test/root-location-contract.ts ./packages/slate/test/editor-runtime-view-contract.ts ./packages/slate/test/rooted-operation-contract.ts ./packages/slate/test/range-ref-contract.ts ./packages/slate/test/transaction-contract.ts ./packages/slate/test/interfaces-contract.ts ./packages/slate-history/test/document-state-history-contract.ts
bun typecheck:packages
bun lint:fix
codex review --uncommitted
Closeout gates after the fix:
# cwd: /Users/zbeyens/git/plate-2/.tmp/slate-v2
bun test:bun
bun typecheck:packages
bun lint
# cwd: /Users/zbeyens/git/plate-2
node tooling/scripts/completion-check.mjs
Issue/reference decision:
Changed scope:
active goal stateactive goal state.tmp/slate-v2/packages/slate/src/core/public-state.ts.tmp/slate-v2/packages/slate/test/document-state-patch-contract.tsdocs/plans/2026-05-22-slate-v2-pretext-layout-rendering-architecture-ralplan.mdResult:
history !== 'skip' and
collab === 'shared' require patch hooks for large values.blocked instead
of pending; resume must set it back to pending before review or edits.Verification evidence:
# cwd: /Users/zbeyens/git/plate-2/.tmp/slate-v2
bun test ./packages/slate/test/document-state-patch-contract.ts
bun test ./packages/slate/test/document-state-patch-contract.ts ./packages/slate/test/collab-document-state-contract.ts ./packages/slate/test/root-location-contract.ts ./packages/slate/test/editor-runtime-view-contract.ts ./packages/slate/test/rooted-operation-contract.ts ./packages/slate/test/range-ref-contract.ts ./packages/slate/test/transaction-contract.ts ./packages/slate/test/interfaces-contract.ts ./packages/slate-history/test/document-state-history-contract.ts
bun typecheck:packages
bun test:bun
bun lint:fix
bun lint
Interrupted evidence:
codex review --uncommitted run was active when the user paused.Current next owner:
final-codex-review-closeoutpending, rerunning
codex review --uncommitted, fixing any accepted finding, then closing the
plan and completion gate only after node tooling/scripts/completion-check.mjs
passes.Issue/reference decision: