Skip to content

0.120 → 0.121 — a matrix member is held by being present, a matrix reaches the plate as the members it holds, and an open matrix admits added items

The break that needs the most care is the one nothing reports: a matrix member spelled as a mapping naming no held flips from unticked to ticked. The value is well-typed under both readings, so no type checker, no load error and no diagnostic fires — the page prints a ticked box, and that member's columns with their answers, where it printed an unticked one. The same flip reaches a Quill.yaml seed: or array default: that spells a member that way.

The loud break is wider: a member mapping storing held at all fails validation as validation::held_stored, and the render with it. 0.120's strict write stored every bare tick as {held: true} — writer.set on any matrix, and quill.parse / conform and card seeding on a matrix with a content column — and the editor spelled held on every member it touched, so most stored documents holding a tick carry one. A Quill.yaml literal storing held fails to load: quill::seed_held_stored in a seed:, quill::default_held_stored in a default:. The section below says how to find both.

Every plate reading a matrix breaks, loudly. A matrix reaches the plate as the members its document holds, each its columns alone: no member carries held or a roster title, and an unheld member is absent. A Typst plate reading m.held or m.title fails to compile, and a member's tick is its own address, so a claim on <field>.<member>.held fails the compile and an acroform form.json binding on it fails to load. The plate section has the rewrite.

Break Surface Action
A matrix member spelled { detail: X } — a mapping naming no held — is held, where it was unheld document YAML, Quill.yaml seed: and default:, rendered output Remove every such member meant to be unticked. Nothing reports this one: find it as below
A matrix member mapping storing held fails validation as validation::held_stored, and the render with it document YAML, stored documents, diagnostics Delete held: true; remove a member stored held: false, whose answers go with it
A Quill.yaml seed: or default: storing held fails to load as quill::seed_held_stored / quill::default_held_stored Quill.yaml The same edit, in the literal
An unticked member keeps no answers in the document editors Hold a member's answers across an untick in the editor's own state, if a retick should restore them. Remove the member to untick it; clearing a held member's columns keeps it held
The strict write rests a bare tick as true and an empty mapping as true, where it wrote {held: true} stored documents None: the new rest is what the previous row asks for. Re-baseline a byte snapshot of a conformed document
A matrix's blueprint # e.g. line spells its member without held: true, and an open matrix's shows a new_item added item blueprint() output Re-baseline anything asserting on blueprint bytes
A matrix reaches the plate as the members it holds, each its columns: no held, no roster title, and an unheld member absent Typst plates A plate reading m.held or m.title fails to compile: print through roster(data, "<field>")
A member's tick is its own address, <field>.<member>; <field>.<member>.held is no address Typst field-region and form-field, acroform form.json A claim on .held fails the compile, and a binding on it fails to load as acroform::dangling_binding: drop the .held segment
A bare tick the boolean coercion refuses is validation::type_mismatch at <field>.<member>, where it was <field>.<member>.held diagnostics Match the new path
resolve() reports a matrix as the members it holds, where it listed every member with held editors reading resolve() Read the roster from schema(), and a member's tick from its presence
A dictionary declaring a matrix carries $path though none of its fields has ink Typst plates comparing, spreading or listing that dictionary's keys Skip $path as you skip $ink
The transform schema carries a matrix's roster as quillmark:roster, [id, title] pairs, where it carried quillmark:order, and a member's node holds its columns alone transform schema consumers; Rust QUILLMARK_ORDER_KEY is QUILLMARK_ROSTER_KEY Read the ids from the pairs
FieldType::Matrix carries open: bool; FieldSchema::members, one object per id, is FieldSchema::member, one shared, beside a new added_item; namespace_props answers None for a matrix Rust A FieldType::Matrix { roster } pattern takes ..; build a FieldSchema through FieldSchema::new; resolve a member through FieldSchema::matrix_member or matrix_member_at
ValidationError::EnumViolation carries open, and ValidationError::HeldStored is new Rust Spell open: false where you build the variant; an exhaustive match gains an arm

A matrix member is held by being present

A document ticks a member by writing it. The rule has one carrier, the member's presence, and the document never stores held:

qualifications:
  flight_cc: true                  # held, unchanged
  cyber_200: {}                    # was unheld; now held
  dodin_ops: { detail: "2024" }    # was unheld; now held
  dco: { held: true, detail: X }   # was held; now validation::held_stored
  sq_cc_candidate: false           # not held, unchanged

0.115 retired the reading that ticked a mapping naming no held, because it overrode the ladder for one key: { detail: X } and { held: false, detail: X } were two mappings one key apart meaning opposite ticks. Presence has no second key to disagree with it. A stored held is refused whichever value it holds: false under a present member contradicts the presence that holds it, and reading it either way is a guess. A held: null is absent, as at every type.

What moves with it:

  • An unticked member keeps nothing. Unticking removes the member, and its columns go with it. An editor that wants a retick to restore them keeps them itself.
  • An empty mapping ticks. {} is a member present with nothing answered. An editor clearing a held member's last column leaves it held; the strict write rests it as true.
  • The blueprint teaches less. Its # e.g. line no longer spells the held: true a mapping needed to tick.

Two finds. The loud one: before upgrading, every stored held: under a matrix member is the edit; after, quill.validate(doc) names each as validation::held_stored at its path, and a quill storing one names it at load. Delete held: true, and remove each member stored held: false.

The silent one is a mapping naming no held that was meant to be unticked, which only a hand or a model writes, in a document or in a quill's seed: or default:. Inline spellings narrow from:

grep -rn ':[[:space:]]*{' <your documents and quills> | grep -v held

A block mapping under a member id needs the roster: for each matrix field in your quills, search its member ids and read the lines under each hit.

A matrix reaches the plate as the members it holds

A matrix is a cell whose value is the set of held members, the way an array's value is its elements, so the plate carries what the document ticks and no more:

"qualifications": {
  "flight_cc": { "detail": {…} },
  "cyber_200": { "detail": {…} }
}

where 0.120 carried every member with held and the roster's title. The vocabulary to print, held or not, comes from roster(dict, key), a helper that reads the roster the backend generates, so a plate keeps no copy of it:

// 0.120
#for (id, m) in data.qualifications {
  [#(if m.held { "[x]" } else { "[ ]" }) #m.title — #m.detail]
}

// 0.121
#import "@local/quillmark-helper:0.1.0": data, ink, roster
#for row in roster(data, "qualifications") {
  [#(if row.held { "[x]" } else { "[ ]" }) #row.title]
  if row.held [ — #ink(row.value).detail]
}

A row is (id, title, held, value, path): value is the member's columns, none where unheld, and path its address. An unheld member has no columns to print, where 0.120 printed their blanks. Pass the dictionary that declares the matrix: roster(card, "qualifications") on a card, roster(row, "quals") in a table row.

A member's tick is its own address. A region or widget on qualifications.flight_cc.held moves to qualifications.flight_cc, and on acroform that address binds a checkbox its presence checks; columns keep their addresses. field-region(row.path)[…] claims a whole row, so an unticked box is clickable in an editor preview too.

Being a cell, a matrix now takes a default:, the set a document that leaves the field out holds, and type: matrix?, which renders an unanswered one none. Both failed to load in 0.120.

An open matrix admits items a document adds

open: true on a matrix lets a document add items beside the roster, each keyed by a snake_case id and carrying a title:

qualifications:
  flight_cc: { detail: UMS Flt/CC }
  wing_ig: { title: Wing IG, detail: 81 TRW }

Nothing changes for a closed matrix. On an open one:

  • The plate receives the held roster members in roster order, then each added item sorted by id, {title, …columns}, and roster returns them in that order. An added item's title is a cell with an address, so ink(row.value).title prints it click-to-edit.
  • A key outside the roster with no title, or one no snake_case id spells, is validation::enum_violation with open: true.
  • The transform schema carries the added item as the matrix node's additionalProperties, and schema() emits open: true (WASM QuillFieldSchema.open).
  • An acroform quill declaring one fails to load as quill::open_matrix_unsupported.