Skip to content

0.107 → 0.108 — a cell holds a value, a namespace holds cells

One distinction runs through this release. A cell holds a value and cuts the commitment ladder for it; a namespace holds cells and is what they compose to. Leaves and array are cells — items: fixes the element type but never the arity, so default: [] and default: [{…}] say what no element declaration can. A typed dictionary and a variant's field set are namespaces.

Two rules follow. A literal is declared where its cell is, so a default: / example: on a typed dictionary is refused at load. And absence is inherited, not terminal, so an absent container makes every cell below it absent and each cell then cuts its own ladder — which is what makes the plate total at every depth, the claim PLATE_DATA.md makes when it says a declared address is a direct read.

Obligation collapses into the same shape: must_fill: is retired, because a field asks one question — does it have a value when nobody types anything? — and default: is the whole answer.

Break Surface Action
must_fill: fails load at every depth Quill.yaml Delete it, or write the default: / example: the message names
A default: / example: on a typed dictionary fails load Quill.yaml Move each value onto the property that holds it
An absent container reaches its properties' default:s rendered output None, unless a plate counted on the blank
resolve() reports a container's strongest contributing rung consumers reading FieldSource Stop reading blank on a container as "renders nothing"
quillmark:must_fill leaves the transform schema consumers of that projection Read default:'s absence off the declaration view
FieldSchema.must_fill and QUILLMARK_MUST_FILL_KEY are gone Rust Call must_fill(); the constant has no replacement
QuillFieldSchema.must_fill is gone WASM Delete the read; default === undefined is the answer

Documents in storage are untouched. Both load errors are loud — the quill is rejected, not the document — and no in-tree quill declares either shape, so the upgrade reads as a quill that stopped loading rather than as a fix.

must_fill: is retired

default: answers both questions a field raises. Its value fills the cell; its absence is the obligation. Declaring must_fill: is a load error (quill::field_parse_error) whose message names the one migration that field's shape takes, because the key alone does not say which:

Declaration Migration
on a typed dictionary delete — a namespace carries no obligation, its leaves do
must_fill: true, no default: delete — a defaultless field is obliged already
must_fill: false beside a default: delete — a defaulted field is unobliged already
must_fill: false, no default: default: the type's blank ("", [], 0, false)
must_fill: true beside a default: the judgment call below

Four of the five restate the derivation and migrate by deletion. The fourth is the corpus's most common default: already: a type-empty default is how a field says "nothing" and stays skippable.

The one behavior deleted

must_fill: true beside a default: rendered a safe value and asked a human to confirm it. Nothing else spells that, so pick which half you meant:

# 0.107
classification:
  type: enum
  values: [UNCLASSIFIED, CUI]
  default: UNCLASSIFIED
  must_fill: true

# 0.108 — keep the value, drop the ask
classification: { type: enum, values: [UNCLASSIFIED, CUI], default: UNCLASSIFIED }

# 0.108 — keep the ask, suggest the value
classification: { type: enum, values: [UNCLASSIFIED, CUI], example: UNCLASSIFIED }

An example: fills the blueprint cell the default: vacated and seeds carrying the !must_fill marker, where a default:-only field seeds nothing. It never renders, so an untouched document renders the blank rather than asserting a value nobody chose. For a marking or an effective date that is usually the safer half: a blank marking is visibly unanswered, an unreviewed UNCLASSIFIED is not.

For a string or enum the blueprint bytes are identical either way. Three shapes are not:

  • A richtext example never inlines, so its cell becomes a bare marker and the value survives only as the # e.g. hint.
  • An integer / number / boolean blank is indistinguishable at the plate from an authored zero.
  • On a variant container the two targets select different worlds: default: CUI renders the CUI world and obliges its cells, example: CUI leaves the discriminant blank.

A typed dictionary holds no literal

# 0.107
address:
  type: object
  default: { street: "5000 Forbes Ave", city: Pittsburgh }
  properties:
    street: { type: string }
    city:   { type: string }

# 0.108 — quill::default_on_namespace, naming `street, city`
address:
  type: object
  properties:
    street: { type: string, default: "5000 Forbes Ave" }
    city:   { type: string, default: Pittsburgh }

quill::example_on_namespace is the example: twin, and both fire wherever a field schema sits — a nested property, a typed table's items:, a variant cell. This is the variant container's existing rule (quill::default_type_mismatch) generalized.

The container spelling was a second declaration of a fact the properties already carried, and the two axes read different ones: default: {name: A} rendered A while obligation derived per property and still reported name unauthored. It was also unchecked, so default: {nope: 1} loaded and crossed an undeclared key to the plate.

Three consequences for a quill that moves its literal down:

  • default: {} stops being a special case. It expanded to the blank-filled shape; per-property type-empty defaults say the same thing and are checked.
  • The blueprint gains its leaf annotations back. A defaulted dictionary rendered as a bare block mapping with no # type comments, being shippable verbatim. Each property now carries its own annotation, marker and # e.g. line.
  • A partial default is no longer a "handled, ignore the rest" signal. Every declared property is emitted, each at its own rung.

An array keeps its literal, and each element it supplies is completed against items: exactly as an authored element is:

rows:
  type: array
  default: [{ who: A }]        # → [{ who: A, role: lead }]
  items:
    type: object
    properties:
      who:  { type: string }
      role: { type: string, default: lead }

Absence is inherited

An absent container returned a value instead of descending, and one branch decided everything below it. A property's default: was unreachable unless the document authored the container above it:

contact:
  type: object
  properties:
    name:  { type: string }
    email: { type: string, default: "hi@example.com" }
# A document omitting `contact` entirely
# 0.107 → contact: {name: "", email: ""}
# 0.108 → contact: {name: "", email: "hi@example.com"}

This is a render-output change for any quill declaring a property default under a container a document may omit. It is also what removes three disagreements at once: contact: {} rendered the leaf defaults that leaving contact out did not, so two spellings of the same state disagreed; a container default: {name: A} crossed whole, so a declared property it omitted was missing from the plate — a direct Typst read of it a compile error, on an address form-field still binds; and default: {}, documented as expanding to the blank-filled shape, emitted {} with no declared key at all.

Writing contact: {} is now a no-op. A declared address is present however much of its container the document left out, at any depth, which is what lets a plate read data.contact.address.city directly rather than through a guarded accessor.

resolve() reports what contributed

A namespace has no rung of its own, so it reports the strongest that contributed: authored when the document wrote any of it, else default when a cell below resolved to one, else blank.

// An absent `contact` over a defaulted `email`
// 0.107
resolved.source === 'blank'      // while rendering hi@example.com
// 0.108
resolved.source === 'default'

An editor ghosting on blank was ghosting the wrong cells. Nothing inside a container the document did not author reads authored, and a variant container counts its live world's cells the same way — so writing one of them lifts a container whose discriminant fell to the schema's default:.

Seeding descends

A dictionary with no example: of its own seeded nothing, so a property's example: was unreachable at every projection: the render floor never emits an example, and the blueprint is a different document. A seed is now composed from whatever its cells commit:

contact:
  type: object
  properties:
    name:  { type: string, example: Ada }
    email: { type: string, default: "hi@example.com" }
    note:  { type: string }
# seed_document() → contact: {name: Ada}

The commit is sparse at every depth — only the cells with an example:, the rest deferring to the render floor — and a dictionary whose cells commit nothing stays absent, as any field does. Markers ride the cell they belong to rather than the container.

A quill moving a container example: down to its properties gets the same seed, minus the keys it never declared a value for.

The transform schema drops obligation

quillmark:must_fill is gone from build_transform_schema, and QUILLMARK_MUST_FILL_KEY with it. That projection is the wire validity contract, and an unauthored must-fill cell is wire-valid by design — the blank leads every emitted enum domain precisely so a generated validator accepts it. A required-shaped flag there read as the gate the design refuses.

The declaration view (QuillConfig::schema(), Quill.schema) carries default:, so a consumer wanting the obligation derives it from that key's absence. No emitted JSON changes for a quill that never declared must_fill:, since the view emits only what an author wrote.

Obligation is unchanged everywhere else: must_fill() is default.is_none(), the !must_fill blueprint marker and the non-fatal validation::must_fill warning behave exactly as before, and it remains a warning rather than a gate.

Rust and WASM surfaces

// The raw field is gone; the derivation is the only answer
field.must_fill                 // 0.107: Option<bool>
field.must_fill()               // 0.108: bool — `default.is_none()`

FieldSchema is #[non_exhaustive], so a struct literal was already going through new plus with_* setters; only a direct read or write of .must_fill breaks.

// crates/bindings/wasm — QuillFieldSchema
must_fill?: boolean             // removed

A compile-time break for an editor typed against the interface, and the only WASM surface change in this release. FieldSource is unchanged as a type; what it reports for a container is not (above).

Not affected

Stored blobs, the document syntax, the content model, and the plate-JSON wire shape for any quill declaring no containers. form.json is unchanged, so the pdfform flatten path and the canvas preview are untouched. The Typst floor moves to 0.15.1 — the version the workspace already resolved to under the 0.15.0 caret, now named as the one the tree is built and tested against.