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:
/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:
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
# leadafter the sequence and dropped# c1and# inner. Nested inside a mapping (o:\n k:\n - a # c), it moved# conto the next key. - A comment indented less than its block: 0.121 moved
# noteafter the whole block and dropped# tail. - A compact nested sequence: 0.121 moved
# c1onto the outer dash line and# c2to the head ofl. - 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:
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.