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
richtextexample never inlines, so its cell becomes a bare marker and the value survives only as the# e.g.hint. - An
integer/number/booleanblank is indistinguishable at the plate from an authored zero. - On a variant container the two targets select different worlds:
default: CUIrenders the CUI world and obliges its cells,example: CUIleaves 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
# typecomments, 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 }
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.
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.