Drop empty paragraphs from canonical document projection #212
- LA
Closes #211
## Problem
An empty Lexical paragraph was exported as an MDAST `paragraph` with no children. `serialize` writes that as an extra blank line, and `parse` discards it. Pressing Enter at the end of a document, or inserting a `/research` card, was enough to make the room persist source that fails its own round trip. The `document-summary` job then failed with "Document source is not canonical." under every harness.
## Change
- `packages/dialect/src/convert.ts`: `$exportPlanTree` drops empty paragraphs at every depth, so projection is canonical. Block addressing already ignored them: `addressable()` skips root-level empty paragraphs and `digests()` goes through `parse`. The Lexical tree and browser editing are unchanged; only the server projection differs. Table cells and a container left with only an empty paragraph export exactly as before. - `apps/server/src/plan/room.ts`: existing checkpoints still store the old blank lines, and `restore()` required the stored source to match the projection exactly. Without a change, those channels would fail to open with "stored plan source does not match its Yjs checkpoint". `restore()` now also accepts a stored source whose canonical form, `serialize(parse(source))`, matches the projection. An unrelated source is still rejected. The next checkpoint rewrites the source in canonical form.
Existing documents keep failing description jobs until their next edit re-projects them.
## Verification
New regression tests cover export at the root, after a Research card, between blocks and inside a blockquote, plus restoring a legacy checkpoint. Both failed before the fix.
Live check with agent-browser against an `AGENT=off` server: I typed text and pressed Enter twice, then read the stored snapshot.
| Checkout | Stored source | `serialize(parse(s)) === s` | |---|---|---| | `main` | `"The harp needs new strings.\n\n\n\n"` | false | | this branch | `"The harp needs new strings.\n"` | true |
A document written by `main` opened on this branch without errors, accepted an edit, and was saved in canonical form (`"…strings.\n\nOrder a full set.\n"`):

The first run of `bun run e2e` failed one test, `responsive-decisions.e2e.ts` "long independent decisions…", on a strict-mode locator. That failure also happens on `main`: cold-start runs of that test failed 2 of 3 times on `main` and 3 of 3 on this branch, so it is an existing flake. The rerun of the full suite passed.
Assistant-verification: bun test unit passed: 1659 pass, 2 skip, 0 fail; new convert and room regression tests failed before the fix Assistant-verification: bun run test:postgres passed: 59 PostgreSQL contract tests Assistant-verification: bun run types passed: all workspaces and E2E Assistant-verification: bun run ci passed: dprint, oxlint, tokens, type scale, Impeccable Assistant-verification: bun run e2e passed: 236 Chromium tests on rerun; the one earlier failure is an existing cold-start flake that also fails on main Assistant-verification: agent-browser E2E passed: text plus two Enters stores canonical source on this branch versus non-canonical on main; a main-written document opens, edits, and re-persists canonically Assistant-model: Claude Opus 5.5 Assistant-workflow: inline Assistant-duration: 16m converged, estimated none given Co-authored-by: Alex Lavaee <lavaman131@github.com>
lavaman131assigned lavaman131last month on GitHub
lavaman131head-ref-force-pushed 908f1b773fdalast month on GitHub
MaggieAppletonreferenced 8c74c164075dlast month on GitHub
- MA
No review summary.
lavaman131head-ref-force-pushed e1fc8c359de2last month on GitHub
- MA
all good
MaggieAppletonhead-ref-force-pushed cd0242ecbcfblast month on GitHub
MaggieAppletonmerged bae781859789last month on GitHub
MaggieAppletonhead-ref-deletedlast month on GitHub
MaggieAppletonreferenced this in Add an Atomic SDK harness for the hosted Planner #210last month on GitHub
lavaman131referenced adb413ed4b01last month on GitHub