Skip to content

build: first round of blast radius#2833

Open
nperez0111 wants to merge 3 commits into
special-nodefrom
blast-radius
Open

build: first round of blast radius#2833
nperez0111 wants to merge 3 commits into
special-nodefrom
blast-radius

Conversation

@nperez0111
Copy link
Copy Markdown
Contributor

@nperez0111 nperez0111 commented Jun 4, 2026

Summary

Audits the blast radius of the blockContainer content-spec change and annotates
every affected (and notably-safe) site in the codebase, plus a written summary of
the conceptual fallout.

The change

packages/core/src/pm-nodes/BlockContainer.ts:27

- content: "blockContent blockGroup?"
+ content: "suggestionBlockContent* blockContent suggestionBlockContent* blockGroup?"

A blockContainer can now hold 0-N suggestionBlockContent shadow nodes both
before and after its blockContent. These hold the old content of a
block with a pending "modify" suggestion.

Rationale

This is not a localised change: it changes what a "block" structurally contains,
which ripples through position arithmetic, conversion/serialization, the public
Block API, collaboration, and every menu that mutates blocks. This PR maps that
surface so the follow-up implementation work is scoped, rather than discovered
piecemeal.

Changes

  • Inline single-line annotations at affected position/structure sites across all
    packages (firstChild, nodeAt(pos+1), childCount, nodeBefore/After,
    compound-group equality checks, etc.).
  • Three root-cause counter comments for the systemic suggestion-blind layers:
    • nodeToBlock.ts:485(Affects ~23 callsites) strips suggestion state.
    • blockToNode.ts:324(Affects ~7 callsites) emits no suggestion nodes.
    • getBlockInfoFromPos.ts:44(Affects ~58 callsites) callers never branch
      on suggestionBefore/suggestionAfter.
  • packages/core/SUGGESTIONS_BLAST_RADIUS.md documenting the considerations below.

Considerations (blast radius)

Two distinct suggestion mechanisms

  1. Block-level marksy-attributed-insert / -format / -delete, allowed
    on blockContainer, blockGroup, column, table nodes. Mark a real block /
    structure as inserted, deleted, or modified.
  2. Shadow nodes${type}--attributed (e.g. paragraph--attributed), group
    is the COMPOUND string "suggestionBlockContent blockContent". They sit inside
    a blockContainer next to blockContent and hold the old content.

Footguns

  • Compound group string. node.type.spec.group === "blockContent" and
    === "suggestionBlockContent" are BOTH false for a shadow node. Use
    node.type.isInGroup(...). Several existing guards (splitBlock.ts:50,
    internal.ts:104, nodeToBlock.ts:608) are dead because of strict-equality or
    a "suggestion-" name prefix the real *--attributed nodes never match.
  • Naming. Real nodes are *--attributed, not suggestion-*.
    SpecialNode.test.ts asserts the non-existent suggestion-* names.
  • firstChild is no longer blockContent. Reading blockContainer.firstChild,
    nodeAt(pos+1), or childCount === 1 now risks landing on / miscounting a
    leading shadow node.
  • Editable shadow content. Shadow nodes are selectable: false but render with
    an editable contentDOM and have NO node view, so applyNonSelectableBlockFix
    never runs. A caret / text-selection can land inside a shadow node, so those
    positions are genuinely reachable.

What is inside editor.document?

Produced by nodeToBlock, which builds Block = {id, type, props, content, children} only and strips all suggestion state (shadow nodes dropped,
y-attributed-* marks ignored). So block.content is always the new content;
the old content is invisible; the public Block type has no suggestion-state
field; and every consumer (getSelection().blocks, getBlock, all ExportManager
APIs) is suggestion-blind.

What does a "deleted" block mean?

A suggested-deleted block still has its data-id, is in the bnBlock group, and
resolves through getNodeById / getBlock / forEachBlock as a normal live
block with no deletion flag. Menus and commands will read and mutate content
the user believes is deleted. This is the most acute hazard.

How do menu items interact with this?

Every side-menu / formatting-toolbar / table-handle action resolves a
suggestion-blind Block and mutates it via updateBlock / removeBlocks /
insertBlocks, none suggestion-aware. Type/color/alignment/file ops on a
suggested block are untracked edits that corrupt the diff; "+" inserts an
untracked block next to tracked content; drag-and-drop moves a suggested block and
deleteSelections the origin untracked; BlockTypeSelect shows the new content's
type, never reflecting the tracked change. These should be disabled or routed
through accept/reject semantics when suggestions are enabled.

How would the unique-id extension rewrite ids?

Shadow nodes carry no own data-id (it lives on the outer blockContainer), so
they should not be assigned ids. But dedupe (UniqueID.ts:208) can regenerate an
id when a suggestion transiently produces a duplicate, potentially detaching a
block from its tracked history. Dedupe must ignore shadow-bearing transitions.

Multi-column

  • column content is "blockContainer+" and carries y-attributed-* marks, so a
    whole column can be a tracked insert/delete.
  • multiColumnDropCursor.ts / ColumnResizeExtension.ts work at column/columnList
    level (shadow-safe) but resolve via the suggestion-blind getNodeById.
  • multiColumnHandleDropPlugin.ts rebuilds an entire columnList from
    nodeToBlock output, dropping any suggestion state in moved columns — the
    highest-risk multi-column path.

Round-trips and serialization

  • blockToNode emits only [contentNode, groupNode], so any Block-JSON round-trip
    drops suggestions.
  • Clipboard copy uses ProseMirror's native serializer on the raw slice, which
    does emit shadow nodes — so copy/paste can duplicate/inject shadow content, and
    the text/html / markdown branches (Block-JSON based) disagree with the
    blocknote/html branch about whether suggestions exist.

Schema-validity hazards

  • updateBlock inserts a blockGroup at blockContent.afterPos, i.e. before a
    trailing shadow node, violating the ... blockGroup? ordering.
  • mergeBlocks is partly suggestion-aware but drops prev.suggestionBefore and
    next.suggestionAfter, and reconstructs with type.create (no validation).
  • BlockInfo.suggestionBefore / suggestionAfter are singular while the schema
    allows *; containers with ≥2 shadows on a side are under-represented.

Impact

This PR is annotation + documentation only (the code diff is comment-only; the only
new file is the markdown summary). No runtime behaviour changes.

Testing

N/A — no behavioural changes. Existing tests unaffected. The audit flags
SpecialNode.test.ts as a separate test-correctness issue (asserts non-existent
suggestion-* node names) to be addressed in follow-up work.

Checklist

  • Code follows the project's coding standards.
  • Unit tests covering the new feature have been added.
  • All existing tests pass.
  • The documentation has been updated to reflect the new feature

Additional Notes

Search the source for the (Affects ~N callsites) counters to find the systemic
entry points, and for the single-line // ... suggestion ... annotations for the
individual sites. Full write-up lives in
packages/core/SUGGESTIONS_BLAST_RADIUS.md.

@vercel
Copy link
Copy Markdown

vercel Bot commented Jun 4, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
blocknote Ready Ready Preview Jun 4, 2026 3:06pm
blocknote-website Error Error Jun 4, 2026 3:06pm

Request Review

}

// Save suggestion node content before reconstruction
// drops prev suggestionBefore and next suggestionAfter on merge
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

part of the previous fixes, mergeBlocks needs to be updated for suggestions anyway, so still 1

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jun 4, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2aa7377d-b88a-48d8-b5ae-801d16afaa20

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch blast-radius

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment thread packages/xl-multi-column/src/extensions/DropCursor/multiColumnHandleDropPlugin.ts Outdated
Comment thread packages/xl-multi-column/src/extensions/DropCursor/multiColumnHandleDropPlugin.ts Outdated
Comment thread packages/xl-multi-column/src/extensions/DropCursor/multiColumnHandleDropPlugin.ts Outdated
Add root-cause counter at getBlockInfoFromPos (suggestionBefore/After
unused by callers), census-pass position annotations, comment cleanup,
and a SUGGESTIONS_BLAST_RADIUS.md covering the conceptual fallout.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant