Skip to content

0.121 → 0.122 — only true and false are booleans, so y, N, yes and Off are text, a stamped checkbox carries both its states, a Typst plate at a path Typst refuses fails to open, a WASM options object refuses a key it does not read, a comment keeps the slot the YAML gives it, and every tag warns at its node

The break that needs the most care is the one nothing reports: an unquoted y, n, yes, no, on or off, in any case, reaches the plate as the text written, where it reached it as "true" or "false". YAML read each word as a boolean, and a string field printed the boolean's token, so middle_initial: N and country: NO both printed false. Each prints what was written. The value is a well-typed string under both readings, so no type checker, load error or diagnostic fires. The same holds wherever a word lands as data: a richtext or plaintext field, a string array, an undeclared field, and $ext, where a host reading collapsed: yes as true reads the string "yes".

The other break nothing reports is a consumer that reads a checkbox's /AP /N as one stream. It moves no document or field value: what moves is the structure of every checkbox the stamp spine writes, in the acroform backend's PDF and in quillmark_pdf::stamp output. Every stamped checkbox, checked or not, now carries a dictionary of both states there, /Yes and /Off, with /AS naming the current one. A reader that takes /N only as a stream finds no stream and draws an empty box where 0.121 drew the check mark. No error or diagnostic fires. A form filler and any viewer resolving /AS are unaffected, and gain a box they can toggle.

The third break nothing reports is a comment that re-emits at the slot the YAML gives it, where 0.121 moved or dropped it. 0.121 worked out a card-yaml block's structure from indentation, so a sequence at its key's column, a comment indented less than its block, a compact nested sequence or a trailer on a plain scalar's continuation line put a comment on another line, or lost it. 0.122 reads each comment's slot from the YAML parser's events. The values are unchanged and no diagnostic fires; the canonical markdown moves, so a hash over it recomputes. The comment section lists the spellings.

The fourth break nothing reports is a failed Typst compile whose error carries the quill's load warnings. Where a Typst render or open fails, the error's diagnostics list its errors, then each warning the quill's load raised (typst::unknown_key, typst::path_skipped and the package warnings), where 0.121 carried errors alone. diagnostics[0], its code and the error's message are unchanged. A host that counts or lists every entry as an error reads each one's severity instead.

The loud break of the boolean reading: a word under a boolean, number or integer field fails validation as validation::type_mismatch, and the render as validation::coercion_failed. consent: yes reaches coercion as the string "yes", and the boolean coercion takes the strings true and false alone. A matrix member's bare tick reads through the same coercion. A number or integer field that read count: yes as 1 refuses it. In Quill.yaml, a word where a boolean belongs fails to load.

A Typst quill whose typst.plate_file names a file the quill holds, at a path Typst's path grammar refuses, fails to open as typst::plate_path_invalid. Such a path holds a \, which Typst refuses on every platform, whether it is a character in a Unix file name or a Windows separator.

0.121 rendered such a quill, loading its plate as main.typ. A main.typ the quill holds then never loaded, and every #import "main.typ" or /main.typ reached the plate instead. A plate whose own file name held the \ drew a typst::path_skipped warning naming it; a Windows \ separator drew no warning.

engine.render, session.render and new Engine throw on an options key they do not read, where 0.121 dropped the key and went ahead on the default. TypeScript flags an unknown key only in an object literal, so an options object built elsewhere, or any call from plain JavaScript, meets the refusal at run time. The call that most needs it is engine.render(quill, doc, { format, today }), which rendered the local date in place of the one the caller asked for; it now rejects.

Break Surface Action
A word under a string, richtext or plaintext field, in a string array, in an undeclared field or in $ext is the text written, where it was true / false rendered output, plate JSON, hosts reading $ext, the canonical markdown and stored bytes of a source parsed again None for a word meant as text. Write true / false where a boolean is meant. Nothing reports this one: find it as below
A word under a boolean field, or as a matrix member's tick, fails validation as validation::type_mismatch and the render as validation::coercion_failed document YAML, diagnostics Write true / false
A word under a number or integer field fails validation as validation::type_mismatch and the render as validation::coercion_failed, where the render floor read 1 / 0 document YAML, diagnostics Write the number
A word under an enum whose values: hold true and false fails as validation::enum_violation, where it read as that member document YAML, diagnostics Write true / false
A word under a date field fails as validation::format_violation, where it failed as validation::type_mismatch diagnostics None: the value fails either way
!!bool on a word fails the parse as parse::yaml_error_with_location, and Quill.yaml's load as quill::yaml_parse_error, where it read as the boolean document YAML, Quill.yaml, QuillValue::from_yaml_str Drop the tag, or write true / false
A word as a boolean default: fails to load as quill::default_type_mismatch, and in a card kind's seed: as quill::seed_type_mismatch Quill.yaml Write true / false
A word as inline:, open:, ui.compact or ui.multiline fails to load as quill::field_parse_error, and as body.enabled as quill::invalid_body Quill.yaml Write true / false
A word in values:, as a matrix member's title, or as a string field's default: is the string written, where it failed to load Quill.yaml None: values: [US, NO, DE] loads
A document value NO under an enum declaring NO is that member, where it was validation::enum_violation document YAML None
QuillValue::from_yaml_str reads the words as strings Rust None, unless a caller relied on the word forms
A stamped checkbox's /AP /N is a state dictionary, /Yes and /Off, that /AS selects from, where a checked box carried one stream and an unchecked box no /AP acroform PDF output, quillmark_pdf::stamp Resolve /N through /AS; for a rasterizer that cannot, stamp with AppearanceStates::Selected
StampOptions gains a public field, states Rust A struct literal spells ..StampOptions::default()
Rendered PDF bytes move wherever a checkbox is stamped hashes or snapshots of rendered acroform PDFs Re-baseline
A plate_file naming a file at a path Typst refuses fails open as typst::plate_path_invalid, where it rendered with the plate loaded as main.typ Typst quills, diagnostics Separate typst.plate_file with /, and rename a plate whose own file name holds a \
today among the render options rejects engine.render, where it was dropped and the local date rendered WASM engine.render Pass the date as the trailing argument: engine.render(quill, doc, options, today)
A render options key outside format, ppi, pages and regions rejects engine.render and throws from session.render WASM render calls Fix the key's spelling, or drop it
An EngineOptions key other than backends throws at new Engine(...) WASM engine construction Spell it backends
A null-prototype or cross-realm Addr or CardInput throws on a key it does not read, where 0.121 dropped the key WASM Drop the key
Addr, Content, Delta, PathStep and Resolved in runtime.d.ts's writer and reader signatures are their declared types, where each read as any WASM TypeScript Fix what tsc names, such as a Content that may be undefined
A failed Typst render or open carries the quill's load warnings after its errors, where every diagnostic was an error Rust RenderError, WASM and Python QuillmarkError.diagnostics Read each entry's severity
A comment in one of the spellings below re-emits at its own slot, where it moved or dropped to_markdown output, hashes over it None. Find the documents it touches as below
A sequence item carrying a trailer beside its first key's emits - # item with the key on the line below, where the key's trailer moved under the first field's value to_markdown output None
parse::unsupported_yaml_tag fires on every tagged node, at its own path diagnostics, quillmark check --strict, quillmark validate Expect warnings on tags inside flow collections, on sequence elements and on keys, and match the corrected paths. check --strict and validate's example check fail on each: drop the tag
A column-zero key outside the field grammar fails as parse::invalid_structure naming the field, where og:title: "Issue #5" failed as a YAML error diagnostics Rename the field
A quoted top-level key keeps its source position on emit, where it moved to the end to_markdown output None
A block whose comments and tags record more than 64 bytes of path per byte of the block, plus 64 KiB, each level costing its key and a fixed overhead, fails as parse::invalid_structure diagnostics Merge the comments, or move them nearer the top level
Payload::nested_comments lists a parsed block's comments in emit order, and a trailer on a sequence item's first key below a bare dash is the item's Rust, storage DTO of a re-parsed document None, unless a consumer compared the list to 0.121's
A stored quillmark/document@0.82.0 row's $id drops with its inline trailer, which re-emitted on the item above to_markdown of a legacy row, and the @0.116.0 bytes it rewrites to None

Only true and false are booleans

Every YAML read in the engine takes only true and false, in any letter case, as booleans: a card-yaml payload, Quill.yaml, and QuillValue::from_yaml_str.

consent: true         # boolean, unchanged
draft: FALSE          # boolean, unchanged
middle_initial: N     # was false; now "N"
country: NO           # was false; now "NO"
size: Off             # was false; now "Off"
tags: [y, n, on]      # was [true, false, true]; now ["y", "n", "on"]
quoted: "y"           # "y", unchanged

The YAML 1.1 reading turned a middle initial or a country code into false, and nothing told the author. A field declared boolean gets no word forms in exchange: the boolean coercion takes the strings true and false, in any letter case, and no other string.

The emitter is unchanged: to_markdown quotes every word ("yes"), so a document already parsed emits the same bytes across the step, and a YAML 1.1 reader of emitted markdown reads each word value as a string too. A source parsed again holds the text where 0.121 held the boolean, so its canonical markdown, its stored bytes and any hash over them move.

The reading happens at parse. A document parsed and stored before the upgrade holds the boolean its parse read: a stored middle_initial: false stays false, in the row and in its to_markdown. Re-enter the value to restore the text.

Finding the words that move

The loud ones name themselves. quillmark check (or quill.validate(doc)) names each word under a boolean, number or integer field as validation::type_mismatch at its path, a matrix member's tick included, and quillmark validate names each word written where a Quill.yaml takes a boolean.

The silent ones are words under every other field, and in $ext. A word meant as text printed true or false, and prints itself. A word meant as a boolean is a string, which a plate or host testing for true reads as not true. Unquoted words, as values and as flow elements, narrow from:

grep -rniE '(^|[,:[{-])[[:space:]]*(y|n|yes|no|on|off)[[:space:]]*($|[]},#])' <your documents and quills>

A stamped checkbox carries both its states

ISO 32000-1 §12.7.4.2.3 gives a check box widget an appearance dictionary naming its on state and Off. The spine writes that shape for every checkbox, whatever its value:

/AS /Off /V /Off
/AP << /N << /Yes 12 0 R /Off 13 0 R >> >>

/Yes draws the check mark and /Off is an empty stream over the same /BBox: the background owns the box, so the mark is all the on state draws. The on-state name stays Yes (CHECKBOX_ON_STATE).

A stage that fills the delivered PDF toggles a box by renaming /AS and /V, as pypdf does, with no appearance to regenerate. In 0.121 that stage had no state to switch to: a checked box had one stream and an unchecked box none, so filling a later section meant re-rendering from YAML, which discards any signature already applied.

A caller of stamp handing the result to a rasterizer that draws /N without resolving /AS — hayro 0.7, which the acroform canvas uses, is one — stamps with the new option for that rasterizer:

use quillmark_pdf::{stamp, AppearanceStates, StampOptions};

let opts = StampOptions {
    states: AppearanceStates::Selected,
    ..StampOptions::default()
};
let preview = stamp(base_pdf, &fields, &opts)?;

Selected writes each box's /N as the one stream its /AS names, so the box draws, but a filler has no other state to switch to: it makes a preview, never a deliverable. The acroform backend's canvas raster is stamped this way, and render() hands back the AppearanceStates::All stamp. The canvas looks as it did in 0.121.

A Typst plate at a path Typst refuses fails to open

The plate loads at its declared path, so a module reaches it, and it reaches a module, by the paths Typst resolves. A path Typst refuses leaves the plate no name of its own, and any stand-in may belong to a file the quill holds, so the backend refuses the plate:

typst:
  plate_file: lay\out.typ   # typst::plate_path_invalid

The refusal fails every surface that reads the plate: a render, a session's open, quillmark validate's render, and quillmark workspace. A plate_file naming no file at all stays typst::plate_missing, whatever its spelling.

An options key the verb does not read throws

// 0.121: renders the local date. 0.122: rejects.
await engine.render(quill, doc, { format: "pdf", today: "2026-03-14" });

// Both: renders 2026-03-14.
await engine.render(quill, doc, { format: "pdf" }, "2026-03-14");

A session takes its date once, at engine.open(quill, doc, today), so session.render({ today }) throws the same way. A misspelled key fails alike: { fromat: "svg" } rendered the default format, and new Engine({ backend }) kept the bundled build where backends would have overridden it.

The refusal is a QuillmarkError whose one diagnostic carries no code, the shape a malformed Addr or CardInput already throws: a call site to fix, not a condition to route on. Its message names the key, and for today where the date goes.

The first call through each site finds it. To find them ahead of a run, search the options objects passed to render and new Engine for a key outside the ones above, an object spread from a wider config included.

A comment keeps the slot the YAML gives it

0.122 reads each comment's container and slot from the YAML parser's events, and to_markdown writes it back there. The spellings that moved a comment:

to:
# lead
- name: a # c1
  # inner
  rank: 1
classification:
  value: CUI
# note
  controlled_by: SAF/AA # tail
l:
  - - x # c1
    - w # c2
note: first
  key:value # z
  • A sequence at its key's column, as PyYAML and kubectl write it: 0.121 moved # lead after the sequence and dropped # c1 and # inner. Nested inside a mapping (o:\n k:\n - a # c), it moved # c onto the next key.
  • A comment indented less than its block: 0.121 moved # note after the whole block and dropped # tail.
  • A compact nested sequence: 0.121 moved # c1 onto the outer dash line and # c2 to the head of l.
  • A trailer on a plain scalar's continuation line: 0.121 dropped # z.

A document stored through the DTO keeps the comments its 0.121 parse read, and re-emits them where 0.121 did, but for a sequence item carrying a trailer beside its first key's, which emits in the bare-dash form. Re-parse it from its markdown source to place them by the YAML.

Comment moves leave no diagnostic. Parse each document's markdown source under 0.121 and under 0.122, emit it with to_markdown, and diff: only documents with a comment in the spellings above change. Sequences at their key's column, the spelling PyYAML and kubectl write, narrow from:

grep -rnE -B1 '^[[:space:]]*- ' <your documents> | grep -E ':[[:space:]]*$'

then compare the indentation of each key: line with the dash below it.

Every tag warns at its node

A tag on any node warns as parse::unsupported_yaml_tag at that node's path: a sequence element (main.notes[0]), a node inside a flow collection (main.flow[1]), a key, a value after an anchor or on the line below its key. 0.121 warned only for a tag opening a block key's value on the key's own line, and named a wrong path where a sequence sat at its key's column (main[3].name for main.to[0].name), a comment sat left of its block (main.controlled_by for main.classification.controlled_by) or a dash line held only an anchor (main.l.y for main.l[0].y). The code is unchanged and the value is kept as read, as before.