Skip to content

0.115 → 0.116 — !must_fill is gone; a card of an undeclared kind, and a body under body.enabled: false, render and warn; an undeclared key warns; a card with no $kind, and a non-mapping card-yaml payload, fail at the fence

The break no error reports: a document that failed to render now renders. A card whose $kind the quill does not declare, and body prose under body.enabled: false, were fatal validation::* errors. Each is now a Warning, on a one-shot render's warnings and from quill.validate(doc), and the render proceeds without the input. A pipeline that relied on the render failing to catch these documents catches nothing: act on the two codes below. On the CLI, render prints them, and quillmark check --strict fails on them.

The rule behind it: a render fails only where the engine would have to invent what the author wrote. Input no declaration claims displaces nothing, so it renders and warns (SCHEMAS.md § What blocks a render).

A quieter one for plates: a typed dictionary or table row carries $ink and $path keys. A plate that walks a typed dictionary's keys, compares one whole, or spreads a row into a call sees one more entry. See Print with ink.

Break Surface Action
quill.seed_document(), seed_main and seed_card commit no example: (a field's or body.example); a seeded card carries its $seed overlay's fields and $body alone, its body otherwise empty rendered output, Rust, bindings A consumer that rendered a seed as a filled-in preview authors the values it wants shown. A quill's starter body moves to a template document's own body, or, for a card seed_card adds, to the main card's $seed.<kind>.$body
The blueprint leaves a defaultless cell empty (title: # string) and moves every example: to a # e.g. line, where it wrote !must_fill with the example as the cell blueprint A consumer parsing blueprint text for the marker reads empty cells instead
A card's description rides its $kind line inline ($kind: main # A formal letter.), where it was an own-line # … under it, ahead of the first field's blueprint A consumer reading the description from the first comment line reads the $kind line's trailer
A typed table with default: [] is led by the whole field commented out, holding a row: # attendees: then the row, above the live attendees: [] blueprint None; the cell is still []
The blueprint's bodies are empty. A body.example rides a # body e.g. line, one YAML scalar closing the card's payload, where it was the body's text; a kind declaring none shows no Write <kind> body here., and a kind under body.enabled: false closes with # no body blueprint, rendered output A consumer submitting a blueprint with bodies unedited renders them empty, where it rendered the example or placeholder as prose. One reading the example from the body reads the # body e.g. line
A matrix column with no example: or default: shows its <type>[<format>] in the matrix's # e.g. line (earned: date<YYYY-MM-DD \| today>), where it showed null blueprint None; a type is not an answer, so a hint pasted into the cell needs that column written
document::FORMAT_RULES is MARKDOWN_RULES, headed Quillmark Markdown rules:; WASM Document.formatRules() is markdownRules() and Python Document.format_rules() is markdown_rules(). The text drops the blueprint's cell reading (a concrete value is a default, an empty one awaits input, # e.g.), and the rule that numbers and booleans must be unquoted, which validation never enforced Rust, WASM, Python Rename the call. A consumer that relied on the rules to explain a blueprint's cells states that where it composes its prompt
document::blueprint_instruction, WASM Document.blueprintInstruction and Python Document.blueprint_instruction are deleted: a prompt is the consumer's to write Rust, WASM, Python Write the sentence where you compose the prompt, e.g. Fill in the `<quill>` blueprint: answer each empty field and write each body.
validation::must_fill is never emitted, by marker or by an unauthored cell diagnostics A strict consumer reading an empty validate as done checks the fields it needs itself
A !must_fill left in markdown is an unknown tag: it drops and its value stays, as an answer. On a key's own line it warns parse::unsupported_yaml_tag at the field's path; on a bare - element, as any tag there, it drops silently markdown, rendered output A document filled from a 0.115 blueprint renders each example it left marked. Delete the marker and its example wherever the cell was never answered
edit::fill_on_mapping and parse::fill_marker_unsupported_position are deleted diagnostics Drop the routes
New storage tag quillmark/document@0.116.0: a field item carries no fill or nested_fills, and a row carrying either, or any other key its type does not name at the document, card, payload or item level, fails the load. Every earlier tag still reads, past both keys, keeping the value. A nested comment spanning lines, which to_markdown wrote as bare YAML, fails the load under any tag stored documents None to load; a host that writes rows itself drops the keys. A row's bytes move on its next save (the tag, and each field item's "fill": false), so a hash over stored bytes recomputes, and a 0.115 build refuses the row from then on
Card::store_fill, CardMut::store_fill, Payload::is_fill, QuillValue::{fill, fill_paths, nonroot_fill_paths, set_fill_at, is_object_at}, FieldSchema::{must_fill, example_content}, BodyCardSchema::example_content, EditError::FillOnMapping and edit::validate_fill_targets are deleted; PayloadItem::Field and PayloadItemWire::Field lose fill (and nested_fills), as do the Field variants of the frozen FrontmatterItemV0_81_0, PayloadItemV0_82_0 and PayloadItemV0_92_0, and PayloadItemWire refuses an unknown key Rust Store with store_field; drop the keys from a CardWire you decode
StoredDocument gains V0_116_0, and STORAGE_V0_116_0 is the current tag. ParseError gains PayloadNotMapping and MissingKind, and FieldSchema the public optional Rust An exhaustive match adds the arms; a FieldSchema literal sets optional
storeFill and isFill are deleted; a card's field item carries no fill or nestedFills, and insertCard throws on an item carrying either, or any other unknown key WASM Store with storeField; drop the keys from a card you build
A card dict's field item carries no fill or nestedFills, and insert_card raises ValueError on an item carrying either, or any other unknown key Python Drop the keys
A card whose $kind the quill does not declare renders instead of failing rendered output Read validation::unknown_card off a one-shot render's warnings or quill.validate(doc); treat it as blocking where your pipeline needs it to be
Body prose under body.enabled: false renders without the body instead of failing rendered output Same, for validation::body_disabled
A block after the root with no $kind fails the parse as parse::missing_kind, located at its fence, where it parsed as a card and failed validation::unknown_card with card: "" diagnostics Route on the new code. See A card with no $kind fails the parse
A stored card with no $kind loads folded into the body above it: its fields and comments as a code block, then its own body; its $ext drops stored documents None to load. Re-author the card from the code block where it was one
ResolvedCard.kind is String, string in TypeScript, and CardReader.kind / CardWriter.kind are never empty Rust, WASM Drop the None / null / "" arm
validation::unknown_card gains allowed, the quill's declared kinds diagnostics None; the key is additive
validation::unknown_card and body_disabled are Severity::Warning diagnostics A consumer treating every validation::* code as fatal routes on severity instead
A one-shot render's warnings (Quillmark::render, WASM engine.render, Python render) carry every quill.validate(doc) warning ahead of the compile's, and after the parse warnings in WASM and Python; a session's do not diagnostics, Rust, WASM, Python A consumer that merged validate's warnings into a one-shot render's drops the merge
ValidationError::UnknownCard and ValidationError::BodyDisabled are deleted Rust An exhaustive match loses two arms. QuillConfig::validate_document returns only what fails a render; Quill::validate carries the warnings
A key the schema does not declare warns validation::unknown_field, at any depth of a claimed card diagnostics A document that validated clean may not. Declare a key the plate reads in Quill.yaml; fix or remove the rest
A typst: key other than plate_file warns typst::unknown_key (args.key) in every render's and session's warnings, and in quillmark validate -v, though not from quill.validate(doc). typst.packages, which 0.115's docs told a quill to declare and no build read, is one Quill.yaml, diagnostics Delete the key. A package the plate imports is vendored under packages/<dir>/ with its typst.toml, as before
A card-yaml payload that is YAML but not a mapping fails as parse::payload_not_mapping, where it was parse::invalid_structure on a card and parse::missing_quill on the root diagnostics Route on the new code. Read the fence's line off location
LiveSession.paint(ctx, page, opts) is paint(ctx, page, scale) and returns nothing; PaintOptions and PaintResult are deleted WASM Pass one number, layoutScale × densityScale; size the canvas's display box from CSS rather than the result
The raster ceiling is 16384 px a side, MAX_RASTER_SIDE, where it was 16384² px of area, MAX_RASTER_PIXELS Rust, rendered output A PNG whose long side passes 16384 px at the asked ppi is refused under backend::invalid_raster_scale; lower the ppi. Rename the constant
An unquoted quill.version in Quill.yaml is its source text: version: 1.10 loads as 1.10, where it loaded as 1.1, and an unquoted number not spelled N.N (1.1e1, 1., .5, +1.2) fails quill::invalid_version Quill.yaml A document pinned @1.1 to such a quill fails quill::version_mismatch: pin the version the Quill.yaml spells
quillmark render without -f takes the format from an -o extension naming one: -o x.png writes PNG, one x-N.png per page, where it wrote a PDF to x.png; a -f the extension contradicts exits 1 before writing CLI Pass -f pdf, or name -o for the format it writes
Quillmark::open / render, Quill::compile_data / compile_checked / resolve, QuillConfig::compile_data / compile_checked, TypedReader::resolve and Backend::open take today: CalendarDate Rust Pass the host's local date
A plate's datetime.today(), whatever its offset:, is the render date the host supplies: the CLI and the WASM and Python bindings supply the local date, where the engine read the UTC clock shifted by offset: rendered output None on the bindings; a render in the evening west of UTC dates today, not tomorrow. A plate that asked for another zone's date (offset: 0 for UTC) gets it from the host: --today, or the render verbs' date argument
A date value may be today: quill.validate accepts it, reader.get() and stored documents carry it, and the blueprint annotates date<YYYY-MM-DD \| today> stored documents, blueprint A consumer parsing stored date values itself handles today, or reads the rendered date through reader.resolve()
usaf_memo's blank date renders a Date fill-in widget where it rendered today's date; the quill keeps version: 0.2.0 rendered output Write the date, or date: today to print the render date (default: today in a quill copied from it)
A typed dictionary, table row and data carry $ink and $path keys, and a card $ink, wherever one of their fields has ink: keys(), values(), pairs(), len(), == against a whole dictionary (and so contains and dedup, since each row's $path differs), repr and a spread (f(..row)) see them. A matrix, holding only members, carries neither plate Skip $-prefixed keys where a plate walks a dictionary; compare fields, not whole dictionaries. See Print with ink
A data key spelled $ink or $path in data, a card or a typed dictionary is dropped before the plate plate Rename the key
display(field, ..) also takes a dictionary and the date's key: display(row, "due", "[year]") plate None
A Typst plate resolves paths from its own file, loaded at its plate_file path, where it was loaded at the quill root whatever directory plate_file named. The quill's other .typ files outside packages/ import by path Typst quills A plate in a subdirectory that reaches assets/… by a bare path spells /assets/…. See A plate resolves paths from its own file
A diagnostic located in a Typst plate names its declared plate_file as location.file (plate.typ, tpl/layout.typ), where it named main.typ. One located in a vendored package names the file under the package's spec (@local/p:0.1.0/lib.typ), where it named the path inside the package alone diagnostics A consumer mapping main.typ to the plate maps the quill's plate_file; one opening a package file strips the spec
ui.title on a field, on main or quill, or on a card kind fails the load: quill::field_parse_error on a field, quill::invalid_ui on a card, each hinting title Quill.yaml Move a literal to the field's or card's own title:, beside description:. See A label is a top-level title
A {field} token in a title fails the load as quill::title_template, and a title on an array's items as quill::title_on_items Quill.yaml Drop the template; a consumer labels an instance from its own values, the first field leading
schema() carries a label as title on the field or card, beside description, and ui carries none schema, WASM, Rust Read field.title / card.title where you read ui.title. QuillFieldUi.title and QuillCardUi.title are gone and QuillFieldSchema.title / QuillCardSchema.title added; in Rust, UiFieldSchema::title and UiCardSchema::title move to FieldSchema::title and CardSchema::title
A declared title leads the field's leading comment and the card's $kind trailer: # <title> — <description>, or # <title> alone blueprint A consumer reading a field's or card's description from that comment reads it from schema() instead
body.unsupported is removed: a Quill.yaml declaring it on main or a card kind fails to load with quill::invalid_body, and schema() carries no unsupported list Quill.yaml, schema Delete the key. See body.unsupported is removed
plate::unsupported_construct is never emitted diagnostics Drop the route
Quill::unsupported_constructs, quill::UNSUPPORTED_CONSTRUCT and BodyCardSchema::unsupported are deleted Rust Drop the calls
QuillBlockConstruct and QuillCardBody.unsupported are deleted WASM Drop the reads
to_markdown writes a null value as a bare key:, where it wrote key: null markdown A consumer hashing, diffing or snapshotting to_markdown output re-baselines; the value round-trips unchanged
to_markdown and the blueprint write a multi-line string as a \| literal block (\|- when it ends without a newline), at any depth, where they wrote one double-quoted line; a string a block would not hold verbatim stays double-quoted. The value round-trips unchanged markdown, blueprint A consumer hashing, diffing or snapshotting to_markdown output re-baselines. One reading blueprint cells as quoted text reads block scalars too
A plaintext(inline) value that is one line plus a trailing newline, as a \| block keeps it, is still refused, and validation::not_inline, edit::field_not_inline and conform::field_not_inline carry trailingNewline: true and name the fix (\|- or a plain scalar) diagnostics None; the key is additive
ValidationError::NotInline and EditError::FieldNotInline gain trailing_newline: bool Rust Set it where you build one; add .. where you destructure one

!must_fill is removed

The marker said a cell held a placeholder rather than an answer, and the schema half of validation::must_fill said a cell with no default: needed one. Neither survives: default: is the value an unanswered cell renders and nothing more, ? moves the render floor and nothing more, and null ≡ absent holds on every surface — field: "" and field: null differ in the value they render and in nothing else.

What seeding makes. A seed is a document nobody has answered: one card per declared kind, each body empty, every field absent and rendering its default: or blank. An example: shows in the blueprint's # e.g. line and nowhere else; body.example shows in its # body e.g. line, and an editor may show it as the empty body's placeholder. seed_card commits the document's $seed overlay for the kind, taken whole, over that.

# 0.115 blueprint
recipient: !must_fill # array<string>
  - Mr. John Doe

# 0.116 blueprint
# e.g. [Mr. John Doe]
recipient: # array<string>

A label is a top-level title

ui keys never reach the blueprint, so a label under ui was invisible to an agent working from it, while a person names a field by that label. title sits beside description on a field and on a card kind, and the blueprint prints it.

# 0.115
card_kinds:
  indorsement:
    description: Chain of routing endorsements.
    ui:
      title: Routing indorsement

# 0.116
card_kinds:
  indorsement:
    title: Routing indorsement
    description: Chain of routing endorsements.

A template (ui.title: '{heading}') has no replacement: a consumer labels an instance from its own values, so field order decides which value leads. Drop it, and declare first the field that names the instance. A row's items.ui.title goes the same way.

The render date

A document dated by the day it renders stores today in a date field, or its quill declares default: today. The engine reads no clock: the host passes the render date, and the field and a plate's datetime.today() both render as it.

let today: CalendarDate = "2026-03-14".parse()?;
let session = engine.open(&quill, &doc, today)?;

A Rust host passes its local date, read from its own clock and built with CalendarDate::new(year, month, day). The CLI takes --today YYYY-MM-DD; engine.open / engine.render / reader.resolve take an optional trailing YYYY-MM-DD string in JavaScript, and engine.render a today= datetime.date in Python. Each defaults to the local date.

What reaches the plate

A card whose $kind the quill does not declare keeps its place in $cards with its $kind as authored, its fields verbatim and uncoerced, and no $body. A plate that branches on the kinds it knows falls through on it, which is what every fixture plate does. Every card carries $kind, so a bare card.at("$kind") reads.

A body under body.enabled: false does not reach the plate, as before.

An undeclared key warns

quill.validate(doc) warns validation::unknown_field at each key the schema does not declare where it sits: a card field, a typed dictionary's property, an array element's property, a matrix member's column, or a cell inside a variant container that no world declares. The page is unchanged: the key still crosses to the plate verbatim, except inside a variant container, whose wire carries only the live world.

A strict consumer that reads an empty validate, or a one-shot render's empty warnings, as done sees the new warning on any document carrying such a key. That includes a key a plate reads through data.at("logo", default: none) without declaring it: declare it in Quill.yaml, and the warning goes.

The hint names the likely fix, and args carry it. container and variant mark a variant cell written beside its discriminant (outcome: motion with moved_by: next to it rather than under it). suggestion names the declared field a misspelled key most likely meant.

A non-mapping payload has its own code

A column-zero ~~~ opens a card-yaml block whatever its info string, so code fenced with tildes in a body reaches the YAML parser, where it usually reads as a string. The failure carried no location and named neither rule nor fix. It is now its own code, located at the opener's line, and its hint names the backtick fence to write instead.

A block holding 0.115 0.116
a string, number, boolean or sequence, as a card parse::invalid_structure, Invalid YAML in card block: expected a mapping, no location parse::payload_not_mapping, located at the opener
the same, as the root block parse::missing_quill parse::payload_not_mapping, located at the opener
an empty or null payload an empty mapping unchanged

args carries actual (string, number, boolean or sequence) and, when the opener has an info string, info: python for ~~~python.

Migrate: a consumer routing the non-mapping case under parse::invalid_structure or parse::missing_quill routes it under parse::payload_not_mapping. To fix a document, change each tilde fence around code to backticks: the diagnostic's line is the one to edit.

A card with no $kind fails the parse

Every block after the root is a card, and a card names its kind. A block whose payload is a mapping naming no $kind is a card missing its kind line, or tilde-fenced code whose text reads as a mapping, and the parser cannot tell which. It fails as parse::missing_kind, located at the opener's line, and its hint names both fixes: add a $kind: line, or fence the code with backticks. args carries info when the opener has an info string.

A block after the root holding 0.115 0.116
a mapping with no $kind, an empty or null payload included a card with no kind: Document.parse succeeds, and the render fails validation::unknown_card with card: "" parse::missing_kind, located at the opener

A stored card with no $kind loads: it folds into the body above it, its fields and comments as an untagged code block and its body after them, so the prose under it stays in place. Its $ext drops. The code block admits the fields' text as any body does, so a bidi control or U+FFFC in a field value is dropped. An island id the body above already holds is renumbered past the highest isl-{n} in either body, and a comment anchor whose id it already holds drops. Every later card moves up one index.

Migrate: a consumer that parsed such a document and waited for the render to fail sees the parse fail instead. To fix a document, add the card's $kind: line, or change the tilde fence around code to backticks. A consumer keying state by card index finds the folded cards in the raw row before it loads: a cards entry whose payload.items holds no kind item. A quillmark/document@0.81.0 card stores a sentinel in place of payload and never folds.

paint takes one scale

paint rasterized at layoutScale × densityScale and reported layoutWidth = widthPt × layoutScale back, a number the caller computed to pass in. It takes the product alone, backing-store pixels per point, as a required argument, and returns undefined.

// 0.115
const r = session.paint(ctx, page, { layoutScale, densityScale });
canvas.style.width = `${r.layoutWidth}px`;
canvas.style.height = `${r.layoutHeight}px`;

// 0.116
session.paint(ctx, page, layoutScale * densityScale);
canvas.style.width = `${widthPt * layoutScale}px`; // or "100%" of a page box
canvas.style.height = `${heightPt * layoutScale}px`;

A call with no scale, or with an options object, throws backend::invalid_raster_scale where the first painted at 1 and the second at the options' product. A scale that is not finite and positive throws the same code, where it threw an uncoded message.

The painter still reduces a scale that would put a side past 16384 px. A consumer that read clamped, effectiveDensityScale or pixelWidth / pixelHeight reads canvas.width / canvas.height, and the scale painted at is canvas.width / widthPt.

One raster ceiling

Core refused a raster past 16384² px of area (MAX_RASTER_PIXELS) and the WASM painter clamped at 16384 px a side, a limit of its own. The side is the one limit now, core's MAX_RASTER_SIDE, and fit_raster_scale is the reduction paint applies. The side rule is the narrower of the two only for a page long on one axis: a PNG export whose long side passes 16384 px at the asked ppi fails under backend::invalid_raster_scale, where it rendered while its area fit.

data, each card, and each typed dictionary or table row carry a printable copy of their fields, read with ink(..). Its values are born in generated code, so they keep their click target in an editor preview through a function parameter, a loop variable, a destructuring, a filter or a package, where a plate's own #row.org keeps it only as a direct read. Compute with the dictionary, print its ink:

// 0.115: the owner's region, rebuilt from a hand-composed address
#for card in data.at("$cards") {
  if card.at("$kind", default: none) == "task" {
    field-region(card.at("$path") + "owner")[#card.owner]
    display(card.at("$path") + "due", "[year]")
  }
}

// 0.116
#for card in data.at("$cards") {
  if card.at("$kind", default: none) == "task" {
    ink(card).owner
    display(card, "due", "[year]")
  }
}

The 0.115 form still works. A field-region stays the tool for ink the plate composes: a sentence, a banner, a computed total. What each field's ink is: Print with ink.

The copy is an ordinary dictionary key, $ink, present with the dictionary's address prefix $path wherever at least one of its fields has ink. A composed value in a row loop claims its region through that prefix: field-region(row.at("$path") + "amount")[..]. Where a plate treats a typed dictionary as a map, skip both:

#for (key, value) in row.pairs().filter(((k, _)) => not k.starts-with("$")) { .. }

A plate resolves paths from its own file

A Typst plate is loaded at its plate_file path in the quill, and every other .typ file outside packages/ beside it, so paths resolve as Typst resolves them: a bare path from the file that names it, a /-rooted path from the quill root. A plate declared as tpl/layout.typ loaded as /main.typ, so its "assets/logo.svg" reached the quill's assets/; it now reaches tpl/assets/logo.svg and fails typst::file_not_found.

// 0.115, plate_file: tpl/layout.typ
#image("assets/logo.svg")
// 0.116
#image("/assets/logo.svg")

A plate at the quill root resolves every path as it did. The plate can now import the quill's modules (#import "lib.typ": header), where each such import failed typst::file_not_found.

body.unsupported is removed

A quill declared, per body, the block constructs its plate does not typeset, and a body holding one drew plate::unsupported_construct. Nothing checked a declaration against the plate, and no editor read it off the schema. The key is now unknown, so a Quill.yaml still declaring it fails to load:

main:
  body:
    unsupported: [rule]   # quill::invalid_body

Delete the key. A construct the plate does not typeset renders as nothing and raises no warning, as an undeclared one already did. usaf_memo drops its unsupported: [rule], so a rule in a memo body warns no more.

backend::declined_construct is unchanged: the Typst backend still warns for an image in a content field.