Skip to content

[lexical-y] Documentation Update: Decision register and agent guide for @lexical/y - #9594

Merged
zurfyx merged 3 commits into
mainfrom
zurfyx/y-decisions-register
Oct 30, 2026
Merged

zurfyx merged 3 commits into
mainfrom
zurfyx/y-decisions-register

Conversation

@zurfyx

@zurfyx zurfyx commented Oct 30, 2026

Copy link
Copy Markdown
Member

Description

The reasons behind @lexical/y's design live in merged pull request bodies, issues and review threads, so a later change or review has no single place to check what was decided and why, and decided questions get argued again.

This adds two files to packages/lexical-y:

  • DECISIONS.md: a decision register, one row per decision with the decision, why, a status (DECIDED, ACCEPTED LIMIT or OPEN), the date and links to the pull requests, issues and docs that carry it. It is backfilled with 73 rows from the work on [lexical][lexical-devtools][lexical-extension][lexical-list][lexical-website][lexical-y][lexical-yjs] Feature: @lexical/y, an experimental Yjs 14 collaboration binding #9400 and the pull requests on top of it: compatibility and API design (zero breaking changes, AI-first rules, typed warnings, required createYDoc and schemaVersion, the layout gate), the storage model (flat layout, declared node structure, only a client's own edits are written, seeding, undo, moves, text identity), block ids and handles, marks and comments, server-side editing, the accepted limits and the open questions.
  • AGENTS.md, with a CLAUDE.md that includes it (the root's convention): read the register before changing the package or reviewing a pull request to it, do not re-open a decided row without new evidence, add a row in the same pull request as a new decision, and the package's testing and merge rules in brief.

Neither file is published: the package's files list includes only dist, src, README.md and LICENSE.

Changeset

None: documentation only, nothing in the published package changes.

Test plan

Before

packages/lexical-y had no decision register and no package-level agent guide.

After

$ pnpm run ci-check
...
Changeset check passed: 31 pending changeset(s) valid.
...
Found 0 errors

prettier --list-different . passes (Markdown is in .prettierignore). Every relative link in both files resolves to a file in the repository, and every row was checked against its source.

…or @lexical/y

## Description

The reasons behind `@lexical/y`'s design live in merged pull request bodies, issues and review threads, so a later change or review has no single place to check what was decided and why, and decided questions get argued again.

This adds two files to `packages/lexical-y`:

- `DECISIONS.md`: a decision register, one row per decision with the decision, why, a status (`DECIDED`, `ACCEPTED LIMIT` or `OPEN`), the date and links to the pull requests, issues and docs that carry it. It is backfilled with 73 rows from the work on #9400 and the pull requests on top of it: compatibility and API design (zero breaking changes, AI-first rules, typed warnings, required `createYDoc` and `schemaVersion`, the layout gate), the storage model (flat layout, declared node structure, only a client's own edits are written, seeding, undo, moves, text identity), block ids and handles, marks and comments, server-side editing, the accepted limits and the open questions.
- `AGENTS.md`, with a `CLAUDE.md` that includes it (the root's convention): read the register before changing the package or reviewing a pull request to it, do not re-open a decided row without new evidence, add a row in the same pull request as a new decision, and the package's testing and merge rules in brief.

Neither file is published: the package's `files` list includes only `dist`, `src`, `README.md` and `LICENSE`.

## Changeset

None: documentation only, nothing in the published package changes.

## Test plan

### Before

`packages/lexical-y` had no decision register and no package-level agent guide.

### After

```
$ pnpm run ci-check
...
Changeset check passed: 31 pending changeset(s) valid.
...
Found 0 errors
```

`prettier --list-different .` passes (Markdown is in `.prettierignore`). Every relative link in both files resolves to a file in the repository, and every row was checked against its source.
@vercel

vercel Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated
lexical Ready Ready Preview Oct 30, 2026 5:49pm UTC
lexical-playground Ready Ready Preview Oct 30, 2026 5:49pm UTC

Request Review

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Oct 30, 2026
@meta-code-review

meta-code-review Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

AI review status for this pull request.

Review status Updated
🔄 In progress (for maintainers only) Oct 30, 2026, 5:50 PM UTC

@pkg-pr-new

pkg-pr-new Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
lexical

npm i https://pkg.pr.new/lexical@9594

@lexical/a11y

npm i https://pkg.pr.new/@lexical/a11y@9594

@lexical/clipboard

npm i https://pkg.pr.new/@lexical/clipboard@9594

@lexical/code

npm i https://pkg.pr.new/@lexical/code@9594

@lexical/code-core

npm i https://pkg.pr.new/@lexical/code-core@9594

@lexical/code-prism

npm i https://pkg.pr.new/@lexical/code-prism@9594

@lexical/code-shiki

npm i https://pkg.pr.new/@lexical/code-shiki@9594

@lexical/compiler

npm i https://pkg.pr.new/@lexical/compiler@9594

@lexical/devtools-core

npm i https://pkg.pr.new/@lexical/devtools-core@9594

@lexical/dragon

npm i https://pkg.pr.new/@lexical/dragon@9594

@lexical/eslint-plugin

npm i https://pkg.pr.new/@lexical/eslint-plugin@9594

@lexical/extension

npm i https://pkg.pr.new/@lexical/extension@9594

@lexical/fast-check

npm i https://pkg.pr.new/@lexical/fast-check@9594

@lexical/file

npm i https://pkg.pr.new/@lexical/file@9594

@lexical/hashtag

npm i https://pkg.pr.new/@lexical/hashtag@9594

@lexical/headless

npm i https://pkg.pr.new/@lexical/headless@9594

@lexical/history

npm i https://pkg.pr.new/@lexical/history@9594

@lexical/html

npm i https://pkg.pr.new/@lexical/html@9594

@lexical/internal

npm i https://pkg.pr.new/@lexical/internal@9594

@lexical/link

npm i https://pkg.pr.new/@lexical/link@9594

@lexical/list

npm i https://pkg.pr.new/@lexical/list@9594

@lexical/mark

npm i https://pkg.pr.new/@lexical/mark@9594

@lexical/markdown

npm i https://pkg.pr.new/@lexical/markdown@9594

@lexical/mdast

npm i https://pkg.pr.new/@lexical/mdast@9594

@lexical/offset

npm i https://pkg.pr.new/@lexical/offset@9594

@lexical/overflow

npm i https://pkg.pr.new/@lexical/overflow@9594

@lexical/plain-text

npm i https://pkg.pr.new/@lexical/plain-text@9594

@lexical/react

npm i https://pkg.pr.new/@lexical/react@9594

@lexical/rich-text

npm i https://pkg.pr.new/@lexical/rich-text@9594

@lexical/selection

npm i https://pkg.pr.new/@lexical/selection@9594

@lexical/table

npm i https://pkg.pr.new/@lexical/table@9594

@lexical/tailwind

npm i https://pkg.pr.new/@lexical/tailwind@9594

@lexical/text

npm i https://pkg.pr.new/@lexical/text@9594

@lexical/utils

npm i https://pkg.pr.new/@lexical/utils@9594

@lexical/y

npm i https://pkg.pr.new/@lexical/y@9594

@lexical/yjs

npm i https://pkg.pr.new/@lexical/yjs@9594

commit: 43308c2

@meta-code-review meta-code-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This looks good: one register for @lexical/y's decisions, with the reason and sources beside each, will save later changes and reviews from re-arguing settled questions, and nothing published changes (the package's files list leaves all three files out). The relative links and y.md anchors resolve, and rows such as D13, D25 and D44 match the code on main. The suggestions below are optional and non-blocking.

1. Rows that depend on open pull requests (packages/lexical-y/DECISIONS.md)

The package AGENTS.md asks readers to treat DECIDED rows as settled (lines 10-12) and to check a claimed limit against main (lines 28-30), but a few rows record outcomes that are not on main yet:

  • D29 (line 63) links #9565, which was closed in favour of #9598; on main, y.md's "Edits made by code" still says text made with other text "is new where it is put".
  • D36 (line 78), and D34's reason that handles fix the token cost (line 76), depend on #9531, so main has no handle API yet.
  • D51 and D52 (lines 109-110) come from #9562, and the y.md Limits sentence for them from #9569, so they are not yet "Documented in Limits" as the legend (line 18) and AGENTS.md (lines 15-16) say. #9562 also notes that D52's own-run case does not occur on main, and tracks its repair as #9589, which D52 could link.
  • AGENTS.md (lines 20-24) points at src/__tests__/catalog/, which #9588 adds.

Marking those rows with the pull request they land with, or merging this after them, would keep the register accurate in between.

2. D06 and D28 (lines 35 and 62)

D06 rules out collaboration vocabulary and collab-only behaviour in lexical, while D28 records that core records text edits while a binding is attached, which is what LexicalEditor._recordTextEdits does (its doc comment says the Yjs 14 binding enables it). Scoping D06 to what was decided, for example no public collaboration API or vocabulary in core while @internal hooks that a binding switches on are fine, would keep two DECIDED rows from contradicting each other.

3. Nit (AGENTS.md lines 37-38)

Naming the code-review bot whose review should be resolved would help, since pull requests here also carry other bots' comments.

| D03 | **AI-first API design.** Each rule sits as high as it can go: type, then lint, then test, then a throw (for untrusted input only), then a warning, then docs. Choices are explicit: a required parameter may be `null`, as `editYState`'s `writer` is. | Most code that uses the library is written by agents, which read types and error data, not prose. | `DECIDED` | 10-28 | [#9437], [#9510] |
| D04 | **Typed warnings.** Core has one generic `LexicalWarning` (`code`, `data`); `@lexical/y` owns its codes (`YWarning`, `isYWarning`, `data.reason`). Messages are short and factual, with no product-facing strings; apps render their own banners from `code` and `data`. | An agent acts on a precise signal, and a product will not show a library's name or wording in its UI. | `DECIDED` | 10-28 | [#9400], [y.md: Warnings][y-warnings] |
| D05 | **Warnings go through the editor's `onWarn`, after the Yjs transaction.** The binding queues them and passes them on once Yjs is idle; a throwing handler rethrows in a microtask. There is no separate `onInfo`. | Lexical's default `onWarn` throws in development, and a throw inside a Yjs observer would abort a remote update halfway. | `DECIDED` | 10-23 | [#9400] |
| D06 | **Core stays free of collaboration.** No collaboration, Yjs or binding vocabulary, API or collab-only behaviour in `lexical` or any package other than `@lexical/yjs` and `@lexical/y`; the playground and the collaboration docs may mention it. | Core concepts must make sense without a binding, and a binding must not leak its model into every app. | `DECIDED` | 10-23 | [#9400], [#9452] |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Optional: as written this contradicts D28 (line 62). Core records text edits while a binding is attached: LexicalEditor._recordTextEdits is documented as enabled by the Yjs 14 binding, next to _recordNodeMoves for the v1 binding, and core exports COLLABORATION_TAG. Scoping D06 to what was decided (for example, no public collaboration API or vocabulary in core, while @internal hooks a binding switches on are allowed) would keep the two rows consistent.

| D26 | **Undo favours people taking turns over concurrent races.** Where a fix makes undo correct for sequential edits at the cost of rare races between an undo and a simultaneous edit of the same text, the sequential case wins. | Taking turns is the common case; a race with a simultaneous edit of the same text is rare. | `DECIDED` | 10-28 | [#9400] |
| D27 | **Moves are a delete and a re-insert** at launch; the binding rewrites the cheaper side. Real moves within a container ([#9449], records after Yjs's unmerged `ContentMove` design) are paused and land last, backward compatible: a new layout version that older clients open read only. Move records leave tombstones, as every Yjs binding does. Moves across containers stay copies. | Every Yjs binding turns moves into delete plus insert; Yjs dropped its own move for complexity. Reliability of the rest came first. | `DECIDED` | 10-25 | [#9449], [#9455], [y.md: Limits][limits] |
| D28 | **Text identity is fixed at the source.** While a binding is attached, core records text edits (`spliceText`, insert and remove, split and merge), and text entity plugins record a grow as a move and a remake as a retype. Text only `setTextContent` or the DOM changed is diffed. The binding adds no per-case heuristics. | No binding can recover intent an API dropped; special-case rules were fragile and regressed neighbouring cases. | `DECIDED` | 10-27 | [#9483], [y.md: Edits made by code][y-code] |
| D29 | **Remade text keeps its common start and end by character**, whatever the node boundaries (y-prosemirror's `updateYText` rule), and never brings back a collaborator's delete. It replaces the word matching of [#9565]. | It is the industry-standard behaviour, and word matching glued words together and merged numbers oddly. | `DECIDED` | 10-30 | [#9565] |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Optional: #9565 was closed on 2026-10-30 in favour of #9598, which carries this decision, so this row could link #9598 and say it lands there. On main, y.md's "Edits made by code" still says text made with other text "is new where it is put".

| D33 | **Lexical owns block ids** (`BlockIdExtension`, `@lexical/extension`), stored as NodeState `blockId`, not as Yjs identity. | Yjs ids do not survive a split, a reseed, JSON or HTML, or paste. Other editors store their own block ids too. Two implementations would drift. | `DECIDED` | 10-23 | [#9421], [block ids][block-ids] |
| D34 | **New blocks get 21 random URL-safe characters (126 bits).** A collision is repaired deterministically: the block that held the id keeps it. Re-examined on 10-29 for shorter ids: kept, since handles (D36) fix the token cost. | Unique without coordination between peers that may be offline or headless; a mistyped id matches nothing. Shorter stored ids would weaken that and change stored formats. | `DECIDED` | 10-29 | [#9421] |
| D35 | **A document stored without ids gets positional ids**, the same on every reader. The derivation is part of the stored format. | Two reads of the same document must name blocks the same way, or an agent's edit points at nothing. | `DECIDED` | 10-22 | [#9421] |
| D36 | **Agents see handles, not ids.** A handle is the shortest prefix, at least 4 characters, of a 64-bit hash of the id that is unique in the document; it works for any id format. Resolving one returns a typed status. HTML export gets a separate `handles` option; `export` stays a boolean as shipped. | About 4 tokens instead of 14, and nothing stored. A typo almost always matches nothing (about 2.5 in 100,000 hit another block), where sequential aliases (`b1`, `b2`) would edit the neighbour. Resolve an agent's reply against the snapshot it read (`{read}`), so a deleted block's handle cannot name a new one. Hashing keeps handles short for derived and time-ordered ids. | `DECIDED` | 10-30 | [#9531] |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Optional: handles come from #9531, which is still open, so main has no handle API yet, and D34 (line 76) gives handles as its reason. Noting that this row lands with #9531 would keep it accurate until then.

Comment thread packages/lexical-y/DECISIONS.md Outdated
Comment on lines +109 to +110
| D51 | **Rare undo and marker-cleanup timings can spread a comment or format onto untouched words** (two writes of one client both undone while a collaborator collapses markers; three authors in [#9575]). | Rare, no text lost. A provenance marker would need a layout version bump and about a week. | `ACCEPTED LIMIT` | 10-30 | [#9562], [#9575] |
| D52 | **Unbolding inside your own bold while a collaborator bolds text starting at its end** can leave the new text plain (when the unbolding client's id is higher). | Rare and cosmetic, no text lost; the fix needs a follow-up repair. | `ACCEPTED LIMIT` | 10-30 | [#9562] |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Optional: both limits come from #9562, and their y.md Limits sentence from #9569, both still open, so they are not yet "Documented in Limits" as the legend (line 18) says. #9562 also notes that D52's own-run case does not occur on main and tracks its repair as #9589, which D52 could link.

## Testing

- Test the input space, not the cases you thought of. Draw inputs from the
catalogs in `src/__tests__/catalog/` (node shapes, block id formats,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Optional: src/__tests__/catalog/ is added by #9588, which is still open, so the directory does not exist on main yet. Merging this after #9588 keeps the instruction followable.


## Before a pull request is ready, and before merging

- Read the automated code-review bot's review on the pull request and resolve

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Nit: naming the bot account would help: pull requests here also carry other bots' comments, and one of them links its own review agent.

…ve decision rows

## Description

Five rows of the `@lexical/y` decision register rested on recommendations that had not been explicitly confirmed. The maintainer confirmed them:

- D14: the binding is coupled to one Yjs version on purpose. `@y/y` is an exact peer dependency, bumped deliberately with the equivalence test, so a product cannot bump Yjs by accident; a runtime guard on the version is a follow-up.
- D25: undo keeps a container a collaborator has put content in (the `deleteFilter`).
- D26: undo favours people taking turns over rare concurrent races.
- D52: the unbold and adjacent bold race is an accepted limit; its repair is tracked in #9589.
- D57: lone surrogates stay as they are, with no code follow-up.

## Changeset

None: documentation only.

## Test plan

### Before

The five rows carried earlier dates and no record of the confirmation.

### After

```
$ pnpm run ci-check
...
Changeset check passed: 31 pending changeset(s) valid.
...
Found 0 errors
```
…ote the v1/v2 fixes merged and closed

## Description

Two rows of the `@lexical/y` decision register did not match the code or the pull request history:

- D72 said the binding's first read is not deferred until every extension registers. The binding starts in `YCollabExtension`'s `afterRegistration`, once every extension has registered (the `TableOnOpen` test comment #9537 corrects says the same). The row now says that, and that the decision is no further deferral or special-casing: an initialization-order bug is fixed in the extension that has it, as #9501 did.
- D12 (no more fixes to `@lexical/yjs` v1 and v2) now notes that one v2 table fix, #9584, merged on 10-30 before the row was checked, and that later v1 and v2 fix pull requests, #9595 and #9596, were closed under it.

## Changeset

None: documentation only.

## Test plan

### Before

D72 contradicted `packages/lexical-y/src/internal/YExtension.ts`, which starts the binding in `afterRegistration`.

### After

```
$ pnpm run ci-check
...
Changeset check passed: 31 pending changeset(s) valid.
...
Found 0 errors
```
@zurfyx
zurfyx added this pull request to the merge queue Oct 30, 2026
Merged via the queue into main with commit 110493b Oct 30, 2026
53 checks passed

This branch was successfully deployed

2 active deployments
Preview – lexical — 43308c2b Deployed Oct 30, 2026 by vercel[bot]
Preview – lexical-playground — 43308c2b Deployed Oct 30, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant