Skip to content

0.96 → 0.97 — Document.getgetStored; $id becomes the unique card handle

Two changes. On the WASM surface, the quill-free transport read Document.get becomes Document.getStored; the interpreted schema-plane read quill.reader(doc).get is unchanged, and Python is unaffected (it has no quill-free field read). In the document model, a card's $id is now unique per document — parse repairs a violation, mutators and storage reject one. If your host never calls doc.get(...) and never stamps the same $id onto two cards, there is nothing to do.

Why

The write surface separates verbatim from typed on two axes — receiver and verb: verbatim writes are store_field / store_fields on Document; typed writes are set / set_all on quill.writer(doc). "The verb carries the lane."

The read surface used to separate the same two lanes on the receiver alone — verbatim Document.get vs. interpreted reader.get, one get doing both jobs. The two read rows in the bindings parity table read as near-duplicates, and the only thing distinguishing a total, returns-undefined-on-a-typo read from a schema-checked, throws-on-a-typo read was which object you called it on.

getStored restores the write side's discipline to the read side: the verbatim read is the read echo of store, and reader.get stays the reader/writer twin of set. The lane now lives in the verb both ways.

getStored's semantics are byte-for-byte what get did — verbatim value for a field (a content object for a richtext field, a scalar/array/object otherwise), body content when addr.field is absent, undefined for an absent field, and a throw only for an out-of-range addr.card. Only the name changed.

Migrate: rename the call sites

Every doc.get(...) transport read becomes doc.getStored(...). The argument is the same Addr | string (a bare string is { field } shorthand).

// 0.96
doc.get('qty')                       // 3
doc.get('missing')                   // undefined
doc.get({ card: 2, field: 'qty' })   // a composable card's field
doc.get({})                          // main body content

// 0.97
doc.getStored('qty')                       // 3
doc.getStored('missing')                   // undefined
doc.getStored({ card: 2, field: 'qty' })   // a composable card's field
doc.getStored({})                          // main body content

Leave reader.get alone — it was already the right name, and it is the read you want whenever a quill is in hand:

quill.reader(doc).get('subject')        // richtext → markdown; unchanged
quill.reader(doc).card(0).get('body')   // card field by its $kind; unchanged

getMarkdown (the body markdown read), getExt / getExtNamespace, and isFill keep their names.

Unaffected

  • Python. quillmark (PyO3) has no quill-free field read; interpreted field reads already go through quill.reader(doc).get. No change.
  • Core (Rust). The transport read is the map-idiomatic payload().get, already lexically distinct from reader.get — no collision, no rename.

$id is now unique per document

$id graduates from free-form metadata to the durable card handle (canon: DOCUMENT_STORAGE.md § Card-id identity). One rule — unique across a document's composable cards, never empty — enforced per boundary:

  • Parse repairs. A duplicate $id keeps the first card's entry and drops it from later cards under a warning (parse::card_id_duplicate); an empty $id is dropped too (parse::card_id_empty). The document still loads, and one save converges on a valid file.
  • Mutators reject. Core push_card / insert_card and WASM insertCard throw edit::card_id_collision / edit::empty_card_id for a card whose $id is taken or empty. The guarded setter Document::set_card_id (re-)stamps a placed card; remove_card_id retires a handle. Python's add_card stamps no $id, so it cannot collide.
  • Storage rejects. A stored JSON blob carrying a duplicate or empty composable-card $id fails to load as malformed. No writer up to 0.96 minted a $id at all, so existing rows are unaffected.

find_card / cardIndexById are unchanged for valid documents — there is now at most one match, so "first match" stops being a caveat. A $id on the main card is outside the scope (main is addressed structurally, never by id) and is preserved verbatim.

Migrate: nothing, unless you deliberately stamped one $id onto several cards. Re-stamp each card with a distinct id and key any shared grouping off a user field or an $ext namespace instead.