@quillmark/wasm 0.132.0 → 0.134.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,423 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.134.0 - 2026-11-02
4
+
5
+ Upgrade path: [0.133 → 0.134](docs/migrations/0.133-to-0.134.md). Every
6
+ content value is the content model, and offsets in content values and changes
7
+ cross WASM in UTF-16 code units, where they counted scalar values. Python's
8
+ `revise_body`, `revise_field` and `revise_document` return a dict receipt,
9
+ where they returned a list of `Diagnostic`, so a loop over the result runs
10
+ over its keys: read `receipt["warnings"]`.
11
+
12
+ ### Content v2
13
+
14
+ - feat(content)!: **the content model.** One value per body or `richtext`
15
+ field: flat text, blocks and containers with ids, marks, anchors apart from
16
+ marks, and islands whose cells and notes are slots of nested content.
17
+ Nothing derived is stored: an item's number is computed, and runs are told
18
+ apart by container id. The line form moves under `quillmark_content::lines`
19
+ and stays what the renderers and markdown codecs read; `migrate` moves
20
+ values between the two, minting ids deterministically.
21
+ - feat(content)!: **the line form's op lane is removed.** `lines::ops`
22
+ (`ChangeBundle`, `LineOp`, `MarkOp`, `IslandOp` and their decoders),
23
+ `Card::apply_body_change`, `Card::apply_field_change`, the `Normalized`
24
+ `apply_*` methods and `EditError::ContentApply` go: a `Change` through
25
+ `Card::apply` is the one edit. `quillmark::session` no longer re-exports
26
+ them. The parity corpus's op-wire cell reads the `Change` `diff` takes
27
+ from the empty value.
28
+ - feat(content): **`Change`, the one way a value is edited.** Splice,
29
+ `setBlock`, `container`, `mark`, `anchor`, `island` and `nested` steps,
30
+ with `invert`, `compose`, `map_pos` and a word-aligned `diff`, each a
31
+ property-tested law. A change yields a valid value or fails and changes
32
+ nothing.
33
+ - feat(content)!: **a block's record rides the `\n` that opens it.** A
34
+ split names the right half and the left keeps its id, so Enter at the end
35
+ of any block, the last included, leaves the text on its id; a join keeps
36
+ the left block's id and kind. `blocks[0]` is the record no splice reaches,
37
+ so `diff` and `replace` keep the stored first id, and `repair` gives the
38
+ first piece of a block it splits beside an island the block's record.
39
+ - feat(content): **`gestures::split_block` and `join_blocks`** build Enter
40
+ and Backspace as one splice each, inserting ahead of a block's opening
41
+ `\n` at its start so the text keeps its id.
42
+ - feat(content): **`gestures::inherited_marks(content, at)`** answers the
43
+ marks typed text carries: `strong`, `emph`, `underline` and `strike` grow
44
+ at their end, `code`, `link` and an `x-` mark at neither edge,
45
+ `font_family` at both.
46
+ - feat(bindings)!: **every WASM offset is UTF-16.** A region's `span`,
47
+ `positionAt`'s and `locate`'s `pos`, a typed revise's `Revised.delta` and
48
+ the deltas `reattach` takes count in UTF-16 code units, as content values
49
+ and changes do; core takes a host's units through `LiveSession::*_in`,
50
+ `Engine::render_in`, `Revised::delta_in` and `Document::reattach_in`.
51
+ - feat(content): **`repair` makes a merged state valid.** A missing
52
+ container leaves a path, a container under two paths takes its first
53
+ block's, a later run gets a derived container, an orphan slot or record
54
+ goes, text beside a table splits off, and a collapsed range anchor is
55
+ dropped. Total and deterministic.
56
+ - feat(content): **repair is how a merged state reads, and a write-back is
57
+ optional.** Peers holding one merged state compute one value without
58
+ writing. Any peer may write back a repair that only deletes or sets; one
59
+ that inserts (a block split beside an island, a heading's hard break, a
60
+ missing cell) is written back by a single writer, since two peers would
61
+ each insert their own. `Repair::inserts` says which, `inserts: true` on
62
+ the wire, and a missing cell reports `Repair::EmptyCell`.
63
+ - feat(core)!: **storage moves to `quillmark/document@0.134.0`**, each body
64
+ and richtext field the content model. An older row loads migrated, with the
65
+ same ids on every load. A value holds each anchor id once over its text
66
+ and every slot, so a stored value holding one twice loads keeping its
67
+ first holder.
68
+ - feat(core)!: **`Card::apply`, `revise` and `replace`** write a body or
69
+ richtext field and answer a `Receipt`: the applied change, the new hash,
70
+ and the dropped and moved anchors. Each takes the hash its writer read and
71
+ refuses a stale one with `edit::stale_base`. `Card::write` takes the
72
+ `Edit` each builds its change from, and `write_in` takes a host's units.
73
+ - feat(wasm)!: **`doc.read`, `apply`, `revise`, `replace`, `annotated` and
74
+ `hash(addr)`, and `invert` and `diff`**, offsets in UTF-16 code units.
75
+ Every content payload crossing the binding is the model. `overwrite`,
76
+ `reviseContent`, `applyChange` and `mapMarks` are removed: call `replace`
77
+ and `apply`. `compose`, `rebase` and `repair` stay unbound until the
78
+ editor or collaboration calls them.
79
+ - feat(wasm)!: **the editing gestures and `mapPos(change, pos, assoc?)`.**
80
+ `splitBlock`, `joinBlocks`, `deleteRange`, `indent`, `outdent`, `wrap` and
81
+ `inheritedMarks` take a `Content` and answer the `Change` to apply, offsets
82
+ in UTF-16. `mapPos` carries a position across a `Change`, where it took a
83
+ line delta, and answers a `MapResult`.
84
+ - feat(python)!: **the same surface in snake case**, offsets in code points.
85
+ - feat(core)!: **a content write names its card by `$id`.** An id no card
86
+ holds refuses with `edit::unknown_card_id`. `Document::card_index` and
87
+ `card_by_id` find a card by id. WASM `Addr.card` and Python `card=`
88
+ take a card's `$id` on every verb, and `apply`, `revise` and
89
+ `replace` take nothing else, so a card moved or inserted between a read and
90
+ its write, its body equal to the one read, no longer takes the write.
91
+ - feat(content)!: **the receipt names every anchor a change loses.** An
92
+ island a splice deletes, or a slot a props step no longer names, names the
93
+ anchors of its slots, each with the `island` and `slot` that held it.
94
+ `revise` and `replace` name every anchor they lose; `apply` every one but
95
+ those its own `anchor` steps removed. `Applied.dropped` and
96
+ `Receipt.dropped_anchors` are `change::Dropped`, an `AnchorContext` and a
97
+ `SlotRef`; WASM spells it `DroppedAnchor`.
98
+ - feat(content)!: **`diff` refuses an invalid target.** `diff` and `replace`
99
+ return the `Invariant` an invalid `after` breaks, where they returned zero
100
+ steps, and reach every valid one step-wise. WASM and Python `diff` answer
101
+ the `Change`.
102
+ - feat(content)!: **every offset-addressed step can hold to what its writer
103
+ read.** `mark` steps and an `anchor` placement take an optional `quote`,
104
+ the text their range covered, and an `island` insert an optional `block`,
105
+ the id of the block it lands in; a mismatch is `ApplyError::Stale`, as a
106
+ splice's `delete` is. `Applied.applied` and `invert` fill both in.
107
+ `MarkStep::Add`, `MarkStep::Remove`, `AnchorStep::Set` and
108
+ `IslandStep::Insert` carry the new fields.
109
+ - feat(content)!: **`map_pos` answers a `MapResult`.** A position always
110
+ lands, inside deleted text on the deletion's `assoc` edge and inside an
111
+ island a splice deleted on the island's slot, with `deleted`,
112
+ `deleted_before`, `deleted_after` and `deleted_across` saying what went
113
+ around it. A splice that deletes an island and names none in `removed` is
114
+ refused, where it mapped a position inside that island as kept. New
115
+ `mapping::StepMap` and `Mapping`, a list of step maps with mirror links, so
116
+ a position a step deleted and its rebased mirror put back maps home.
117
+ - feat(content)!: **reserved `x-` names on three vocabularies.** A block
118
+ kind, mark type or island type spelled `x-` and more decodes into
119
+ `BlockKind::Ext`, `MarkKind::Ext` or `IslandType::Ext`, its attrs or props
120
+ held as written, and re-encodes to the same bytes; any other unknown name
121
+ still refuses. Validation is structural only. An `x-` block reads as a
122
+ paragraph, an `x-` mark formats nothing, and an `x-` island renders and
123
+ exports to markdown as nothing, which the annotated read spells as its
124
+ token so a revise keeps it; a content write's revise or replace that
125
+ removes one refuses with `edit::drops_ext_island`, and `apply` removes it.
126
+ Steps set and remove them. `MarkStep::Remove`'s `tag` is a `String`,
127
+ `BlockKind::tag` and `MarkKind::tag` return `&str`, and `IslandType` is no
128
+ longer `Copy`.
129
+ - feat(content): **`rebase(base, change, over)` moves a change past another
130
+ made from the same value**, after ProseMirror's `rebaseSteps`. Each step
131
+ maps through the inverse of its change's earlier steps, then `over`, then
132
+ those steps rebased, and holds to its `delete`, `quote` or `block` on the
133
+ value it now reads; a step `over` deleted the text of, or whose id is gone,
134
+ is a `rebase::Conflict` naming it. A step by id that would overwrite what
135
+ `over` changed (a block's kind or path, a container, an island's props)
136
+ conflicts rather than undoing it, as does a splice that deletes an island
137
+ or block `over` changed, so a diff's `setBlock` names only the fields it
138
+ changes.
139
+ `Mapping::map_point` and `project` map
140
+ as `apply` moves a point anchor and inside one slot.
141
+ - feat(core)!: **a stale `base` rebases the write instead of refusing it.**
142
+ Each card keeps, in memory only, a log of its recent writes per value: one
143
+ snapshot of the value, and each write's normalized change and the hash it
144
+ left. A base rebases while the writes since it hold at most `LOG_STEPS`
145
+ (4096) steps, thousands of keystrokes, and the base the newest write read
146
+ whatever its size; past twice that, the oldest writes fold into the
147
+ snapshot, so the log holds one copy of the value however large.
148
+ `apply`, `revise` and `replace` with a base the log holds rebase over the
149
+ changes since, and a revise or replace diffs from the value the base
150
+ names. The receipt's new `rebased` says so. A conflict refuses with
151
+ `edit::rebase_conflict`, carrying `step` and `reason`; a base the log does
152
+ not reach is `edit::stale_base` as before. `Card::read_at` answers the
153
+ value a base names, and `BaseError::Conflict` carries `step` and
154
+ `reason`.
155
+ - fix(content): **`replace` moves an anchor between the prose and a cell**,
156
+ where it refused with `DuplicateId`: it carries only an anchor the new
157
+ value names nowhere. A props step naming a slot in `slots` that the props
158
+ do not name says so, apart from one naming a slot the island holds.
159
+ - feat(content)!: **a block or island keeps its id when a change moves
160
+ it.** Block and island ids are unique at the end of a change, as anchor
161
+ ids are, so a step may insert a copy before another deletes the original;
162
+ a step naming an id held twice between steps is `ApplyError::BadStep`.
163
+ `diff` keeps the id of a block or island moved either way, where it
164
+ re-minted one moved toward the front.
165
+ - perf(content): **`apply` runs a change in one pass.** Each step works on
166
+ the blocks it touches, checks only what it touched, and answers its
167
+ inverse as it applies; the whole value is validated once, at the end.
168
+ `invert` reads those inverses back, and a receipt lowers the value or
169
+ compares anchors only where something was dropped or rewritten. A
170
+ 4000-step change to 288k characters applies in 26 ms, where it took 96 s,
171
+ and inverts in 30 ms, where it took 496 s; a revise of it takes 0.1 s and
172
+ a rebase 0.2 s, where they took 294 s and 396 s. An inverse's mark steps
173
+ restore each run of marks that differs, not one range over all of them.
174
+ `tests/speed.rs` is the gate, run in CI in a release build.
175
+ - perf(content): **converting a change or value to UTF-16 finds each
176
+ offset by binary search**, where it walked the text from its start: the
177
+ 4000-step change crosses each way in about 20 ms, where it took 12 s out
178
+ and 19 s in.
179
+ - perf(core): **`content_hash` digests the content value as written**,
180
+ whose keys are already in order, rather than sorting a copy; the bytes are
181
+ the same.
182
+
183
+ ### Card identity
184
+
185
+ - feat(core)!: **every composable card carries a `$id`.** It emits after
186
+ `$kind`, is minted where none is written (`c` and five base32 letters of a
187
+ fixed SHA-256 sequence, so equal markdown parses to equal ids), and a parse
188
+ re-mints a malformed or repeated one with `parse::card_id_reminted`. A
189
+ null `$id` (`$id:`, `~`) is no `$id`.
190
+ `insert_card` refuses a bad or held id (`edit::invalid_card_id`,
191
+ `edit::duplicate_card_id`). An older row mints its ids on load. `$id`
192
+ never reaches the plate JSON.
193
+ - feat(core)!: **a whole-document revise pairs cards by `$id` first.** An
194
+ incoming card spelling a stored card's id revises it, the most similar of
195
+ several spelling one keeps it, and the rest align by text as before. A
196
+ revised card keeps its stored id; an inserted one keeps a fresh id or mints
197
+ past every stored one. A list field's items align by text, so an item
198
+ inserted ahead of an anchored one leaves the anchor on its own item.
199
+ - feat(core)!: **a comment selector names its card.** `AnchorSelector` and
200
+ `DocumentAnchor` gain `card_id`, and `reattach` resolves a selector's card by
201
+ it wherever the card moved. WASM spells it `cardId` on `AnchorContext`,
202
+ `DocumentAnchor` and the `Card` / `CardInput` `id`; Python `card_id` and the
203
+ card dict's `id`.
204
+
205
+ ### Two-way annotated read
206
+
207
+ - feat(content)!: **a revise reads the annotated read back.** A
208
+ `qm-anchor` tag the writer kept pins its anchor's start where it stands,
209
+ one it dropped leaves the anchor to the diff, and one naming nothing stored
210
+ reports `qm-anchor[ref]`. The read marks a tag it moved off its anchor's
211
+ start `at="span"` or `at="line"`, which pins nothing.
212
+ - feat(content)!: **an island markdown cannot spell reads as a
213
+ `qm-island` token**, `<qm-island ref="ID"></qm-island>`, which a revise
214
+ keeps as stored: a table's unnamed keys, an equation's unwritable TeX, an
215
+ image's extra props. A token naming no stored island reports
216
+ `qm-island[ref]`; a cold import drops every token, reported as
217
+ `qm-island`. A `Base::Read` merge compares the sides as annotated reads,
218
+ so a token survives it and a moved tag pins through it.
219
+
220
+ ### Anchors on every text surface
221
+
222
+ - feat(content)!: **a table cell's and a footnote note's anchors join the
223
+ comment contract.** `Content::anchor_sites` reads every anchor with its
224
+ `Surface`, prose or a cell, and `anchor_contexts`, the annotated read's
225
+ `anchors` and every `dropped_anchors` receipt list cell anchors, a cell's
226
+ text as its `line`. `RemoveAnchor` removes an id from every cell, an anchor
227
+ `Add` refuses an id a cell holds, and an island op refuses a cell anchor
228
+ whose id the value already holds (`ApplyError::AnchorIdCollision`). A revise
229
+ keeps a cell's anchor where its island continues and the cell's text is
230
+ unchanged. `locate_site` and `reattach` search the cells with the prose.
231
+
232
+ ### Scoped reads
233
+
234
+ - feat(core): **the annotated read of one address.** `Card::body_annotated`
235
+ and `field_annotated` (WASM `doc.annotatedAt(addr)`) read one body or
236
+ richtext field as the revise at that address reads it back, and as its
237
+ `baseMarkdown`.
238
+
239
+ ### Guarded revise
240
+
241
+ - feat(core)!: **a revise states what its writer read.** `Base::Hash` is the
242
+ hash of the value read (`Card::body_hash`, `Card::field_hash`,
243
+ `Document::hash`, `content_hash`, and the typed writer's `hash` /
244
+ `field_hash` through the schema: SHA-256 over `quillmark/hash@1` and the
245
+ key-sorted canonical JSON), and a value that now hashes otherwise refuses
246
+ the write with `edit::stale_base`. `Base::Read` is the markdown read, annotated or not:
247
+ when the value has changed since, the writer's edit and the change merge by
248
+ line (`quillmark_content::delta::merge_lines`), so a person's edit made
249
+ between an LLM's read and its write lands beside the LLM's, and runs of
250
+ lines both changed refuse with `edit::rebase_conflict`. Both carry `current`,
251
+ the hash now. The verbs are `revise_body_against`, `revise_field_against`,
252
+ the writers' `revise_document_against`, and the free
253
+ `revise_import_against`. `EditError` gains `Base(BaseError)` and
254
+ `BoundParseError` gains `Base`.
255
+ - feat(wasm): **`BaseOptions` on every revise.** The writer's `reviseBody` /
256
+ `reviseField` / `reviseDocument` take an optional
257
+ `{ baseHash?, baseMarkdown? }`, read through the writer's `hash()` /
258
+ `hashAt(addr)` or the quill-free `doc.hashAt(addr)` / `doc.hash()`.
259
+ - feat(python): **`base_hash` and `base_markdown` on the writer's revise
260
+ verbs**, read through the writer's `hash()` / `hash_at(field=None,
261
+ card=None)` and the quill-free `doc.hash_at` / `doc.hash()`.
262
+
263
+ ### Markdown export
264
+
265
+ - feat(content): **an export can name what it does not spell.**
266
+ `export::to_markdown_checked` returns `Exported { markdown, losses }`, the
267
+ markdown read back and compared with the content: each `ExportLoss` is a
268
+ mark written over other text or dropped, an island that reads back
269
+ otherwise, or `Text` where the text or its blocks do. Core mints one
270
+ `export::lost_construct` warning per loss (`lost_construct`), its
271
+ `construct` the mark's wire type, the island's type or `text`, a mark's
272
+ `start` and `end` beside it.
273
+ - fix(content): **a `*`, `_` or `~~` mark over edge whitespace keeps its
274
+ text.** `bar ` in bold wrote no bold at all, its delimiter never closing
275
+ against a space; it now writes `**bar** `, the mark over `bar`.
276
+ - perf(content): **`to_markdown` reads each line's marks alone**, so export is
277
+ linear in the marks where it scanned every mark per line.
278
+ - feat(content)!: **a markdown import reports the bidi controls it drops**,
279
+ under `parse::dropped_construct` with `construct: "bidi"`. `Dropped` gains
280
+ `Bidi`.
281
+
282
+ ### Comments
283
+
284
+ - feat(content): **anchor contexts and exact re-attachment.** The `anchor`
285
+ module reads each prose anchor's quote, prefix and suffix off the stored
286
+ text as it is, an inline island's `U+FFFC` slot included
287
+ (`anchor_contexts`), finds the one range a context names (`locate`), and
288
+ re-adds a dropped anchor through the `Add` arm (`Content::reattach`), a range
289
+ over exactly its quote and a point at the one place its context meets. The
290
+ rule never guesses: a quote of `MIN_ANCHOR_QUOTE` (four) scalar values or
291
+ more, or island slots alone, its context agreeing on `ANCHOR_AGREE_USV`
292
+ (eight) each side or as far as it reaches where the field's edge cut it, a match overlapping inserted text where a
293
+ delta is given, and exactly one such place. `Delta::inserted_spans` is
294
+ public. (#2349)
295
+ - feat(core): **`Document::reattach` and `Revised.dropped_anchors`.**
296
+ `Document::anchor_contexts` / `anchor_context(id)` read each anchor's
297
+ `AnchorSelector { id, path, quote, prefix, suffix }`, and
298
+ `Document::reattach(selectors, deltas)` places dropped anchors at their
299
+ paths, path-strict, an id live anywhere in the document, a cell's included,
300
+ answered without a write. `Revised` gains `dropped_anchors`, each
301
+ `AnchorRead { id, line }`, at every rung that revises one body or field, and
302
+ `revise_import` takes the `&Normalized` it names them from. The facade
303
+ re-exports `AnchorSelector`, `Reattached`, `AnchorRead` and `DocPath`.
304
+ (#2349)
305
+ - feat(wasm): **`anchorContexts`, `anchorContext`, `reattach` and
306
+ `droppedAnchors`.** The selector crosses flat, its `path` a `DocPath`
307
+ string; `reattach(selectors, deltas?)` takes a `Record<path, Delta>` and
308
+ returns `{ reattached, orphaned }`. The `Revised` receipt of
309
+ `writer.reviseBody` and `writer.reviseField` gains
310
+ `droppedAnchors: { id, line }[]`. (#2349)
311
+ - feat(python)!: **the revise verbs return the whole receipt.**
312
+ `writer.revise_body` / `revise_field` return `{ delta, warnings,
313
+ dropped_anchors }` and `revise_document` `{ dropped_anchors, warnings, from }`,
314
+ its warnings the same `Diagnostic` objects, where each returned a list of
315
+ them. `doc.anchor_contexts()`, `doc.anchor_context(id)` and
316
+ `doc.reattach(selectors, deltas=None)` expose the handle. (#2349)
317
+ - docs(canon): **`COMMENTS.md` states the comment-anchor contract**: the
318
+ thread outside the document, the id it shares with its anchor, the matching
319
+ rule, drift, table cells, and the hosting rule. (#2349)
320
+
321
+ ### Removed
322
+
323
+ - refactor(wasm)!: **WASM API no consumer uses.** `Document.revise` (the
324
+ 0.133 markdown revise; `revise` is now the content-lane write),
325
+ `rebase`, `loadStored`, `storeFields`, `Quill.seedMain`,
326
+ `Document.markdownRules`, `assignInstances`, `CardReader` and
327
+ `reader.card(i)`, `CardWriter.index` / `kind`, the writer's and reader's
328
+ `document` getters and `LiveSession.backendId` are removed. The migration
329
+ guide's § Removed names each replacement.
330
+ - refactor(core,pdf)!: **public items nothing outside their crate uses.**
331
+ `BUNDLE_MEDIA_TYPE` is removed; `QuillConfig::is_snake_case_identifier`,
332
+ `quillmark_pdf::CHECK_GLYPH` and `FieldSpec::assert_finite_rect` are
333
+ crate-private; `SessionHandle::update` has no default, and the
334
+ `backend::update_unsupported` code no built-in backend raised is gone.
335
+ - refactor(content)!: **`serial::line_kind_to_value` is removed**, and
336
+ `serial::line_kind_from_value`, `container_from_value` and
337
+ `mark_from_value` are crate-private: decode a whole value through
338
+ `lines::serial::from_canonical_value`.
339
+
340
+ ### Dependencies
341
+
342
+ - build(deps): **`serde-saphyr` 2.0**, with the YAML error sanitizer
343
+ stripping its new API names from messages; **Typst's `hayro` 0.7** for the
344
+ PDF form backend's raster; `pyo3` 0.30.1 and `lopdf` 0.46.
345
+ - ci(release): PyPI publishes only where `PUBLISH_PYPI` is set.
346
+
347
+ ## v0.133.0 - 2026-10-29
348
+
349
+ Upgrade path: [0.132 → 0.133](docs/migrations/0.132-to-0.133.md). The
350
+ blueprint is retired: a model writes the JSON authoring document, so a prompt
351
+ built from `quill.blueprint` moves to `quill.authoringSchema`. A root
352
+ `example.md` is an ordinary file: a quill's example is what its `Quill.yaml`
353
+ declares, so move each value onto its field before upgrading.
354
+
355
+ ### LLM authoring
356
+
357
+ - feat(core,cli,wasm,python)!: **LLMs author documents as JSON; the blueprint
358
+ is retired.** The authoring document is the values form lifted to the whole
359
+ document, `{fields, body, cards: [{kind, fields, body}]}`, every rich-text
360
+ field and body a Markdown string and `null` absent.
361
+ `QuillConfig::authoring_schema()` is its JSON Schema, built by
362
+ `build_transform_schema` under the new `SchemaCodec::Text`: every cell admits
363
+ `null`, every declared object refuses an undeclared key, a variant-bearing enum is a `oneOf` per world, and each slot carries
364
+ its `title`, `description`, `default` and `examples`.
365
+ `Quill::to_authoring` exports a document, a field stored as `null` absent, and `Quill::from_authoring` imports
366
+ and conforms one, refusing a malformed shape with `authoring::invalid_shape`,
367
+ its RFC 6901 pointer in `args.pointer`. Export never fails: an undecodable
368
+ or string-held content leaf passes as stored, so every export validates
369
+ against the schema and imports back unchanged.
370
+ WASM `authoringSchema` / `toAuthoring` / `fromAuthoring`, Python
371
+ `authoring_schema` / `to_authoring` / `from_authoring`, and
372
+ `quillmark schema --authoring` expose them. `QuillConfig::blueprint()`,
373
+ `quill.blueprint`, `quillmark blueprint` and `print_blueprint` are deleted;
374
+ `quillmark validate` renders the empty and seeded documents.
375
+ `build_transform_schema` takes a `SchemaCodec`. The blueprint's load rules go
376
+ with it: a multi-line `description:` and an enum member holding `>`, `;` or
377
+ `|` load, and `quill::description_multiline` and
378
+ `quill::format_literal_reserved_char` are deleted. (#2331)
379
+
380
+ ### Examples
381
+
382
+ - feat(core)!: **`example:` and a body's `description` and `example` join the
383
+ quill schema.** A slot at any depth declares at most one `example:`, a value
384
+ of its type checked at load as `default:` is, under
385
+ `quill::example_{type_mismatch,not_in_enum,format_violation,held_stored,on_namespace,over_max,over_length}`
386
+ and `quill::richtext_example_import`. It answers no cell. A body's `description` is text for the writer and its
387
+ `example` markdown that imports without a drop; either beside
388
+ `enabled: false` is `quill::invalid_body`. `schema()` carries them,
389
+ `FieldSchema` and `BodyCardSchema` gain the fields, and WASM types them as
390
+ `QuillFieldSchema.example` and `QuillCardBody.{description,example}`. (#2329)
391
+ - feat(core,cli,wasm,python)!: **the example document is derived.**
392
+ `Quill::example_document()` returns `Option<Document>`: the seeded document
393
+ with every declared `example:` committed and each kind's `body.example` as
394
+ its body, `None` where the quill declares none. A root `example.md` is an
395
+ ordinary file: `EXAMPLE_FILE` and `quill::example_reference` are deleted,
396
+ WASM `exampleDocument()` and Python `example_document()` no longer throw, and
397
+ `quillmark validate` validates and renders the derived document, warning
398
+ `cli::example_file_ignored` while a root `example.md` remains. The
399
+ fixture quills declare the values their `example.md` held. (#2329)
400
+
401
+ ### Content
402
+
403
+ - feat(content,core,wasm): **`reviseContent` revises a body or field to a
404
+ content and keeps its prose anchors.** WASM `doc.reviseContent(addr, rt)`,
405
+ over core's `Card::revise_body_content` / `revise_field_content` and the
406
+ content crate's `delta::carry_anchors`, diffs a content built elsewhere
407
+ against the stored value, carries each prose anchor whose id `rt` does not
408
+ hold, and returns the `Revised` receipt with its text `delta`. `rt`'s own
409
+ anchors and island ids land as they are.
410
+ - feat(content)!: **normalizing content moves no text.** A table's slot
411
+ sharing its line with prose is canonical: `Content::normalize` leaves the
412
+ line as given, where it inserted a `\n` on each side, and `to_markdown`
413
+ breaks it instead. `overwrite`, `reviseContent` and
414
+ `serial::from_authored_value` take the shape where they threw `block island
415
+ in a line's prose`, an `IslandOp::Set` on such a table lands, and a stored
416
+ row holding it loads as stored under the same tag. Typst sets the table as a
417
+ block where its slot sits, closing a heading around it, and a hard break
418
+ after a line the slot ends clears. WASM gains `normalize(content)`, the form
419
+ `overwrite` stores. (#2330)
420
+
3
421
  ## v0.132.0 - 2026-10-27
4
422
 
5
423
  Upgrade path: [0.131 → 0.132](docs/migrations/0.131-to-0.132.md). A row
package/README.md CHANGED
@@ -182,38 +182,9 @@ const doc = Document.storageVersionOf(content)
182
182
  : Document.fromMarkdown(content);
183
183
  ```
184
184
 
185
- The `schema` value (`quillmark/document@0.131.0` for a document holding a font
186
- mark, `@0.129.0` for any other) is the **model version**, not the running crate
187
- version. It is a hand-set constant, bumped only when the `Document` model
188
- itself changes, so every `0.131.x` patch release reads and writes those same
189
- values.
190
-
191
- - **Upgrading is safe.** A newer build reads documents an older build's
192
- *writer* produced. Each schema version's wire format is frozen and never
193
- changes; when the model does change, the new build ships a migration that
194
- converts old payloads on `fromStored`. A document you commit as your
195
- canonical on-disk format keeps loading across crate upgrades until its tag
196
- is retired: 0.130 reads `@0.124.0` and `@0.129.0` only, so resave an older
197
- row with a 0.124 to 0.129 build before taking 0.130, as that release's
198
- migration guide shows. The other exception is a row a host authored a
199
- content construct of its own into — a line `kind`, container, mark `type`
200
- or island `type` outside the vocabulary. Those are refused from 0.113 on;
201
- see that release's migration guide. An island `loss` is not one of them:
202
- 0.115 drops the key, so a row spelling one opens whatever the class and
203
- comes back without it. From 0.126 on, a row whose `$quill` is a lone major,
204
- `memo@1`, or whose body nests past 12 levels is refused too; rewrite it
205
- before upgrading, as that release's migration guide shows.
206
- - **Downgrading is not.** `fromStored` rejects a `schema` version it does not
207
- read, newer than the build or retired, rather than guessing at its format.
208
- Don't feed documents written by a newer build back into an older one.
209
-
210
- `Document.currentStorageVersion()` is the newest tag this build writes. A
211
- save writes the oldest tag that holds the row, so a fresh row can carry an
212
- older tag still read: a row holding no font mark, `table` element, merged
213
- table cell or table `head` 0.130 cannot draw saves as
214
- `quillmark/document@0.129.0`. A tag other than `currentStorageVersion()` is
215
- therefore not a mismatch. Use `storageVersionOf` after `fromStored` throws,
216
- to tell "a version this build does not read" from "corrupt":
185
+ The tag model, what upgrading reads, and the one-time rewrites a release asks of stored rows are in [Persistence](https://quillmark.dev/integration/persistence/).
186
+
187
+ `Document.currentStorageVersion()` is the newest tag this build writes, and a save writes the oldest tag that holds the row, so a tag other than it is not a mismatch. Use `storageVersionOf` after `fromStored` throws, to tell "a version this build does not read" from "corrupt":
217
188
 
218
189
  ```ts
219
190
  try {
@@ -224,13 +195,7 @@ try {
224
195
  }
225
196
  ```
226
197
 
227
- `storageVersionOf` does not validate the payload: it only reads the
228
- `schema` field, returning `undefined` for non-JSON, non-objects, or
229
- payloads that don't carry one.
230
-
231
- In short: persist the `toStored` string, resave rows under a retired tag
232
- before upgrading past it, never downgrade. The full design (including how
233
- migrations are added) is in `prose/canon/DOCUMENT_STORAGE.md`.
198
+ `storageVersionOf` does not validate the payload: it only reads the `schema` field, returning `undefined` for non-JSON, non-objects, or payloads that don't carry one.
234
199
 
235
200
  ### Cards, seeds, and addresses
236
201
 
@@ -240,12 +205,21 @@ values from the `Document` payload: there is no separate form-view projection.
240
205
  `quill.validate(doc)` scores it without invoking the backend.
241
206
 
242
207
  `quill.exampleDocument()` returns the quill's example, a filled-in page whose
243
- values are made up, pinned to the quill's `name@MAJOR.MINOR`; `undefined`
244
- when the quill has no `example.md` at its root.
208
+ values are made up: the seeded document with each `example` the schema
209
+ declares committed, a field's as its value and a body's as the body;
210
+ `undefined` when the quill declares none.
211
+
212
+ An LLM writes a document as JSON: `quill.authoringSchema` is its JSON Schema,
213
+ `quill.fromAuthoring(reply)` builds and conforms the `Document` (the JSON twin of
214
+ `quill.parse`), and `quill.toAuthoring(doc)` exports any document back, every
215
+ rich-text field and body a Markdown string. A shape error throws
216
+ `authoring::invalid_shape` at its JSON path; a value of the wrong type lands
217
+ with a `conform::*` warning and `quill.validate(doc)` reports it at its
218
+ document path.
245
219
 
246
220
  `quill.seedDocument()` returns a starter document: one card per kind, each
247
221
  carrying the kind's `seed` from `Quill.yaml` and no other field;
248
- `quill.seedMain()` and `quill.seedCard(kind)` seed one card. All
222
+ `quill.seedCard(kind)` seeds one composable card. Both
249
223
  return the read `Card` shape of `doc.main` / `doc.cards`, which `doc.insertCard`
250
224
  accepts directly:
251
225
 
@@ -282,16 +256,10 @@ the content object, a `text` field as its literal string), and as authored
282
256
  through the transport door. For the `Content` either way read
283
257
  `quill.reader(doc).getContent(addr)`, which decodes through the codec the
284
258
  field's declared type names. Card-scoped verbs take a
285
- `CardAddr` (`{ card? }`) first: `doc.getExt({ card: 2 })`, and the batch below.
286
-
287
- Batch mutation: `doc.storeFields({}, {...})` / `doc.storeFields({ card: index }, {...})`
288
- apply a whole object atomically: on any invalid field nothing is applied and
289
- the thrown error carries one diagnostic per offending field (`path` = field
290
- name). The address is first (never shape-overloaded, since `card` is a legal
291
- field name), and parses strictly: a stray key throws rather than silently
292
- reading as `{}`. The main card is `{}`, or **`MAIN_CARD_ADDR`** (from
293
- `@quillmark/wasm`), a frozen alias that spells the intent:
294
- `doc.storeFields(MAIN_CARD_ADDR, {...})`.
259
+ `CardAddr` (`{ card? }`) first: `doc.getExt({ card: 2 })`. An address parses
260
+ strictly: a stray key throws rather than silently reading as `{}`. The main
261
+ card is `{}`, or **`MAIN_CARD_ADDR`** (from `@quillmark/wasm`), a frozen alias
262
+ that spells the intent: `doc.getExt(MAIN_CARD_ADDR)`.
295
263
 
296
264
  ### Typed writes: `commit*` is the default, `store*` is the quill-free primitive
297
265
 
@@ -308,13 +276,12 @@ on `Document` itself (**store** = verbatim, **set** = typed):
308
276
  on the typed path an undeclared name is a typo, not a fallback. The batch form
309
277
  (`setAll`) is all-or-nothing: an undeclared name aborts the whole write and its
310
278
  per-field diagnostics name every offending field, so a whole-form submit
311
- surfaces every typo `storeFields` would silently absorb. (The raw wasm class
312
- carries the quill-taking `_commitField` / `_commitFields` / `_addCard` /
279
+ surfaces every typo the opaque store would silently absorb. (The raw wasm
280
+ class carries the `_commitField` / `_commitFields` / `_addCard` / `_revise` /
313
281
  `_reviseField` ABI the writer delegates to, hidden from the `.d.ts`.)
314
282
 
315
283
  - **`store*`: the deliberate quill-free primitive.** `doc.storeField(addr, value)`
316
- / `doc.storeFields(cardAddr, {...})` validate only the field
317
- name/depth/kind and store the value verbatim, no quill required. Reach for it
284
+ validates only the field name/depth/kind and stores the value verbatim, no quill required. Reach for it
318
285
  on purpose when you *want* the opaque store: quill-agnostic storage/migration
319
286
  infra that has no bundle and must write regardless of a drifted schema;
320
287
  store-now-validate-later editors holding in-progress input that `commit`
@@ -346,7 +313,7 @@ ed.card(2).reviseBody("**note**"); // a composable card's body;
346
313
  is lazy: it never throws; an out-of-range index throws `IndexOutOfRange` at the
347
314
  write.
348
315
 
349
- #### `DocumentReader` / `CardReader`: the read twin
316
+ #### `DocumentReader`: the read twin
350
317
 
351
318
  `quill.reader(doc)` carries the writer's ephemerality and schema authority:
352
319
 
@@ -355,7 +322,7 @@ const v = quill.reader(doc);
355
322
  v.get("subject"); // the values form: every content leaf as its codec's text, else as stored
356
323
  v.getContent("subject"); // the same read as a `Content`, whichever lane stored it
357
324
  v.bodyMarkdown(); // the main body markdown (quill-free)
358
- v.card(0).bodyMarkdown(); // a card's body markdown; its fields read through get
325
+ v.get({ card: 0 }); // a card's body markdown; its fields read through get
359
326
  v.resolve(); // the render view: blank-filled, coerced, each field tagged with its rung
360
327
  ```
361
328
 
@@ -450,15 +417,7 @@ if (cssPxPerPt > 0) { // 0 while the canvas has no layout
450
417
 
451
418
  ### Schema model
452
419
 
453
- A field declares no `required` key: nothing is required.
454
-
455
- **`default:`** — what an unanswered field renders. With one, `quill.blueprint`
456
- renders that value under a type-only `# <type>` annotation and the render path
457
- uses it when the document omits the field. Without one, the blueprint leaves the
458
- cell empty and an absent field blank-fills.
459
-
460
- An unanswered field draws no diagnostic. Partial documents are accepted, and
461
- `engine.render(quill, doc)` throws only for malformed input.
420
+ A field declares no `required` key and an unanswered field draws no diagnostic; [`default`](https://quillmark.dev/quills/quill-yaml-reference/#default) is what an unanswered field renders.
462
421
 
463
422
  ### Errors
464
423
 
@@ -492,19 +451,11 @@ throws a real `Error` and attaches the property, so there is no constructor to
492
451
  `instanceof` against. Narrow with `isQuillmarkError`, which also works on errors
493
452
  from any build or WASM instance in the page.
494
453
 
495
- `diagnostics` is always non-empty: length 1 for most failures, length N for
496
- backend compilation errors, and `message` is derived from it. `diagnostics[0]`
497
- is an error. A failed Typst `engine.render` or `engine.open` also carries the
498
- quill's load warnings after its errors, so read each entry's `severity`. The
499
- same shape applies to every throw site:
454
+ `diagnostics` is always non-empty and its first entry is an error; [Error Handling](https://quillmark.dev/integration/error-handling/) has the shape, the code namespaces and the warnings that follow. The same shape applies to every throw site:
500
455
 
501
456
  - `Document.fromMarkdown`: parse errors (missing root `$quill` metadata, YAML
502
457
  errors, `parse::input_too_large` for inputs > 10 MiB).
503
- - `Document` mutators (`storeField`, `insertCard`, the writer's
504
- `set`, etc.): mutator failures carry a namespaced `edit::*` `code` on
505
- `diagnostics[0]` (`edit::invalid_field_name`, `edit::unknown_field`,
506
- `edit::index_out_of_range`, `edit::field_coercion_failed`, …). Route on
507
- `diagnostics[0].code`, never on message text.
458
+ - `Document` mutators (`storeField`, `insertCard`, the writer's `set`, etc.): a namespaced `edit::*` `code` on `diagnostics[0]`.
508
459
  - `engine.render` / `session.render`: backend compilation failures and
509
460
  validation errors.
510
461
  - An object argument carrying a key its verb does not read: the render
@@ -512,8 +463,8 @@ same shape applies to every throw site:
512
463
  key counts, a non-enumerable one and one holding `undefined` included. The
513
464
  argument must be a plain object, its prototype `null`, `Object.prototype`
514
465
  of any realm, or a null-prototype object whose keys count too, so a `Map`, a
515
- class instance or `Object.create({ … })` throws. A payload item, the fields
516
- object `storeFields` takes, and a `$ext` or `$seed` value read their own
466
+ class instance or `Object.create({ … })` throws. A payload item and a
467
+ `$ext` or `$seed` value read their own
517
468
  enumerable keys alone: a non-enumerable or inherited key there is neither
518
469
  read nor refused.
519
470
  The diagnostic names the key or what was passed, and carries no `code`: it