Skip to content

0.109 → 0.110 — the seam spells the container discriminator, and the type requires it

Documents in storage are untouched and stored blobs re-encode byte for byte. The one break is TypeScript, it is reported, and it is wide: every hand-built container literal must spell instance.

0.109 gave Container a discriminator and left it a writer's obligation no checker asked for. The op lane got the requirement first, on ContentContainerInput. That covered the lane an anchor-preserving editor writes through and left the one a whole-document codec writes through — overwrite, CardInput.body — reporting nothing. This closes the other half, and deletes the extra type doing it.

Break Surface Action
ContentContainer.instance is required TypeScript Spell it. assignInstances for a flattened tree; 0 where nothing adjacent shares the shape
ContentContainerInput is removed TypeScript ContentContainer — the two types merged
LineOp.setContainers takes ContentContainer[] TypeScript None, if it was already spelling the field
Content parsed from a stored document lacks the field TypeScript A cast at that boundary; storage still omits a zero

Additive in Rust: serial::to_seam_value. serial::to_canonical_value is unchanged, and so is every byte it writes.

Why a read type could not require it

Content is a read shape and a write shape at once. Every documented composition turns one into the other — overwrite(addr, importMarkdown(md)), spelled out in overwrite's own docstring, and insertCard(removeCard(0)) under "every Card is a valid CardInput". A field the read may omit is a field the write cannot require, so the obligation stayed invisible on exactly the lane most likely to carry it.

The encoding is what made the read omit it. One encoder served storage and the bindings alike, and it wrote instance only where the value was non-zero — the right trade for storage, where the omission is what keeps a row written before the field existed re-encoding byte for byte.

The two forms

serial::to_seam_value is to_canonical_value with every Container::instance spelled, zero included. The two decode to one value; they differ in what a reader is shown.

The bindings encode the seam form on every lane typed Content: reader.getContent / getContentAt, getStored on a body, importMarkdown, rebase, and the card wire behind document.main, document.cards, document.card(i), removeCard, makeCard, quill.seedMain and quill.seedCard. Python's content reads carry it too, where the value is a plain dict and no checker is involved.

Storage keeps the omission, and so do the render lanes: the plate JSON a backend lowers, the resolved view, and the value at rest in a document.

What stays in the storage form on the seam is what is typed unknown: getStored on a field and PayloadItem.value both answer with the stored bytes verbatim, which is their contract. Read a field's content through reader.getContent.

Required is not correct

A checker reports a container that omits the field. It cannot report a 0 stamped on every run, which is byte-for-byte the write that welds them: a second list's items come back as continuation paragraphs of the first, markers gone. The requirement makes the field unskippable, not right.

The rule is assignInstances from @quillmark/wasm/runtime: one entry per container run in document order, null for a block carrying no container at that depth, returning containers ready to write.

const [outer, , inner] = assignInstances([listA, null, listB]);
// 0, 0 — the paragraph between them parts the runs
const [a, b] = assignInstances([listA, listB]);
// 0, 1 — adjacent, one shape, and the pair that would otherwise weld

Reading is still not the mirror of writing. A read spells the field on every container, and it carries a discriminator on pairs no writer had to spell: 1. beside a list starting at 3 differs by start, so those runs arrive apart with nothing written, and the canonical form spends one anyway because Markdown reads only a list's first number.

The one shape that still needs a cast

Content read out of a stored document — a JSON.parse of a row, a body lifted out of document.toJson() — is in the storage form, so a zero instance is absent and the value does not satisfy Content. That boundary is unknown on the way in either way; assert it there.

The narrower alternative is to read the document through the binding rather than around it: Document.fromJson(json) and then doc.main.body hands back the seam form.

Rust

Container and its encoding are unchanged. serial::to_canonical_value, container_to_value and Normalized::to_canonical_json all still write the storage form, and from_canonical_value reads either.

A Rust embedder holds a Container and reads instance() off it, so nothing here reaches it. to_seam_value is for a host that hands JSON to a language binding and wants the read to be a legal write.