@quillmark/wasm 0.114.0 → 0.116.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,382 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.116.0 - 2026-09-26
4
+
5
+ Upgrade path: [0.115 → 0.116](docs/migrations/0.115-to-0.116.md).
6
+
7
+ ### What blocks a render
8
+
9
+ - feat(core)!: **a card no kind claims, and a body under `body.enabled: false`,
10
+ render and warn.** A render fails only where the engine would have to invent
11
+ what the author wrote: a value that will not read as its declared cell's
12
+ type, or markup the grammar cannot read. Input no declaration claims
13
+ displaces nothing, so it renders and warns; SCHEMAS.md § "What blocks a
14
+ render" sorts every validation code into incomplete, malformed and
15
+ unclaimed. `validation::unknown_card` and `validation::body_disabled` are
16
+ `Severity::Warning` from `Quill::validate`, `unknown_card` carries
17
+ `allowed`, and an unclaimed card keeps its place in `$cards` with its
18
+ fields verbatim. `ValidationError::UnknownCard` and `BodyDisabled` are
19
+ deleted. This is the release's silent break: a pipeline that relied on the
20
+ render failing catches nothing. (#1877)
21
+ - feat(core)!: **an undeclared key warns `validation::unknown_field`**, at
22
+ every depth of a claimed card: a card field, a typed dictionary's property,
23
+ an array element's, a matrix member's column, and a cell inside a variant
24
+ container no world declares. `args` carry `suggestion`, the closest declared
25
+ field the document leaves unwritten, or `container` and `variant` for a
26
+ variant cell written beside its discriminant rather than under it. (#1871)
27
+ - feat(quillmark,cli)!: **a one-shot render carries validate's warnings.**
28
+ `Quillmark::render`, WASM `engine.render` and Python `render` put every
29
+ `Quill::validate` warning on the result's `warnings` ahead of the compile's,
30
+ so the unclaimed input the page left out is on the result a pipeline reads.
31
+ A session's warnings stay its current compile's. (#1931)
32
+ - feat(core)!: **a card-yaml block with no `$kind` fails the parse** as
33
+ `parse::missing_kind`, located at its opener, with a hint naming both fixes:
34
+ a `$kind:` line, or a backtick fence. Such a block was a card missing its
35
+ kind line or tilde-fenced code, and reading it as either was a guess. Every
36
+ card now carries `$kind`: `ResolvedCard.kind` is `String` (`string` in
37
+ TypeScript), and a stored card with no `$kind` folds into the body above it
38
+ on load, its payload as a code block, then its body. (#1905)
39
+ - feat(core)!: **a card-yaml payload that is not a mapping fails at its
40
+ fence** as `parse::payload_not_mapping`, located at the opener, with `actual`
41
+ and `info` args and a hint naming the backtick fence, where it was an
42
+ unlocated `parse::invalid_structure` on a card and `parse::missing_quill` on
43
+ the root. (#1873)
44
+
45
+ ### `!must_fill` is removed
46
+
47
+ - refactor(core)!: **`!must_fill` and `validation::must_fill` are gone.** The
48
+ marker said a cell held a placeholder rather than an answer, and the schema
49
+ half of the warning read `default:`'s absence as an obligation. `default:`
50
+ is the value an unanswered cell renders, `?` moves the render floor, and
51
+ null ≡ absent holds on every surface. A value under the marker keeps its
52
+ value: in markdown the tag drops as any unknown tag does, warned
53
+ `parse::unsupported_yaml_tag` at the tagged node's rooted path (which that
54
+ code now carries), and a stored row of any earlier tag is read past its
55
+ `fill` keys. Deleted: `store_fill`,
56
+ `is_fill`, the `QuillValue` fill set, `FieldSchema::{must_fill,
57
+ example_content}`, `edit::fill_on_mapping`,
58
+ `parse::fill_marker_unsupported_position`, WASM `storeFill` / `isFill`, and
59
+ the card wire's `fill` / `nestedFills`. (#1906, #1933)
60
+ - refactor(core)!: **the storage tag is `quillmark/document@0.116.0`.** A
61
+ field item carries no `fill` or `nested_fills`, and a row carrying either,
62
+ or any key its type does not name at the document, card, payload or item
63
+ level, fails the load, as does a nested comment spanning lines, which
64
+ `to_markdown` would have written as bare YAML. Every earlier tag still
65
+ reads. A row's bytes move on its next save, so content hashes
66
+ recompute and a 0.115 build refuses the row from then on.
67
+ - feat(core,wasm,python)!: **a card-wire payload item refuses an unknown
68
+ key.** A 0.115 integration still sending `{value, fill: true}` committed the
69
+ placeholder as an answer, and an item typo vanished; `insertCard` throws and
70
+ `insert_card` raises `ValueError` instead. (#1933)
71
+ - feat(core): **a `?` on a cell's type renders it `none` when unanswered**,
72
+ so a plate can tell "0 votes against" from "nobody entered a tally". `?` and
73
+ `default:` are exclusive (`quill::optional_default`), a namespace takes no
74
+ `?` (`quill::optional_namespace`), and the transform schema projects
75
+ `type: [t, "null"]`. (#1880)
76
+
77
+ ### The blueprint and the seed
78
+
79
+ - feat(core)!: **seeding commits no `example:`.** An example documents a
80
+ cell's shape rather than answering it, and `body.example` is guide text
81
+ quills write as instructions. `seed_document`, `seed_main` and `seed_card`
82
+ commit the `$seed` overlay alone; every other body is empty and every other
83
+ field absent. (#1923)
84
+ - feat(core,wasm,python)!: **the blueprint answers nothing on the author's
85
+ behalf.** A defaultless cell is empty (`title: # string`) with its
86
+ `example:` on a `# e.g.` line above it; every body is empty, a
87
+ `body.example` riding a `# body e.g.` line that closes the payload and a
88
+ kind under `body.enabled: false` closing with `# no body`; a card's
89
+ description rides its `$kind` line; a `default: []` table is led by the
90
+ whole field commented out holding its row; and a matrix names its columns
91
+ in an `# e.g.` line, a column with no example showing its
92
+ `<type>[<format>]`. (#1874, #1942)
93
+ - feat(core,wasm,python)!: **`FORMAT_RULES` is `MARKDOWN_RULES`**, headed
94
+ `Quillmark Markdown rules:` (`markdownRules` / `markdown_rules`), and keeps
95
+ only what departs from plain Markdown and YAML. `blueprint_instruction` is
96
+ deleted in every binding: a prompt is the consumer's to write. (#1942)
97
+ - feat(core)!: **`to_markdown` writes a multi-line string as a `|` literal
98
+ block** (`|-` without a trailing newline), at every depth, where it wrote
99
+ one double-quoted line. A string a block would not hold verbatim stays
100
+ quoted. The value round-trips unchanged; the markdown bytes move. (#1883)
101
+
102
+ ### The quill authoring contract
103
+
104
+ - feat(core)!: **a field's and a card kind's label is a top-level `title`**,
105
+ beside `description`, which `schema()` emits and the blueprint prints as
106
+ `# <title> — <description>`. `ui.title` fails the load, a `{field}` token
107
+ in a `title` is `quill::title_template`, and a `title` on an array's `items`
108
+ is `quill::title_on_items`. (#1922)
109
+ - refactor(core,wasm)!: **`body.unsupported` is removed**, with
110
+ `plate::unsupported_construct`, `Quill::unsupported_constructs` and the WASM
111
+ `QuillBlockConstruct`. Nothing verified a declaration against the plate and
112
+ no editor read it; a `Quill.yaml` still declaring it fails
113
+ `quill::invalid_body`.
114
+ - fix(core)!: **an unquoted `quill.version` reads as written**: `version:
115
+ 1.10` is `1.10`, where it loaded as `1.1`, and `1.1e1` fails
116
+ `quill::invalid_version`.
117
+ - feat(core)!: **a `plaintext(inline)` value ending in a newline names `|-`
118
+ as the fix.** `validation::not_inline`, `edit::field_not_inline` and
119
+ `conform::field_not_inline` carry `trailingNewline: true`, and
120
+ `ValidationError::NotInline` / `EditError::FieldNotInline` gain
121
+ `trailing_newline`. (#1918)
122
+ - fix(core): `quill::invalid_name` suggests a name the loader accepts.
123
+
124
+ ### The render date
125
+
126
+ - feat(core)!: **the host supplies the render date; a `date` field may hold
127
+ `today`.** The engine reads no clock. `Quillmark::open` / `render`,
128
+ `Quill::compile_data` / `compile_checked` / `resolve`,
129
+ `QuillConfig::compile_data` / `compile_checked`, `TypedReader::resolve` and
130
+ `Backend::open` take a required `today: CalendarDate`; a plate's
131
+ `datetime.today()` returns it whatever its `offset:`. The CLI (`--today`)
132
+ and the WASM and Python bindings default to the local date. A `date` value
133
+ is `YYYY-MM-DD` or `today`, and the blueprint annotates `date<YYYY-MM-DD |
134
+ today>`. `CalendarDate`'s parse fails with `ParseDateError`, which tells a
135
+ day the calendar lacks from a string that is no date. (#1900, #1930)
136
+ - feat(fixtures)!: `usaf_memo`'s blank `date` renders a fill-in field where it
137
+ rendered today's date.
138
+
139
+ ### Plates and the Typst backend
140
+
141
+ - feat(typst)!: **a dictionary carries the `$ink` twin its fields print
142
+ through.** A scalar printed off `data` kept its click target only as a
143
+ direct read in the plate. `data`, each card, and each typed dictionary or
144
+ table row carry `$ink` and `$path` wherever a field has ink: compute with the
145
+ dictionary, print its ink (`ink(row).org`). A date's ink prints its default
146
+ display, and `display(row, "due", "[year]")` formats one by dictionary and
147
+ key. The keys are visible to `keys()`, `pairs()`, `len()`, `==` and a
148
+ spread. (#1868)
149
+ - feat(typst)!: **a plate imports the quill's `.typ` files by Typst's path
150
+ rules.** The plate loads at its `plate_file` path and every `.typ` file
151
+ outside `packages/` beside it, so a bare path resolves from the importing
152
+ file and a `/`-rooted one from the quill root. Plate diagnostics name the
153
+ declared `plate_file`, where they named `main.typ`, and a vendored package's
154
+ file under its spec (`@local/p:0.1.0/lib.typ`), apart from a quill module
155
+ of the same path. (#1869)
156
+ - feat(typst): **a `typst:` key the backend does not read warns
157
+ `typst::unknown_key`**, `typst.packages` included; a package is vendored
158
+ under `packages/`, never fetched. A package manifest without `version` or
159
+ `entrypoint` is skipped at load.
160
+ - refactor(typst): the `when` / `value-or` plate helpers are dropped; a plate
161
+ reads an optional cell with `!= none`.
162
+ - fix(typst): a negative scalar's ink lexes, and region queries stay linear.
163
+
164
+ ### Rendering, the CLI and the bindings
165
+
166
+ - feat(core)!: **one raster ceiling, 16384 px a side**, `MAX_RASTER_SIDE`,
167
+ owned by core, where core refused past 16384² px of area and the WASM
168
+ painter clamped by side on its own. A PNG export long on one axis that fit
169
+ by area now fails `backend::invalid_raster_scale`.
170
+ - feat(wasm)!: **`paint(ctx, page, scale)` takes one scale and returns
171
+ nothing**; `PaintOptions` and `PaintResult` are deleted, and a missing or
172
+ non-finite scale throws `backend::invalid_raster_scale`.
173
+ - feat(cli): **`quillmark workspace` exports a plate for Typst's own
174
+ tooling**: the generated helper for one document, every vendored package
175
+ and the backend's fonts, with the `typst watch` command over them. A rerun
176
+ replaces the `packages/` and `fonts/` an earlier export wrote. The facade
177
+ re-exports it as `quillmark::typst_workspace`. (#1869)
178
+ - feat(cli): **`quillmark check` prints a document's every diagnostic**, and
179
+ `--strict` exits `1` on a warning, for a CI gate over a repository of
180
+ documents; `render` prints the warnings for what the page leaves out.
181
+ (#1872)
182
+ - fix(cli)!: `render` takes its format from an `-o` extension naming one, and
183
+ a `-f` the extension contradicts exits `1` before writing.
184
+ - feat(core,wasm,python): `Quill.emptyDocument()` / `Quill.empty_document()`
185
+ reach the bindings, and WASM `isInline` / `isPlain` export the content
186
+ predicates. (#1919)
187
+
188
+ ### Fixes
189
+
190
+ - fix(core): the YAML prescan keeps comments, tags and anchors with the value
191
+ they sit on across continuation lines, dash lines, block scalars, multi-line
192
+ flow collections, a value below its key, nested keys with an inner `:` or a
193
+ leading `-`/`?`, and a removed item's trailing comment; a `$seed` overlay edit keeps the comment on
194
+ its `$seed` line.
195
+ - fix(core): `to_markdown` projects a content object at every depth of a
196
+ field's value, and a container under a sequence item's first key
197
+ round-trips.
198
+ - fix(core): a folded island past `isl-{u64::MAX}` mints a fresh id, and a
199
+ folded card's `$ext` stays out of the body.
200
+ - fix(core): the unknown-key walk stays linear in a mapping's keys.
201
+ - fix(core): a `$seed.<kind>.$body` for a kind under `body.enabled: false`
202
+ warns `validation::seed_unknown_field`, where the seed dropped it silently.
203
+ - fix(cli): `validate -v` reports a backend load warning once, where each of
204
+ the three canonical renders repeated it.
205
+ - fix(quillmark): the facade re-exports `ParseDateError`.
206
+
207
+
208
+ ## v0.115.0 - 2026-09-22
209
+
210
+ Upgrade path: [0.114 → 0.115](docs/migrations/0.114-to-0.115.md).
211
+
212
+ ### The quill authoring contract
213
+
214
+ - feat(core)!: **a `matrix` member mapping takes the ordinary ladder.** Two
215
+ rules decided what a stored member spelling meant — a bare scalar is the tick,
216
+ and a mapping naming no `held` is held — and the second retires. A member is
217
+ an `object` of a synthesized `held: {type: boolean, default: false}` beside
218
+ its columns, so a mapping is the member object already and the tick it names
219
+ no key for resolves as every other absent cell does, to its `default:`. The
220
+ old rule overrode that ladder for one key in one type, and bought a spelling
221
+ convenience for it: two mappings one key apart meant opposite ticks, the
222
+ shorter one ticked, and any producer building the member object without
223
+ conforming — an editor writing a column before the tick, a serde-built
224
+ payload, `{}` — set a tick it never wrote. `{ detail: X }` is now unheld with
225
+ its detail retained, which is what `{ held: false, detail: X }` already meant;
226
+ the bare scalar and every mapping naming `held` are unchanged. What remains is
227
+ exactly `conform_variant`'s bare-scalar rule, so `SCHEMAS.md` §Matrix lists
228
+ three things as the type's own rather than four. The flip is silent — the
229
+ value is well-typed under both readings, and a defaultless column's
230
+ `validation::must_fill` leaves with the tick it was gated on — so the upgrade
231
+ guide leads with it. (#1860)
232
+ - feat(core): **a typed dictionary's property opens a variant world.**
233
+ `variants:` was card-level, so a conditional block had to sit at the top of a
234
+ card even where the fields it governs belong to a dictionary. The rule was
235
+ never about depth: what stays one level deep is the gap between a field's
236
+ *declared* shape and its *live* one, and a dictionary's own shape is the
237
+ schema's — so a world opened there deepens the address and leaves the gap
238
+ alone. `header.classification` reads, binds, branches and strands exactly as
239
+ `classification` does, one step down. Every other position would deepen the
240
+ gap and still refuses one: an array element, whose liveness would vary per
241
+ index against a form bound once; a matrix column, inside a grid the page
242
+ prints in full; and another variant's cell, which would make the plate's one
243
+ branch a tree. `quill::variant_placement` now names the position carrying the
244
+ ban rather than restating the old rule, and the ban is **sticky** — an object
245
+ inside any of them inherits it instead of laundering a world into a position
246
+ that cannot hold one. The surfaces that dispatch on the container before
247
+ walking the type needed nothing (coercion, validation, the render floor,
248
+ conditional `must_fill`, the transform schema, `acroform` binding); the three
249
+ that scanned card fields now descend, so `validation::out_of_variant` finds a
250
+ strand at `main.header.classification.controlled_by`, the blueprint seats a
251
+ world's cells — live or commented — at the dictionary's indent, and the seed
252
+ resolves a nested discriminant before walking its field set.
253
+ - feat(core)!: **`ui.layout: table` contracts its columns.** The key was checked
254
+ for its outer shape alone — `type: array` whose `items` is an `object` — so a
255
+ row holding a container loaded clean and no editor could draw it: the author
256
+ wrote a key, the loader took it, and a surface ignored it with no signal at
257
+ either end. A column that is not a leaf is now
258
+ `quill::table_column_not_flat`, naming the column (`appendices[].entries`)
259
+ rather than the field. This splits the key along a seam it always had: a
260
+ **contract** that every column is a leaf, settled at load, and a **request**
261
+ to draw a grid, which an editor still declines for a width or a cell renderer
262
+ it does not have. Shape leaves the decline list, since shape was never a
263
+ consumer's judgement to make. The boundary is containment, not height — prose
264
+ is a column whatever its `inline`, because how tall a cell renders is the
265
+ editor's call and what it contains is not. `FieldType::is_leaf` is the one
266
+ definition, an exhaustive match a new type cannot skip, and `acroform`'s
267
+ `is_scalar_or_prose` is gone in favor of it: the same question had two
268
+ answers in two crates. Part of #1859.
269
+ - feat(core,wasm,python): **the empty document has a constructor.**
270
+ `Quill::empty_document()` is `Document::new` under the quill's own reference,
271
+ so the three canonical documents are three constructors beside
272
+ `QuillConfig::blueprint()` and `Quill::seed_document()`, and no caller spells
273
+ `name@version` to reach the one a plate is bound to render. `quiver_test.rs`
274
+ reads it rather than formatting the two-line markdown itself. The bindings
275
+ carry it as `Quill.emptyDocument()` / `Quill.empty_document()`.
276
+ - feat(cli)!: **`quillmark validate` compiles the plate.** It renders the empty
277
+ document, the blueprint and the seed through the quill's backend at the
278
+ backend's first declared format, and reports each failure as a
279
+ `cli::canonical_document_failed` error naming which of the three it is,
280
+ followed by the backend's own diagnostics. The contract that a plate renders
281
+ the empty document was checked for the fixtures quiver alone, and `render`
282
+ with no input file renders the seed, the fullest of the three: a plate that
283
+ reads a blank badly passed both. A quill whose plate does not compile now
284
+ exits `1` where it exited `0`, and a backend that does not resolve is
285
+ `cli::backend_unresolved` followed by the engine's own diagnostic. A render's
286
+ warnings join the `-v` list, and `validate --no-render` is the configuration
287
+ read on its own. Closes #1844.
288
+
289
+ ### The content model
290
+
291
+ - refactor(content)!: **an island stores its value, not its provenance.** `Loss`
292
+ had one minting rule — a table cell dropping an inline image's url — and no
293
+ reader: export dispatches on an island's `type`, the Typst emitter and the
294
+ acroform binder never looked, and an `islandOps` `set` stored whatever class
295
+ the op named. Two cells written `| a cat |` and `| ![a cat](cat.png) |` mint
296
+ equal `props` and differ only in the markdown behind them, which the
297
+ projection has no syntax to carry back out, so the class survived neither a
298
+ `to_markdown` → `from_markdown` hop nor re-derivation from the row holding it
299
+ — which is why the content fixed point had to be stated modulo it. An
300
+ island's canonical form is `{id, type, props}`, the fixed point is exact, and
301
+ the closed vocabularies cover the four discriminators a projection dispatches
302
+ on. `Loss`, `Island::loss`, `Island::with_loss`, `IslandType::default_loss`,
303
+ `ContentIsland.loss` and `ContentLossClass` are deleted; a stored row spelling
304
+ `loss` opens with the key ignored and absent from what comes back, and a
305
+ `loss` on an authored island or an `islandOps` entry is ignored rather than
306
+ refused. A host that surfaced the class surfaces the drop at the import
307
+ instead, where the markdown it is about is still in hand. Closes #1757.
308
+ - refactor(content,core,wasm,python)!: **a block island's line is a `para`, and
309
+ `LineKind::Island` is gone.** The kind was a second copy of a fact the island
310
+ beside it already carried: markdown writes a `table` as a block and an `image`
311
+ inline, so `IslandType::block_only` settled which line a lone slot sat on and
312
+ `normalize` wrote that answer back over whatever was stored. Every read site
313
+ now asks the island — export already dispatched on the type, the Typst emitter
314
+ already lowered `Island` as `Para`, and the one fact the kind alone still
315
+ answered, "this block renders one line", moves to the new public
316
+ `Content::block_island_at`, which `normalize` reads beside
317
+ `takes_continuations` when it clears a stranded `continues`. Landing a block
318
+ island is two channels rather than three: the delta opens the line, the island
319
+ op fills it, and the `setKind` goes. The TS `ContentLineKind` loses its
320
+ `{ kind: "island" }` arm, so a host branching on it typechecks red; a `setKind`
321
+ still naming `island` lands as the `para` it would have settled to.
322
+ - refactor(core)!: **the storage tag is `quillmark/document@0.115.0`.** The two
323
+ lines above move canonical bytes — the `loss` key for every document holding
324
+ an island, the line kind for every document holding a table — so content
325
+ hashes recompute once on those rows. `DocumentV0_112_0` freezes with a raw
326
+ `body` and the new `DocumentV0_115_0` carries the `CanonicalContent`; the hop
327
+ between them is that body's decode, and `V0_93_0 → V0_112_0` becomes a retag
328
+ now that both trees spell `body` raw. Stored rows need no migration: the
329
+ content decoder reads `island` as `para` wherever it meets it, permanently,
330
+ since the same spelling rests untagged inside every `richtext` field and
331
+ `$seed` overlay, where no hop reaches it. Closes #1647.
332
+
333
+ ### Schema, validation and the resolved view
334
+
335
+ - refactor(core)!: **a schema is loaded, not deserialized.** `QuillConfig`,
336
+ `CardSchema` and `FieldSchema` serialize and no longer implement
337
+ `Deserialize`, and `FieldSchema::from_quill_value` is crate-internal. A field
338
+ arrives in two steps — the parse, which folds the sibling keys (`inline:`,
339
+ `values:`, `members:`, `max:`) into the type payload, and the shape walk,
340
+ which rejects an `object` without `properties`, an `array` without `items`,
341
+ an empty `properties` map, and the placements — and only `parse_fields` ran
342
+ both, so a config assembled through serde carried states no `Quill.yaml` can
343
+ reach, absorbed downstream without a diagnostic. Gating the serde path
344
+ instead would have duplicated the walk and collapsed its diagnostic codes
345
+ into `quill::field_parse_error` at that boundary. The way to a config is
346
+ `QuillConfig::from_yaml_with_warnings`, or a `Quill` read off a file tree;
347
+ serialization is untouched, so `schema()`, `schema_yaml()` and every emission
348
+ read the same loaded form they did. Closes #1836.
349
+ - refactor(core,wasm)!: **a matrix roster is one flat mapping; group headings
350
+ are gone.** A block's `group:` was declared in the schema and absent from the
351
+ value, the address and the vocabulary — the blueprint emits
352
+ `matrix<a | b | c>` off one flat key space, so the partition reached the
353
+ visual editor and nowhere else, buying one shared `properties` map for a
354
+ schema concept, its load rules and a reserved key. `members:` is now
355
+ `{id: Title}` directly, `MatrixGroup`, `MATRIX_GROUP_KEY` and the TypeScript
356
+ `QuillMatrixGroup` are deleted, and `FieldType::Matrix` carries an
357
+ `IndexMap<String, String>` read through the new `FieldType::matrix_roster`,
358
+ which replaces `matrix_members`. The projection stops writing `group` onto
359
+ every member, which returns `group` to the column names a quill may declare,
360
+ and `quill::duplicate_matrix_member` retires: the roster is a mapping, so it
361
+ has one slot per id exactly as the stored value does and a duplicate is
362
+ unspellable on both sides. Closes #1851.
363
+
364
+ ### The blueprint
365
+
366
+ - feat(core)!: **a dormant variant world is its own cells, commented out.** A
367
+ variant-bearing enum's blueprint named every world it could not show, one
368
+ `# when CUI: controlled_by, poc, category` line each — the one place on the
369
+ surface where a cell was named rather than shown, dropping the position,
370
+ obligation, type, description and example a cell carries. Every world now sits
371
+ under a `# when <MEMBER>:` header in declaration order: the selected world's
372
+ cells live, every other world's the same cells with a `# ` in front, at the
373
+ slot they would occupy live. A reader activates a world by setting the
374
+ discriminant and deleting `# `. One builder and one emitter serve both, so a
375
+ dormant line is byte-for-byte the live line, and the block round-trips through
376
+ `Document::parse` as the comments it is, reaching neither the validator nor
377
+ the render floor. Closes #1846.
378
+
379
+
3
380
  ## v0.114.0 - 2026-09-18
4
381
 
5
382
  ### Schema, validation and the resolved view
package/README.md CHANGED
@@ -150,8 +150,7 @@ document was built. Parse warnings and the `conform::*` warnings both ride
150
150
  other way (`fromStored`, a stored row), returning the `conform::*` `Diagnostic[]`
151
151
  (`[]` when everything rested). It is idempotent and a byte no-op on an
152
152
  already-canonical document, YAML comments included, so calling it on every load
153
- is safe. A `!must_fill` marker anywhere in a field's value skips that field; a
154
- value the strict write refuses stays as authored under a warning. Both throw
153
+ is safe. A value the strict write refuses stays as authored under a warning. Both throw
155
154
  when the document declares a `$quill` this quill does not answer to, before any
156
155
  mutation.
157
156
 
@@ -175,9 +174,9 @@ const doc = Document.storageVersionOf(content)
175
174
  : Document.fromMarkdown(content);
176
175
  ```
177
176
 
178
- The `schema` value (`quillmark/document@0.112.0`) is the **model version**,
177
+ The `schema` value (`quillmark/document@0.116.0`) is the **model version**,
179
178
  not the running crate version. It is a hand-set constant, bumped only when
180
- the `Document` model itself changes, so every `0.112.x` patch release reads
179
+ the `Document` model itself changes, so every `0.116.x` patch release reads
181
180
  and writes that same value.
182
181
 
183
182
  - **Upgrading is safe.** A newer build reads documents an older build's
@@ -187,8 +186,10 @@ and writes that same value.
187
186
  canonical on-disk format keeps loading across crate upgrades: there is no
188
187
  need to pin old wasm to read old data. The exception is a row a host
189
188
  authored a content construct of its own into — a line `kind`, container,
190
- mark `type`, island `type` or `loss` outside the vocabulary. Those are
191
- refused from 0.113 on; see that release's migration guide.
189
+ mark `type` or island `type` outside the vocabulary. Those are refused from
190
+ 0.113 on; see that release's migration guide. An island `loss` is not one of
191
+ them: 0.115 drops the key, so a row spelling one opens whatever the class and
192
+ comes back without it.
192
193
  - **Downgrading is not.** `fromStored` rejects an *unknown* (i.e. newer)
193
194
  `schema` version rather than guessing at a format it predates. Don't feed
194
195
  documents written by a newer build back into an older one.
@@ -218,8 +219,9 @@ To render a form editor, read field definitions from `quill.schema` (walk
218
219
  values from the `Document` payload: there is no separate form-view projection.
219
220
  `quill.validate(doc)` scores it without invoking the backend.
220
221
 
221
- `quill.seedDocument()` returns a starter document with each field's `example:`
222
- committed; `quill.seedMain()` and `quill.seedCard(kind)` seed one card. All
222
+ `quill.seedDocument()` returns a starter document: one card per kind, each body
223
+ empty, every field absent (no `example:` is committed);
224
+ `quill.seedMain()` and `quill.seedCard(kind)` seed one card. All
223
225
  return the read `Card` shape of `doc.main` / `doc.cards`, which `doc.insertCard`
224
226
  accepts directly:
225
227
 
@@ -246,7 +248,7 @@ card is an object literal: one `{ type: "field", key, value }` per field in
246
248
  `{ card?, field? }`, absent `card` = main, absent `field` = body, and a bare
247
249
  string is shorthand for `{ field }`. So `doc.storeField("qty", 3)` targets the
248
250
  main card's `qty`, `doc.storeField({ card: 2, field: "qty" }, 3)` a composable
249
- card's. Reads are total over the field axis (`getStored` → `undefined`, `isFill` → `false` for
251
+ card's. Reads are total over the field axis (`getStored` → `undefined` for
250
252
  an absent field; only an out-of-range card throws); field writes throw on a body
251
253
  address. `getStored` is the verbatim transport read, distinct from the interpreted
252
254
  `quill.reader(doc).get`; `bodyMarkdown` is the body markdown read (a `CardAddr`; a field's
@@ -287,7 +289,7 @@ on `Document` itself (**store** = verbatim, **set** = typed):
287
289
  `_reviseField` ABI the writer delegates to, hidden from the `.d.ts`.)
288
290
 
289
291
  - **`store*`: the deliberate quill-free primitive.** `doc.storeField(addr, value)`
290
- / `doc.storeFields(cardAddr, {...})` (and `storeFill`) validate only the field
292
+ / `doc.storeFields(cardAddr, {...})` validate only the field
291
293
  name/depth/kind and store the value verbatim, no quill required. Reach for it
292
294
  on purpose when you *want* the opaque store: quill-agnostic storage/migration
293
295
  infra that has no bundle and must write regardless of a drifted schema;
@@ -352,43 +354,47 @@ tells you which pages to repaint (`dirty ∩ visible`). `update` is transactiona
352
354
  on throw, every read keeps serving the last-good compile. Don't open a session
353
355
  per export, and don't re-open per edit: `update` instead.
354
356
 
357
+ Both take the render date as an optional last argument (`YYYY-MM-DD`, default
358
+ the local date): what a `today` date field and a plate's `datetime.today()`
359
+ render as. A session keeps the date it opened with, so a preview left open past
360
+ midnight renders yesterday's until it is reopened.
361
+
355
362
  A document that compiles to zero pages still produces a valid session
356
- (`pageCount === 0`); `paint(ctx, 0)` and `pageSize(0)` then throw. Branch on
363
+ (`pageCount === 0`); `paint(ctx, 0, scale)` and `pageSize(0)` then throw. Branch on
357
364
  `pageCount === 0` to render a "no pages to preview" UI rather than relying on
358
365
  the throw.
359
366
 
360
367
  Their `warnings` differ in reach. `engine.render` returns one list for the
361
- whole pipeline: `doc.warnings` (parse, `conform::*`,
362
- `plate::unsupported_construct`) ahead of the compile's own. A session outlives
368
+ whole pipeline: `doc.warnings` (parse and `conform::*`), then every
369
+ `quill.validate(doc)` warning, then the compile's own. A session outlives
363
370
  the document it opened from, so `session.render` and `session.warnings` carry
364
- the compile half alone — read `doc.warnings` beside them.
371
+ the compile's alone — read `doc.warnings` and `quill.validate(doc)` beside them.
365
372
 
366
373
  ### Canvas Preview
367
374
 
368
- `session.paint(ctx, page, opts?)` rasterizes a page directly into a
375
+ `session.paint(ctx, page, scale)` rasterizes a page directly into a
369
376
  `CanvasRenderingContext2D` (main thread) or
370
377
  `OffscreenCanvasRenderingContext2D` (Worker), skipping PNG/SVG byte
371
378
  round-trips.
372
379
 
373
- The painter owns `canvas.width` / `canvas.height`: it sizes the backing
374
- store itself. Consumers own `canvas.style.*` (or the layout system that
375
- sets them) and read `layoutWidth` / `layoutHeight` from the returned
376
- `PaintResult`.
380
+ `scale` is backing-store pixels per point. The painter owns `canvas.width` /
381
+ `canvas.height`, reducing `scale` where it must so neither exceeds 16384 px;
382
+ consumers own `canvas.style.*`. A canvas styled to fill its page box
383
+ needs nothing back from the paint:
377
384
 
378
385
  ```ts
379
- const result = session.paint(canvas.getContext("2d"), 0, {
380
- layoutScale: 1, // layout px per pt (page geometry unit)
381
- densityScale: window.devicePixelRatio, // backing-store density
382
- });
383
-
384
- canvas.style.width = `${result.layoutWidth}px`;
385
- canvas.style.height = `${result.layoutHeight}px`;
386
+ canvas.style.width = "100%"; // the box sets the display size
387
+ const cssPxPerPt = canvas.clientWidth / session.pageSize(0).widthPt;
388
+ if (cssPxPerPt > 0) { // 0 while the canvas has no layout box
389
+ session.paint(canvas.getContext("2d")!, 0, cssPxPerPt * window.devicePixelRatio);
390
+ }
386
391
  ```
387
392
 
388
- - `layoutScale` sets the display-box size (`layoutWidth = widthPt * layoutScale`);
389
- fold `devicePixelRatio`, in-app zoom, and `visualViewport.scale` into
390
- `densityScale`. Their product is the rasterization scale, clamped at 16384 px
391
- per side (`result.clamped`, `result.effectiveDensityScale`).
393
+ - Fold `devicePixelRatio`, in-app zoom, and `visualViewport.scale` into `scale`.
394
+ - A canvas with no layout box (`display: none` on it or an ancestor, detached,
395
+ in a zero-width container) has `clientWidth` 0, and a 0 scale throws
396
+ `backend::invalid_raster_scale`: skip the paint, and paint when a
397
+ `ResizeObserver` reports a width.
392
398
  - `paint` writes the whole backing store with `putImageData`, which ignores the
393
399
  2D context transform, `globalAlpha`, and clip. Give each visible page its own
394
400
  `<canvas>`: no compositing, sub-rect, or transform reaches through `paint`.
@@ -400,33 +406,28 @@ canvas.style.height = `${result.layoutHeight}px`;
400
406
  cache them between committed `update`s only. After one, the count is
401
407
  `ChangeSet.pageCount`, and every page in `ChangeSet.dirtyPages` needs its
402
408
  `pageSize` re-read.
403
- - In a Worker, pass an `OffscreenCanvasRenderingContext2D`; the layout
404
- dimensions are informational there. Loading the WASM module inside the Worker
405
- is the host's responsibility.
409
+ - In a Worker, pass an `OffscreenCanvasRenderingContext2D`. An
410
+ `OffscreenCanvas` has no layout box to measure, so the main thread posts the
411
+ scale. Loading the WASM module inside the Worker is the host's
412
+ responsibility.
406
413
  - `paint` / `pageSize` throw on a page the compile does not have, a zero-page
407
414
  compile included, naming the index and the `pageCount` that excludes it. That
408
415
  throw is the whole contract: open the session and handle it.
409
416
 
410
417
  ### Schema model
411
418
 
412
- A field carries two independent axes, and no `required` one.
419
+ A field declares no `required` key: nothing is required.
413
420
 
414
- **Value** — what the cell holds. With a `default:`, `quill.blueprint` renders
415
- that value under a type-only `# <type>` annotation and the render path uses it
416
- when the document omits the field. Without one, an `example` takes the cell as
417
- a suggested value, and an absent field blank-fills.
421
+ **`default:`** — what an unanswered field renders. With one, `quill.blueprint`
422
+ renders that value under a type-only `# <type>` annotation and the render path
423
+ uses it when the document omits the field. Without one, the blueprint leaves the
424
+ cell empty and an absent field blank-fills.
418
425
 
419
- **Obligation** — whether a human must author the field, read off `default:`'s
420
- absence: a defaulted field asks nobody, a defaultless one asks. An obliged
421
- field carries the `!must_fill` marker in `quill.blueprint`, and
422
- `quill.validate(doc)` emits the non-fatal `validation::must_fill` warning while
423
- the document leaves it unauthored — from either of two triggers, named by the
424
- diagnostic's `trigger` arg: `marker` for a marker the document still carries,
425
- `unauthored` for a cell the schema obliges and the document never filled.
426
- Authoring the field's blank discharges the obligation; clearing the key does
427
- not.
426
+ **`example:`** — the schema's illustration of the field's shape.
427
+ `quill.blueprint` shows it on a `# e.g.` line above the field; it never takes a
428
+ cell and never renders.
428
429
 
429
- Neither axis gates render. Partial documents are accepted, and
430
+ An unanswered field draws no diagnostic. Partial documents are accepted, and
430
431
  `engine.render(quill, doc)` throws only for malformed input.
431
432
 
432
433
  ### Errors
@@ -514,7 +515,7 @@ in a browser that hasn't shipped it:
514
515
  const session = await engine.open(quill, doc);
515
516
  try {
516
517
  for (let p = 0; p < session.pageCount; p++) {
517
- session.paint(ctx, p);
518
+ session.paint(canvases[p].getContext("2d")!, p, scale);
518
519
  }
519
520
  } finally {
520
521
  session.free();