0.87.0 → 0.88.0 — Form view removed, field_absent/Unendorsed rename, example folds into seeding, one Card shape¶
0.88.0 ships four breaking changes, all in the schema / editor-surface area:
- The schema-aware form view is removed — validation diagnostics move to
the focused
validate(doc)entry point. - The absence diagnostic is renamed
validation::must_fill_absent→validation::field_absent, and the schema cell axis is renamed "Must Fill" → "Unendorsed". "Must-fill" now names only the blueprint sentinel, never the cell. - The
examplereference document is removed — its "show me a filled-out one" role is served by seeding. - One
Cardshape flows in and out; the flatCardInputis removed andpushCard/insertCardaccept the shape they return. Build fresh cards withDocument.makeCard.
None of these change how a document renders: incomplete documents still render (each absent field zero-fills in the plate projection — see 0.85 → 0.86). The changes are to the editor/authoring API surface and the diagnostic vocabulary.
Quick reference¶
| 0.87 | 0.88 |
|---|---|
Quill::form / quill.form(doc) |
Quill::validate / quill.validate(doc) |
Quill::blank_main / quill.blankMain |
drive from quill.schema, or quill.seedDocument() |
Quill::blank_card / quill.blankCard |
drive from quill.schema, or quill.seedDocument() |
Form / FormCard / FormFieldValue / FormFieldSource types |
removed — no projection type |
validation::must_fill_absent |
validation::field_absent |
form::unknown_card_kind |
validation::unknown_card |
schema cell "Must Fill" (no default:) |
schema cell Unendorsed |
QuillConfig::example() / quill.example |
Quill::seed_document() / quill.seedDocument() / quill.seed_document() |
ValidationError::MustFillUnset { source } |
ValidationError::FieldAbsent + MustFillSentinel |
pushCard({ kind, fields, body }) (CardInput) |
pushCard(Document.makeCard(kind, fields, body)), or pass a Card through |
push_card(card) -> () (Rust) |
push_card(card) -> Result<(), EditError> (validates kind) |
The <must-fill> blueprint sentinel and the fatal
validation::must_fill_sentinel diagnostic are unchanged — "must-fill"
still names the sentinel, just not the cell.
1. The form view is removed¶
Quill::form, Quill::blank_main, Quill::blank_card (and their
quill.form / blankMain / blankCard bindings) are gone, along with the
Form, FormCard, FormFieldValue, and FormFieldSource types. In their
place is a single, focused entry point:
- Rust:
Quill::validate(&Document) -> Vec<Diagnostic> - WASM:
quill.validate(doc): Diagnostic[] - Python:
quill.validate(doc) -> list[dict]
This is a hard cutover for any consumer that called form / blankMain /
blankCard.
Why¶
The form view did four unrelated jobs at once. Three of them have a more natural home, so the projection itself was redundant:
| Form view did | Where it lives now |
|---|---|
forwarded validation::* diagnostics |
validate(doc) — the same diagnostics, no projection wrapper |
dropped unknown cards + emitted form::unknown_card_kind |
validate(doc) already reports the identical condition as validation::unknown_card |
bundled per-field value / default / example / source |
a Document-payload × quill.schema join the consumer does directly |
sorted fields by ui.order |
read each field's ui.order from quill.schema and sort at the render site |
validate(doc) includes the non-fatal validation::field_absent signal that
render demotes, so it doubles as the completeness surface that
source: "missing" used to provide.
Validation / diagnostics¶
// before
const errors = quill.form(doc).diagnostics.filter(d => d.severity === "error");
// after
const errors = quill.validate(doc).filter(d => d.severity === "error");
Field doneness (the old source: "missing")¶
A field was "missing" when it was absent and had no default:. That is
exactly what validation::field_absent reports — keyed by path:
const incomplete = new Set(
quill.validate(doc)
.filter(d => d.code === "validation::field_absent")
.map(d => d.path),
);
Field values, defaults, and order (the old FormCard.values)¶
Build the join yourself from quill.schema (field definitions, default,
example, ui) and the Document payload (authored values). The schema's
fields map is keyed alphabetically; sort by each field's ui.order for
presentation order:
const schema = quill.schema.main; // QuillCardSchema
// Authored values: fold the card's `payloadItems` field entries into a map.
const payload = Object.fromEntries(
doc.main.payloadItems
.filter(item => item.type === "field")
.map(item => [item.key, item.value]),
);
const ordered = Object.entries(schema.fields)
.sort(([, a], [, b]) => (a.ui?.order ?? Infinity) - (b.ui?.order ?? Infinity));
for (const [name, field] of ordered) {
const value = payload[name]; // undefined ⇒ fall back to field.default
const source = name in payload ? "document" : field.default !== undefined ? "default" : "missing";
// render an input for `name` using field.type / field.ui / field.example …
}
The same join works against quill.schema.card_kinds[kind] for composable
cards (iterate doc.cards, look up each card's kind; a kind with no schema
entry is the unknown-card case validate already flags).
Blank starting state (the old blankMain / blankCard)¶
There is no separate blank projection. To render an empty editor, drive it from
quill.schema with no payload (every field falls back to its default, or is
absent). For a pre-filled starter document, use quill.seedDocument().
These are two distinct paths, not interchangeable: seedDocument /
seedMain / seedCard are illustrative — they commit each field's
example: value, so a seeded card arrives full of placeholder content (e.g.
"ORG/SYMBOL"), not blank. Reach for seeding when you want a worked example;
drive the schema directly when you want an empty form. Do not substitute
seedDocument() for the old blankMain / blankCard if you wanted blank.
Unknown card kinds¶
The form::unknown_card_kind diagnostic is now validation::unknown_card
(same condition, canonical validation::* code, with a cards[N] path). The
card is no longer silently dropped from a view — your card iteration decides
how to present it.
2. field_absent / Unendorsed rename¶
The diagnostic for an absent schema field is renamed:
The reframing behind the rename: absence is not a fill requirement. The
render floor zero-fills an absent field, so nothing is truly "required" — the
"must fill" framing overclaimed an obligation the engine never enforces.
"Must-fill" is now scoped to where it is honest: the blueprint's <must-fill>
sentinel (the author/LLM communication device) and the fatal
validation::must_fill_sentinel when that sentinel survives into a render.
The schema cell axis is renamed to match. A field with no default: is now
Unendorsed (the antonym of Endorsed, a field that carries a default:),
not "Must Fill":
| Schema cell | 0.87 name | 0.88 name | Behaviour |
|---|---|---|---|
no default: |
Must Fill | Unendorsed | blueprint renders <must-fill>; absence is a non-fatal validation::field_absent signal |
has default: |
Endorsed | Endorsed | blueprint renders the default with ; delete-ok; absent ⇒ default fills |
Migrating¶
If you route on the diagnostic code, update the string:
- quill.validate(doc).filter(d => d.code === "validation::must_fill_absent")
+ quill.validate(doc).filter(d => d.code === "validation::field_absent")
In Rust, the ValidationError::MustFillUnset { source } variant is gone. It
splits into two flat variants, and the MustFillSource enum is removed:
- ValidationError::MustFillUnset { path, source: MustFillSource::Absent, .. } => handle_absent(path),
- ValidationError::MustFillUnset { path, source: MustFillSource::Sentinel, .. } => handle_sentinel(path),
+ ValidationError::FieldAbsent { path, .. } => handle_absent(path),
+ ValidationError::MustFillSentinel { path, .. } => handle_sentinel(path),
If you display the cell axis to authors (a "required"/"Must Fill" badge), relabel it Unendorsed.
3. The example reference document is removed¶
QuillConfig::example() and the Quill.example (WASM) / quill.example
(Python) getters are gone. The example document emitted an annotated,
no-sentinel "filled-out" string, but nothing consumed its annotations — the
authoring surface is blueprint(), and its rendered output is identical to
seeding-then-rendering. So the projection collapses into the seed.
Its "show me a filled-out one" role is served by seeding, which returns a
committed Document rather than an annotated string:
- Rust:
Quill::seed_document() -> Document - WASM:
quill.seedDocument(): Document - Python:
quill.seed_document() -> Document
Migrating¶
- const filled = quill.example; // annotated String
+ const filled = quill.seedDocument().toMarkdown(); // committed Document → Markdown
Seeding commits only each field's example: value and leaves every other
field absent; default: and the type-empty zero are interpolated at render, not
persisted (the commitment ladder).
A field with both an example and a default renders its example in a seed.
CLI¶
render with no input file now renders the seeded document directly (it used
to render the example string). No flags change; the output is equivalent.
4. One Card shape in and out; CardInput removed¶
pushCard / insertCard used to take a flat CardInput { kind, fields?, body? }
that was not the same shape a card came back as ({ kind, payloadItems, … }).
Passing a returned card straight back silently dropped its fields. Now there is
one Card shape in both directions — the shape returned by cards,
removeCard, and quill.seedCard is exactly what pushCard / insertCard
accept:
// before — flat input, lossy, different from the read shape
doc.pushCard({ kind: "note", fields: { author: "Alice" }, body: "Hi" });
// after — build the Card shape with makeCard, or pass one straight through
doc.pushCard(Document.makeCard("note", { author: "Alice" }, "Hi"));
doc.pushCard(quill.seedCard("note")); // seed → push now round-trips
const removed = doc.removeCard(0);
doc.insertCard(2, removed); // read → re-insert now round-trips
# before
doc.push_card({"kind": "note", "fields": {"author": "Alice"}, "body": "Hi"})
# after
doc.push_card(Document.make_card("note", {"author": "Alice"}, "Hi"))
doc.push_card(quill.seed_card("note"))
makeCard(kind, fields?, body?)(Document.make_cardin Python) is the ergonomic constructor that replaces the old flat input — each entry offieldsbecomes a card field, in insertion order.- A stale
{ kind, fields }object now raises a clear error instead of deserializing into an empty card (fieldsis an unknown key). - The
Cardobject hoists the$entries to named members:kind(string,""when none), optionalquill/id, optionalext, andpayloadItems(user fields + comments, in order). WASM uses camelCasepayloadItems; Python uses snake_casepayload_items(item entries —type/key/value/fill/text/inline— are identical across both). - The
fillflag on a field item is the!fillauthoring marker (true⇒ the source wrotekey: !fill <value>), signalling a template placeholder to be replaced rather than authored input. It defaults tofalse— an ordinary committed value — and that is whatmakeCardandseedCard/seedDocumentemit: seededexample:values are committed as ordinary content, not flagged placeholders. The engine does not track whether a seeded value is still untouched, so a seeded field reads as authored content; if you want "this is still the example, clear it on first edit" behaviour, that provenance is the consumer's to layer on top —filldoes not carry it. - Rust:
Document::push_cardnow returnsResult<(), EditError>and validates the card's composable kind (asinsert_carddoes); the canonical card wire type isquillmark_core::CardWire(From<&Card>/TryFrom).
Updating host code — checklist¶
- Replace every
quill.form(doc)call withquill.validate(doc); read diagnostics off the returned array directly (no.diagnosticswrapper). - Replace
blankMain/blankCardwith aquill.schema-driven empty editor (for a blank form), orquill.seedDocument()for an example-filled starter — these are not interchangeable; seeding commitsexample:values, it does not produce an empty document. - Rebuild any field/value/order table from the
quill.schema×Documentjoin shown above; drop references to the removedForm*types. - Update diagnostic routing:
validation::must_fill_absent→validation::field_absent, andform::unknown_card_kind→validation::unknown_card. - Relabel any "Must Fill" / "required" cell badge to Unendorsed.
- Replace
quill.examplereads withquill.seedDocument(). - (Rust) Update
matcharms overValidationErrorfor theFieldAbsent/MustFillSentinelsplit. - Replace flat
pushCard/insertCardinput ({ kind, fields }) withDocument.makeCard(...), or pass aCardfromcards/removeCard/seedCardstraight through. (Rust) handle the newpush_cardResult.