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.