Skip to content

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:

  1. The schema-aware form view is removed — validation diagnostics move to the focused validate(doc) entry point.
  2. The absence diagnostic is renamed validation::must_fill_absentvalidation::field_absent, and the schema cell axis is renamed "Must Fill" → "Unendorsed". "Must-fill" now names only the blueprint sentinel, never the cell.
  3. The example reference document is removed — its "show me a filled-out one" role is served by seeding.
  4. One Card shape flows in and out; the flat CardInput is removed and pushCard / insertCard accept the shape they return. Build fresh cards with Document.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:

- validation::must_fill_absent
+ validation::field_absent

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_card in Python) is the ergonomic constructor that replaces the old flat input — each entry of fields becomes a card field, in insertion order.
  • A stale { kind, fields } object now raises a clear error instead of deserializing into an empty card (fields is an unknown key).
  • The Card object hoists the $ entries to named members: kind (string, "" when none), optional quill / id, optional ext, and payloadItems (user fields + comments, in order). WASM uses camelCase payloadItems; Python uses snake_case payload_items (item entries — type / key / value / fill / text / inline — are identical across both).
  • The fill flag on a field item is the !fill authoring marker (true ⇒ the source wrote key: !fill <value>), signalling a template placeholder to be replaced rather than authored input. It defaults to false — an ordinary committed value — and that is what makeCard and seedCard / seedDocument emit: seeded example: 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 — fill does not carry it.
  • Rust: Document::push_card now returns Result<(), EditError> and validates the card's composable kind (as insert_card does); the canonical card wire type is quillmark_core::CardWire (From<&Card> / TryFrom).

Updating host code — checklist

  1. Replace every quill.form(doc) call with quill.validate(doc); read diagnostics off the returned array directly (no .diagnostics wrapper).
  2. Replace blankMain / blankCard with a quill.schema-driven empty editor (for a blank form), or quill.seedDocument() for an example-filled starter — these are not interchangeable; seeding commits example: values, it does not produce an empty document.
  3. Rebuild any field/value/order table from the quill.schema × Document join shown above; drop references to the removed Form* types.
  4. Update diagnostic routing: validation::must_fill_absentvalidation::field_absent, and form::unknown_card_kindvalidation::unknown_card.
  5. Relabel any "Must Fill" / "required" cell badge to Unendorsed.
  6. Replace quill.example reads with quill.seedDocument().
  7. (Rust) Update match arms over ValidationError for the FieldAbsent / MustFillSentinel split.
  8. Replace flat pushCard / insertCard input ({ kind, fields }) with Document.makeCard(...), or pass a Card from cards / removeCard / seedCard straight through. (Rust) handle the new push_card Result.