Repository navigation
[lexical-y] Documentation Update: Decision register and agent guide for @lexical/y - #9594
Conversation
…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.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
AI review status for this pull request.
|
lexical
@lexical/a11y
@lexical/clipboard
@lexical/code
@lexical/code-core
@lexical/code-prism
@lexical/code-shiki
@lexical/compiler
@lexical/devtools-core
@lexical/dragon
@lexical/eslint-plugin
@lexical/extension
@lexical/fast-check
@lexical/file
@lexical/hashtag
@lexical/headless
@lexical/history
@lexical/html
@lexical/internal
@lexical/link
@lexical/list
@lexical/mark
@lexical/markdown
@lexical/mdast
@lexical/offset
@lexical/overflow
@lexical/plain-text
@lexical/react
@lexical/rich-text
@lexical/selection
@lexical/table
@lexical/tailwind
@lexical/text
@lexical/utils
@lexical/y
@lexical/yjs
commit: |
There was a problem hiding this comment.
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
mainhas no handle API yet. - D51 and D52 (lines 109-110) come from #9562, and the
y.mdLimits sentence for them from #9569, so they are not yet "Documented in Limits" as the legend (line 18) andAGENTS.md(lines 15-16) say. #9562 also notes that D52's own-run case does not occur onmain, and tracks its repair as #9589, which D52 could link. AGENTS.md(lines 20-24) points atsrc/__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] | |
There was a problem hiding this comment.
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] | |
| | 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] | |
| | 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] | |
There was a problem hiding this comment.
| ## 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, |
|
|
||
| ## Before a pull request is ready, and before merging | ||
|
|
||
| - Read the automated code-review bot's review on the pull request and resolve |
There was a problem hiding this comment.
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 ```
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 LIMITorOPEN), 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, requiredcreateYDocandschemaVersion, 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 aCLAUDE.mdthat 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
fileslist includes onlydist,src,README.mdandLICENSE.Changeset
None: documentation only, nothing in the published package changes.
Test plan
Before
packages/lexical-yhad no decision register and no package-level agent guide.After
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.