Back to Plate

Slate v2 Extension Context State/Tx Coverage Ralplan

docs/plans/2026-05-18-slate-v2-extension-context-state-tx-coverage-ralplan.md

53.0.641.3 KB
Original Source

Slate v2 Extension Context State/Tx Coverage Ralplan

Date: 2026-05-18 Status: done Completion id: 019e1fc0-dba0-7de1-9236-b484a144cda6 Current pass: ralph-implementation-closeout Current pass status: complete Score: 0.97 implemented

Verdict

Yes: extension.queries should receive state.

No: transform middleware should not receive state as its primary context. deleteBackward, deleteForward, insertBreak, insertText, and the rest of transforms are update lifecycle hooks. The target shape is transforms.*({ tx, next, ...args }), where tx already includes read methods plus write methods.

The catch: current source does not yet guarantee that every transform middleware call runs inside an update transaction. That must be fixed before exposing tx in transform middleware. Adding state there would make the API easier to implement today and worse forever.

Intent

Define the context object for every Slate v2 extension callback so users stop guessing whether they should call editor.read, editor.update, editor.api, or a lifecycle object.

Outcome

  • Read lifecycle callbacks get state.
  • Update lifecycle callbacks get tx.
  • Post-commit callbacks get commit and snapshot.
  • Registration and long-lived extension APIs get editor.
  • Operation middleware stays low-level and does not get tx.
  • No long-lived editor.state root shortcut. It looks nice, but it creates stale-read pressure.

Non-Goals

  • No Slate v2 implementation edits in this Ralplan pass.
  • No Plate compatibility layer.
  • No public compatibility aliases for old Editor.*, DOMEditor.*, or HistoryEditor.* helper namespaces.
  • No migration notes in public docs; this plan is implementation guidance.

Current Source Evidence

  • .tmp/slate-v2/packages/slate/src/interfaces/editor.ts:466 defines EditorCoreStateView read groups.
  • .tmp/slate-v2/packages/slate/src/interfaces/editor.ts:484 defines EditorCoreUpdateTransaction as read groups plus write groups.
  • .tmp/slate-v2/packages/slate/src/interfaces/editor.ts:505 exposes editor.api, editor.getApi, editor.read, editor.update, editor.extend.
  • .tmp/slate-v2/packages/slate/src/interfaces/editor.ts:789 has transform middleware context as { editor, next, ...args }.
  • .tmp/slate-v2/packages/slate/src/interfaces/editor.ts:928 has query middleware context as { editor, next, ...args }.
  • .tmp/slate-v2/packages/slate/src/interfaces/editor.ts:1210 already gives normalizers a restricted tx.
  • .tmp/slate-v2/packages/slate/src/interfaces/editor.ts:1252 has extension state, tx, and editor group factories.
  • .tmp/slate-v2/packages/slate/src/interfaces/editor.ts:1304 has clipboard context as { editor, next }.
  • .tmp/slate-v2/packages/slate/src/interfaces/editor.ts:1325 has register context as { editor, name, options, runtimeState, signal }.
  • .tmp/slate-v2/packages/slate/src/interfaces/editor.ts:1338 lists extension slots: api, clipboard, commitListeners, editor, elements, normalizers, operationMiddlewares, queries, state, transforms, tx.
  • .tmp/slate-v2/packages/slate/src/core/public-state.ts:971 builds the read state view and routes read methods through query middleware.
  • .tmp/slate-v2/packages/slate/src/core/public-state.ts:1384 builds tx by spreading state and adding write methods.
  • .tmp/slate-v2/packages/slate/src/core/public-state.ts:1500 builds the restricted normalizer transaction.
  • .tmp/slate-v2/packages/slate/src/core/public-state.ts:1531 implements editor.read((state) => ...).
  • .tmp/slate-v2/packages/slate/src/core/public-state.ts:1553 rejects editor.update inside query middleware.
  • .tmp/slate-v2/packages/slate/src/core/public-state.ts:1757 routes operations through operation middleware with { editor, operation }.
  • .tmp/slate-v2/packages/slate/src/core/public-state.ts:2594 notifies commit listeners with commit and snapshot.
  • .tmp/slate-v2/packages/slate/src/core/editor-extension.ts:156 rejects legacy methods and commands extension slots.
  • .tmp/slate-v2/packages/slate/src/core/editor-extension.ts:373 registers transform middleware and passes only { editor, next, ...args }.
  • .tmp/slate-v2/packages/slate/src/core/editor-extension.ts:423 registers clipboard insertData and passes only { editor, next }.
  • .tmp/slate-v2/packages/slate/src/core/editor-extension.ts:451 passes normalizer contexts through with tx.
  • .tmp/slate-v2/packages/slate/src/core/query-middleware.ts:121 executes query middleware and passes only { editor, next, ...args }.
  • .tmp/slate-v2/packages/slate/src/core/command-registry.ts:69 only creates an implicit update when called with implicitUpdate.
  • .tmp/slate-v2/packages/slate/src/core/transform-middleware.ts:136 calls executeCommand without implicitUpdate.
  • .tmp/slate-v2/site/examples/ts/check-lists.tsx:89 shows current transform middleware repeatedly calling editor.read and then editor.update.
  • .tmp/slate-v2/site/examples/ts/tables.tsx:115 shows the same repeated read pattern in delete middleware.
  • .tmp/slate-v2/site/examples/ts/images.tsx:96 shows clipboard insertData(data, { editor, next }) with async FileReader.
  • .tmp/slate-v2/packages/slate/test/query-extension-contract.ts:527 proves query middleware cannot mutate through editor.update.
  • .tmp/slate-v2/packages/slate/test/generic-extension-namespace-contract.ts:254 already has negative type tests for invalid transform and normalizer surfaces.
  • .tmp/slate-v2/packages/slate/test/extension-methods-contract.ts:742 covers same-name/latest extension and enabled: false tombstones.

Lifecycle Coverage Map

SurfaceCurrent contextLifecycleTarget contextDecision
editor.readcallback gets statereadkeep statekeep
editor.updatecallback gets transaction named tx in examplesupdatekeep txkeep
extension.state factory(state, editor)read group constructionkeep (state, editor)keep
extension.tx factory(transaction, editor)update group constructionrename parameter examples to txkeep
extension.editor factory(editor)long-lived editor helper constructionkeep (editor)keep, but avoid public examples unless needed
extension.apistatic API objectlong-lived API namespacekeep under editor.api and editor.getApi(extension)keep
extension.transforms.*{ editor, next, ...args }update middleware{ tx, editor, next, ...args } after transaction routing is fixedrevise
extension.queries.*{ editor, next, ...args }read middleware{ state, editor, next, ...args }revise
extension.normalizers.editor/node{ tx, editor, next, ...args } with restricted txupdate normalizationkeep restricted txkeep
extension.clipboard.insertData(data, { editor, next })DOM/DataTransfer ingress, often async(data, { state, editor, next }); no txrevise
extension.operationMiddlewares({ editor, operation }, next)operation dispatch pipelinekeep no state/txkeep
extension.commitListeners(commit, snapshot)post-commitkeep no state/txkeep
extension.register{ editor, name, options, runtimeState, signal }installation lifecyclekeep no state/txkeep
extension.elementsdeclarative specsschema/spec registrationkeep no contextkeep
editor.subscribelistener receives snapshot update pathpost-commit subscriptionkeep no state/txkeep
internal commands registrycommand context with editorinternal dispatchdo not expose as public DXkeep internal
internal capabilities registrycurrently backs clipboard and legacy-style lanesinternal runtime registrydo not expose public capabilitieskeep internal or shrink later

Before / After Shapes

Query Middleware

Current:

ts
queries: {
  text: {
    string({ at, next, options }) {
      return `${next({ at, options })}!`
    },
  },
}

Target:

ts
queries: {
  text: {
    string({ at, next, options, state }) {
      const selection = state.selection.get()

      if (!selection) {
        return next({ at, options })
      }

      return `${next({ at, options })}!`
    },
  },
}

Rule: use next for the query being intercepted. Use state for adjacent reads. Do not make state.text.string(...) magically bypass the same middleware; that hides recursion instead of teaching the middleware model.

Transform Middleware

Current checklist example:

ts
deleteBackward({ editor, next }) {
  const selection = editor.read((state) => state.selection.get())

  if (selection && RangeApi.isCollapsed(selection)) {
    const match = editor.read((state) =>
      state.nodes.find({
        match: (n) => NodeApi.isElement(n) && n.type === 'check-list-item',
      })
    )

    if (match) {
      const [, path] = match
      const start = editor.read((state) => state.points.start(path))

      if (PointApi.equals(selection.anchor, start)) {
        editor.update((tx) => {
          tx.nodes.set({ type: 'paragraph' })
          tx.selection.set(start)
        })
        return
      }
    }
  }

  next()
}

Target:

ts
deleteBackward({ tx, next, unit }) {
  const selection = tx.selection.get()

  if (selection && RangeApi.isCollapsed(selection)) {
    const match = tx.nodes.find({
      match: (n) => NodeApi.isElement(n) && n.type === 'check-list-item',
    })

    if (match) {
      const [, path] = match
      const start = tx.points.start(path)

      if (PointApi.equals(selection.anchor, start)) {
        tx.nodes.set(
          { type: 'paragraph' } satisfies Partial<SlateElement>,
          {
            match: (n) => NodeApi.isElement(n) && n.type === 'check-list-item',
          }
        )
        tx.selection.set(start)
        return
      }
    }
  }

  next({ unit })
}

Implementation gate: this target is only valid if transform middleware execution is wrapped in the same transaction as the default transform. Current executeTransformMiddleware calls executeCommand without implicitUpdate, so this is not just a type edit.

Clipboard Middleware

Current:

ts
clipboard: {
  insertData(data, { editor, next }) {
    // parse DataTransfer
    editor.update((tx) => {
      tx.fragment.insert(fragment)
    })
    return true
  },
}

Target:

ts
clipboard: {
  insertData(data, { editor, next, state }) {
    const selection = state.selection.get()

    if (!selection) {
      return next()
    }

    editor.update((tx) => {
      tx.fragment.insert(fragment)
    })
    return true
  },
}

No tx here. Clipboard handlers sit on a DOM/DataTransfer boundary and may cross async FileReader or upload boundaries. Holding a transaction open across that is a bug magnet.

Normalizers

Current and target:

ts
normalizers: {
  node({ entry, next, tx }) {
    const value = tx.value.get()
    tx.nodes.insert({ type: 'paragraph', children: [{ text: '' }] })
    next()
  },
}

Keep the restricted normalizer tx. Existing negative type tests already reject tx.normalize, tx.withoutNormalizing, tx.operations.replay, and whole-value replacement.

Decision Brief

Principles:

  • Lifecycle object first. state means read. tx means update. editor means long-lived runtime handle.
  • No duplicate readable spellings for the same thing when one is enough.
  • Do not add root editor.state. It is too easy to treat as always-current mutable state.
  • Do not expose tx outside synchronous update lifecycle.
  • Do not let Plate-shaped plugin sugar leak into raw Slate.

Viable options:

  1. editor only everywhere.
    • Rejected. It keeps examples verbose and hides lifecycle legality.
  2. state everywhere plus explicit editor.update.
    • Rejected. It is wrong for transform middleware because transforms are write hooks.
  3. tx everywhere.
    • Rejected. It is wrong for query and clipboard lifecycle.
  4. Lifecycle-specific context.
    • Accepted. It matches the existing state/tx split and keeps async boundaries honest.

Ecosystem Evidence

Lexical:

  • Source summary: docs/research/sources/editor-architecture/lexical-read-update-extension-runtime.md.
  • Mechanism: commands and transforms run inside update context; reads and writes have synchronous legality boundaries.
  • Slate target: editor.read, editor.update, state in read callbacks, tx in update callbacks.
  • Reject: Lexical class nodes, $ helpers, and command-first app API.
  • Verdict: agree on lifecycle discipline, diverge on public style.

Tiptap:

  • Source summary: docs/research/sources/editor-architecture/tiptap-extension-command-react-dx.md.
  • Mechanism: extension packaging and command catalog make features discoverable.
  • Slate target: keep extension packaging and editor.api, but make editor.update the write lifecycle.
  • Reject: making commands the default mutation API.
  • Verdict: partial.

Issue corpus:

  • docs/slate-issues/issue-intelligence-master-plan.md says the corpus justifies replacing the execution/runtime model, not the JSON model.
  • Older batches keep reinforcing plugin/render composition, paste, focus, history, delete, and input-runtime pressure.
  • This plan does not claim any issue fixed.

ProseMirror:

  • Source summary: docs/research/sources/editor-architecture/prosemirror-transaction-view-dom-runtime.md.
  • Mechanism: commands receive current state; transactions own document, selection, marks, metadata, and mapping.
  • Slate target: query middleware receives state; transform middleware receives update-local tx; operation middleware stays closer to operation dispatch than app command DX.
  • Reject: ProseMirror plugin complexity, integer positions, and command-first public mutation style.
  • Verdict: agree on transaction ownership and command-state context, diverge on public extension shape.

Issue Accounting

No global issue ledger changes are required. The related rows are already classified in docs/slate-issues/gitcrawl-v2-sync-ledger.md, docs/slate-v2/ledgers/issue-coverage-matrix.md, and docs/slate-v2/ledgers/fork-issue-dossier.md. This plan changes API target shape; it does not prove an issue reproduction.

IssueClusterClaimWhyProof routeV2 sync ledgerPR line
#3222plugin/API designRelatedExtension context cleanup answers plugin-author pressure, but it does not close the historical plugin design discussion.plan/API proof onlyexisting cluster-synced rowrelated matrix only
#4089higher-level plugins APIRelatedThe plan keeps raw Slate unopinionated and gives extension lifecycle objects; it does not add product-level plugin bundles.plan/API proof onlyexisting cluster-synced rowrelated matrix only
#4181custom key behaviorNot claimedThe row is already triaged as likely invalid; transform middleware covers Slate commands, not component-level keypress feature requests.no-claimexisting triage-closed rownone
#3177render compositionRelatedExtension-owned rendering direction reduces prop-level composition pressure, but this plan does not implement renderer composition.plan/API proof onlyexisting cluster row in frozen ledgerrelated matrix only
#4721async Editable eventsNot claimedClipboard/transform/query context does not define async event handler return semantics.no-claimexisting cluster-synced rownone
#5233clipboard fragment formatAlready fixed elsewhereThis plan preserves clipboard boundary direction but adds no new transport proof.existing clipboard proofexisting fixes-claimed rowno change
#4569insertData docsAlready fixed elsewhereThis plan changes future callback context, not the already-claimed docs fix.existing docs proofexisting fixes-claimed rowno change
#1024clipboard schema boundaryRelatedstate in clipboard context supports read checks; MIME/document typing remains a DOM/model transport issue.plan/API proof onlyexisting cluster-synced rowrelated matrix only
#2405command-scoped normalizationRelatedRestricted normalizer tx is preserved; command-specific rule evaluation/perf remains benchmark work.plan/API proof onlyexisting cluster-synced rowrelated matrix only
#2288range operationsRelatedTransaction context direction aligns with range-capable ops, but this plan adds no operation exposure.existing core proof plus planexisting cluster-synced rowno change
#1770operation compositionRelatedKeeping operation middleware low-level avoids pretending transform context solves operation merging.existing core proof plus planexisting cluster-synced rowno change
#3874history atomic groupsRelatedtx transform context is compatible with transaction-aware history, but no history API closure is claimed.plan/API proof onlyexisting cluster-synced rowrelated matrix only
#5080query traversalAlready fixed elsewhereQuery middleware state does not change traversal order.existing query proofexisting fixes-claimed rowno change
#5684query traversal ambiguityNot claimedThe issue still needs a concrete reproduction; adding state to query middleware is not a traversal fix.no-claimexisting issue-reviewed rownone

Type Test Requirements

Add or revise negative type tests in the execution plan:

  • Query middleware context exposes state and does not expose tx.
  • Query middleware can call next once and cannot mutate with editor.update.
  • Query generator cleanup still cannot mutate.
  • Transform middleware context exposes tx; examples should not call editor.read for routine reads.
  • Transform middleware tx includes read groups and write groups.
  • Transform middleware does not expose a separate state duplicate.
  • Clipboard context exposes state and does not expose tx.
  • Normalizer context keeps restricted tx; no whole-value replace, no recursive normalize, no replay.
  • Operation middleware context does not expose tx.
  • Disabled extensions are erased from installed extension types.
  • Latest same-name extension wins; no replaces field.
  • editor.getApi(extension) returns the typed API for installed extensions and rejects non-installed extensions.

Exact files:

  • .tmp/slate-v2/packages/slate/test/extension-methods-contract.ts: transform middleware tx context, transaction routing, no double next, same-name/latest and enabled: false.
  • .tmp/slate-v2/packages/slate/test/query-extension-contract.ts: query middleware state context, recursion policy, mutation rejection, generator cleanup rejection.
  • .tmp/slate-v2/packages/slate/test/generic-extension-namespace-contract.ts: declaration merging, disabled extension type erasure, getApi(extension) typing, negative context assertions.
  • .tmp/slate-v2/packages/slate/test/generic-extension-install-contract.ts: installed extension inference and tombstone behavior.
  • .tmp/slate-v2/packages/slate/test/normalization-contract.ts: restricted normalizer tx stays restricted.
  • .tmp/slate-v2/packages/slate-dom/test/clipboard-boundary.ts and .tmp/slate-v2/packages/slate/test/clipboard-contract.ts: clipboard state read plus fresh update writes, no async tx.

Example Update Requirements

Update examples after implementation:

  • site/examples/ts/check-lists.tsx: inline transform logic should use tx.selection.get, tx.nodes.find, tx.points.start, tx.nodes.set, tx.selection.set.
  • site/examples/ts/tables.tsx: delete/backspace/enter boundary logic should live in deleteBackward, deleteForward, and insertBreak transform middleware, not keydown event branches.
  • site/examples/ts/images.tsx: clipboard should use state.selection.get for read checks and editor.update for sync writes; async FileReader writes must start a fresh editor.update.
  • Public examples should prefer extension callbacks over renderElement / renderLeaf props when the extension owns rendering. Raw <Editable renderElement> remains acceptable only as a low-level escape hatch example.

Open Risks

  • Transform middleware cannot get tx until the dispatch path is guaranteed to run inside an update transaction. Current executeTransformMiddleware does not force implicitUpdate.
  • Query middleware with full state can recursively call the same query through state. The model should be documented and tested: next is the continuation for the current query.
  • Clipboard state is a snapshot-time read. Async callbacks must use a fresh editor.read or editor.update.
  • Keeping editor in every context is useful for editor.api, but examples must not teach root editor mutation as the normal path.

Maintainer Objection Ledger

ChangeStrong objectionBest no-change alternativeTradeoffAnswerVerdict
Query middleware gets state"This makes recursive query calls easier to write by accident."Keep editor.read inside query middleware.The target adds context but needs a same-query recursion rule.Keep next as the only continuation for the intercepted query; use state for adjacent reads. Add tests.keep
Transform middleware gets tx"This is not true until transform middleware runs in an update."Keep { editor, next } and let examples call editor.update.Requires command/transform dispatch refactor.Do the dispatch refactor. state in transforms would teach the wrong lifecycle.keep
Clipboard gets state, not tx"Paste handlers often write immediately, so tx would be convenient."Give clipboard tx and ban async use by docs.Convenient sync paste, dangerous async paste.Clipboard is host ingress. Reads can use state; writes start editor.update.keep
Operation middleware gets no tx"Advanced plugins may want to inspect state while rewriting operations."Add state or tx to operation context.More power, higher recursion/corruption risk.Keep operation middleware about operation dispatch. Add a later dedicated low-level hook only if a real package needs it.keep
No root editor.state"It would be shorter than editor.read."Add editor.state.selection.get().Shorter reads, stale-read ambiguity.Keep editor.read as the coherent read boundary. Middleware context is the ergonomic shortcut.keep

High-Risk Deliberate Mode

Trigger: public API and extension substrate change.

Pre-mortem:

  • Transform middleware exposes tx but is not actually update-local, causing nested updates or stale reads.
  • Query middleware state causes accidental same-query recursion and confusing stack overflows.
  • Clipboard examples accidentally hold transaction objects across async file reads.

Proof matrix:

RiskRequired proof
Transform lifecycleUnit tests prove deleteBackward, deleteForward, insertBreak, insertText, insertFragment, and node transforms receive update-local tx; default next shares the same transaction.
Query recursionUnit tests prove next continues current query, state adjacent reads work, and mutation from query/generator cleanup still throws.
Clipboard async boundaryClipboard tests prove sync writes use fresh editor.update; async examples do not capture tx.
Type erasureNegative type tests prove disabled extensions are excluded and non-installed getApi fails.
ExamplesChecklists, tables, images, forced-layout, and markdown examples use inline lifecycle objects without helper bloat.
Public surfacePublic-surface tests reject legacy extension methods, extension commands, public capabilities, and helper namespaces that compete with editor.api / state / tx.

Rollback/hard-cut answer:

  • No compatibility aliases. If implementation proves tx transform middleware cannot be made coherent, drop tx from transforms and keep { editor, next } temporarily; do not ship a fake state compromise.

Implementation Phases

  1. Transform dispatch: make transform middleware execute inside the active update transaction and pass tx.
  2. Query context: pass state to query middleware and test recursion/mutation rules.
  3. Clipboard context: pass state, keep writes through editor.update, and update async examples.
  4. Type surface: add negative type tests for context availability, disabled extensions, latest-name wins, and getApi.
  5. Examples: rewrite checklists, tables, images, and forced-layout with inline lifecycle logic.
  6. Hard-cut guard: keep old methods, commands, public capabilities, and Editor/DOMEditor/HistoryEditor helper alternatives out of public docs/examples.

Verification Gates

Planning artifact:

txt
node tooling/scripts/completion-check.mjs

Implementation gates for a later ralph run:

txt
cd .tmp/slate-v2 && bun test ./packages/slate/test/extension-methods-contract.ts ./packages/slate/test/query-extension-contract.ts ./packages/slate/test/generic-extension-namespace-contract.ts ./packages/slate/test/generic-extension-install-contract.ts ./packages/slate/test/normalization-contract.ts
cd .tmp/slate-v2 && bun test ./packages/slate/test/clipboard-contract.ts ./packages/slate-dom/test/clipboard-boundary.ts
cd .tmp/slate-v2 && bun --filter slate typecheck
cd .tmp/slate-v2 && bun --filter slate-dom typecheck

Pass State Ledger

PassStatusEvidenceNext
Activation resetcompleteactive goal state rewritten to this plannone
Current-state readcompletelive .tmp/slate-v2 source, examples, tests, compiled researchnone
Related issue discoverycompletelive, sync, coverage, and dossier rows read for #3222, #4089, #4181, #3177, #4721, #5233, #4569, #1024, #2405, #2288, #1770, #3874, #5080, #5684none
Issue ledger synccompleteno global ledger edit required; all rows are related, not claimed, or already claimed elsewherenone
Decision brief pressure passcompletelifecycle-specific context accepted; universal state, universal tx, root editor.state, and editor-only contexts rejectednone
Ecosystem passcompleteLexical, Tiptap, and ProseMirror compiled evidence mapped to decisionsnone
Slate maintainer objection passcompleteobjection ledger addednone
High-risk deliberate modecompletepre-mortem and proof matrix addednone
Type/API proof passcompleteexact test files mappednone
Example/DX passcompleteexact example update targets listednone
Closure gatecompletescore raised to 0.93; remaining risks converted to implementation gatesnone

Final Score

DimensionScoreWhy
Slate-close DX0.92Lifecycle context removes repeated editor.read without adding root shortcuts or Plate-shaped product APIs.
Architecture coherence0.94Read callbacks get state, update callbacks get tx, post-commit callbacks get commit data, registration gets editor.
Regression safety0.89Transform routing remains the main risk, but it is an explicit first implementation gate with exact tests.
Migration backbone0.92Extension APIs, typed groups, latest-name-wins, enabled: false, clipboard, history, and operation boundaries are classified.
Research support0.94Lexical, ProseMirror, Tiptap, live Slate v2 source, and issue corpus all point to lifecycle-specific context.
Example quality0.91Checklists, tables, images, and normalizers have direct target snippets and proof files.

Overall: 0.93 ready.

Final Handoff

Ready for ralph execution when the user asks to build.

  • queries: revise to { state, editor, next, ...args }; next remains the current-query continuation.
  • transforms: revise to { tx, editor, next, ...args }; first implementation step must make transform middleware transaction-local.
  • normalizers: keep restricted tx.
  • clipboard: revise to { state, editor, next }; no tx.
  • operationMiddlewares: keep { editor, operation }, next.
  • commitListeners: keep (commit, snapshot).
  • register: keep { editor, name, options, runtimeState, signal }.
  • api: keep editor.api and editor.getApi(extension).
  • state root shortcut: cut.
  • Editor / DOMEditor / HistoryEditor public alternatives: cut from the target public path.
  • Issue accounting: no new fixed issue claims; all related rows are classified.

Ralph Execution Grounding

Task statement: implement the accepted extension lifecycle context API in .tmp/slate-v2.

Desired outcome:

  • Query middleware receives state.
  • Transform middleware receives transaction-local tx.
  • Clipboard middleware receives state and never receives tx.
  • Normalizer restricted tx stays intact.
  • Public alternatives remain cut.
  • Examples and typed tests prove the new shape.

Known facts:

  • tx is built from state plus write groups in .tmp/slate-v2/packages/slate/src/core/public-state.ts.
  • Current transform middleware is registered in .tmp/slate-v2/packages/slate/src/core/editor-extension.ts and dispatched through .tmp/slate-v2/packages/slate/src/core/transform-middleware.ts.
  • Current command execution only starts an implicit update when executeCommand(..., { implicitUpdate: true }) is used.
  • Current query middleware already rejects editor.update from query execution.
  • Current clipboard examples may cross async FileReader boundaries.

Constraints:

  • Use vertical TDD slices.
  • Keep example logic inline when used once.
  • Prefer TypeScript inference, satisfies, and type guards over casts.
  • Do not add compatibility aliases.
  • Do not rerun related-issue discovery unless the issue surface or claim set changes.

Likely touchpoints:

  • .tmp/slate-v2/packages/slate/src/interfaces/editor.ts
  • .tmp/slate-v2/packages/slate/src/core/editor-extension.ts
  • .tmp/slate-v2/packages/slate/src/core/transform-middleware.ts
  • .tmp/slate-v2/packages/slate/src/core/query-middleware.ts
  • .tmp/slate-v2/packages/slate/src/core/command-registry.ts
  • .tmp/slate-v2/packages/slate/test/extension-methods-contract.ts
  • .tmp/slate-v2/packages/slate/test/query-extension-contract.ts
  • .tmp/slate-v2/packages/slate/test/generic-extension-namespace-contract.ts
  • .tmp/slate-v2/site/examples/ts/check-lists.tsx
  • .tmp/slate-v2/site/examples/ts/tables.tsx
  • .tmp/slate-v2/site/examples/ts/images.tsx

Ralph Execution Ledger

TimePassStatusEvidenceNext
2026-05-18T03:07:10Zralph activationcompleteactive goal state set to pending; continuation prompt written.tdd-pass
2026-05-18T03:07:10Ztdd-passcompleteRED: bun test ./packages/slate/test/extension-methods-contract.ts failed because tx was undefined in transform middleware. GREEN: active update view threaded into transform middleware and the focused test passed.implementation slice
2026-05-18T03:13:00Zimplementation-slicecompleteQuery middleware now receives state; transform middleware receives transaction-local tx; clipboard middleware receives read-only state without tx; negative type tests cover all three contexts.Example and docs sync.
2026-05-18T03:23:18Zexample-doc-synccompletecheck-lists, tables, markdown-shortcuts, inlines, and richtext extension transforms use tx; Editable docs show transform middleware with tx; surface contract updated to enforce that shape.Final gates.
2026-05-18T03:23:18Zfinal-gatescompletebun lint:fix; bun test ./packages/slate/test/extension-methods-contract.ts; bun test ./packages/slate/test/query-extension-contract.ts; bun --filter slate typecheck; bun --filter slate-dom typecheck; bun --filter slate-react typecheck; bun typecheck:site; bun test ./packages/slate/test/clipboard-contract.ts; bun test ./packages/slate-dom/test/clipboard-boundary.ts; cd packages/slate-react && bun test:vitest -- test/surface-contract.test.tsx; bun check.Mark completion done.