@quillmark/wasm 0.92.1 → 0.95.1

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,15 +1,312 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.95.1 - 2026-07-19
4
+
5
+
6
+
7
+ ## v0.95.0 - 2026-07-19
8
+
9
+ - release: re-cut to finish binding dists after the first publish run cancelled mid-flight
10
+ - **breaking** typst: a present `date` / `datetime` field lowers to a click-to-edit value-object — `(value: datetime(..), display: (..args) => text(value.display(..args)))` — instead of a bare Typst `datetime`, so the rendered glyphs are born at a generated `text(..)` node carrying a region keyed on the field's schema path: a date placed by a vendored package, or a card's date riding a shared loop variable, is click-to-edit. A blank date stays `none`, so `!= none` guards hold. Plates migrate two shapes: `(data.f.display)("…")` (paren form — the stored `display` is a closure on a dict, not a method) and `data.f.value` for anything native (comparison, `.year()`-family components, datetime-consuming packages). The tonguetoquill flagship quills' `display-date` dispatches on `type(date)`, since their `datetime.today()` blank-date fallback stays a native datetime (#990)
11
+ - **breaking** core,typst,pdfform: split the `datetime` field type into strict `date` and `datetime` (#717/#799 resolved) — `date` accepts a bare `YYYY-MM-DD` and rejects any time component; `datetime` accepts offset-less wall-clock `YYYY-MM-DDThh:mm[:ss]` (seconds zero-filled) and rejects timezone offsets, the space separator, fractional seconds, and bare dates. Offsets are rejected, never dropped (the engine does no zone math); storage stays verbatim; no truncation in either direction. A `date` lowers to the three-component Typst `datetime(year:, month:, day:)` (unchanged emission) and a `datetime` to the six-component constructor, carrying the wall-clock time. The transform schema marks `date` as `format: "date"` (keeping `format: "date-time"` for `datetime`), and the blueprint reads `date<YYYY-MM-DD>` / `datetime<YYYY-MM-DDThh:mm[:ss]>`. Most deployed `datetime` fields hold a bare date and migrate to `type: date` with byte-identical data; the fixtures (usaf_memo, cmu_letter) do so. No deprecation alias — `datetime` rejecting a bare date is the decided end-state (#991)
12
+ - **breaking** python: the binding commits to the typed lanes — field I/O flows through `quill.writer(doc)` / `quill.view(doc)` exclusively and `Document` is quill-free data and structure. Removed (WASM-only by scope, not lag — their audience is not a Python audience): the opaque field store (`store_field` / `store_fields` / `store_fill` and card twins), the content lane (`install` / `revise` / `apply_change`), the quill-free field reads (`get` / `get_card_field`), and the module codec fns (`import_markdown` / `export_markdown` / `rebase` / `map_pos`). The composable `$ext` / field-remove card twins fold onto a trailing `card=None` selector and `push_card` folds into `insert_card(card, at=None)`. Mirrored additions: `writer.revise_field` (the `Delta` receipt is not surfaced), `writer.add_card(kind, fields=None, body=None, at=None)`, `writer.card(i).kind`, and the `Writer` / `CardWriter` / `View` / `CardView` handle classes exported from the package (#970)
13
+ - **breaking** core,wasm,python: complete the `RichText` → `Content` residual sweep (#976 folds into #982) — retire the last informal-`corpus` / model-`RichText` *identifiers* the mechanical rename missed: `CorpusHit` → `ContentHit`, `EditError::CorpusApply` → `EditError::ContentApply`, `RichtextDecodeError::NotCorpus` → `NotContent` (the codec-specific `RichtextDecodeError` type itself is kept), storage DTO `CanonicalRichText` → `CanonicalContent` (serde is unchanged — no wire migration), and the model-generic Typst emitter `emit_richtext` / `emit_richtext_inline` → `emit_content` / `emit_content_inline` (they lower any `Content`, richtext *or* plaintext). Also fixes the `ParseError` display strings `richtext json …` → `content json …`, the doubled-word find-replace debris (`content content model` → `content model`), and stale prose/comments across canon and rustdoc. Schema/codec names are untouched — `richtext` / `plaintext` tokens, `FieldType::{RichText,PlainText}`, `field_richtext` / `FieldRichtext*` / `apply_field_richtext_change`, `richtext(inline)` — they name codecs, not the model. "corpus" is purged from the tree entirely, including the ordinary-English test-set names that meant a *collection* of fixtures (`fixture_corpus` → `fixtures`, `synthetic_corpus` → `synthetic_inputs`) (#982)
14
+ - **breaking** core,wasm,python: a schema-bound read view — `Quill::view(&doc)` / `quill.view(doc)`, the read twin of `quill.writer(doc)`. `view.get(addr)` interprets each field by its declared type (a `richtext` field → markdown, a `plaintext` field → its literal text via the plaintext codec, every other type → its canonical value verbatim), returns absent as `undefined` / `None`, and — the authority the quill-free `getMarkdown` lacks — throws `UnknownField` for a name the schema does not declare and `FieldRichtextDecode` for a content field holding an undecodable value. Core `TypedReader::get` returns a `ReadValue` (`Markdown`/`Plaintext`/`Value`); `view.card(i)` is the card cursor; core adds `Card::field_plaintext` (the `to_plaintext` twin of `field_markdown`). **`getMarkdown`'s field half retires**: `getMarkdown` / `get_markdown` / `get_card_markdown` are now body-only (WASM `getMarkdown` takes a `CardAddr`, a present `field` throws; Python drops the `name` parameter) — a field's markdown is read through `view.get`. The quill-free body projection stays on `Document` (#978)
15
+ - **breaking** content: one delta-application contract — implicit trailing retain is `try_apply`'s semantics (a short delta retains the untouched remainder; the error is over-consumption only), `apply` panics on an over-long delta instead of clamping (clamping is silent corruption), and `extend_to_base` is removed. `split_line` / `join_line` rebase marks through their one-char `\n` splice with `map_pos` — the same mapping the text-delta channel uses — so marks no longer drift across line ops and `apply_field_change` canonicalizes once (a single terminal normalize instead of one per stage); line sync rebuilds in one forward pass instead of per-`\n` `Vec` splices. Mark ops are specified in final-text coordinates (post-delta, post-line-op — the frame they validate against) (#926, #987)
16
+ - **breaking** core: storage blobs tagged `@0.81.0` / `@0.82.0` fail as an unknown schema version — the read-only `V0_81_0` / `V0_82_0` DTO trees and their forward migrations are retired (nothing persisted on this lineage predates `@0.92.0`; `0.82.0` was yanked). `V0_92_0` stays the oldest shape read, and its payload types back the current write path. DOCUMENT_STORAGE.md records variant retirement as the policy when no stored population remains (#929)
17
+ - **breaking** core,wasm,python: the markdown projection stops appending a trailing newline — `to_markdown` projects a *value*, not a file, so `field_markdown` / `body_markdown` (WASM `getMarkdown` / `exportMarkdown`, Python `export_markdown` / `get_markdown`) no longer grow a `\n`; `writer.set("subject", "Hello")` reads back as `"Hello"`, not `"Hello\n"`. `.qmd` files still end in one newline (owned by `Document::to_markdown`, the file writer) and the content fixed point is unchanged (import is newline-insensitive) (#965)
18
+ - **breaking** all: rename the content genus off its codec's name — crate `quillmark-richtext` → `quillmark-content`, type `RichText` → `Content` (and `RichTextLine`/`RichTextContainer`/`RichTextMark`/`RichTextIsland` → `ContentLine`/…), const `RICHTEXT_MEDIA_TYPE` → `CONTENT_MEDIA_TYPE` and its wire string `application/quillmark-richtext+json` → `application/quillmark-content+json`, `#[serde(skip)]` companion caches `FieldSchema::{default,example}_corpus` → `_content`, `SegmentMap.corpus: Range<usize>` → `.content`, Typst-emitter `EmittedContent` → `Emission` (it is markup + source map, not a Typst `content` value). Schema tokens `richtext` / `plaintext`, `FieldType::{RichText,PlainText}` variants, and the codec-specific `field_richtext` / `FieldRichtext*` / `apply_field_richtext_change` / `richtext(inline)` surface are unchanged — those name codecs, not the model. Canonical body JSON is nameless, so stored documents don't migrate; `contentMediaType` consumers pin to the new spelling. Retires the informal "corpus" noun to end the code/prose split (#976)
19
+ - **breaking** core,wasm,python: `getMarkdown` / `get_markdown` / `get_card_markdown` stop conflating an absent field with a present-but-not-richtext one — a present field that does not decode as richtext (a scalar/array/object a `storeField` wrote) now throws `FieldRichtextDecode` instead of reading back `undefined` / `""`; absence still returns the absent shape. Core `Card::field_markdown` becomes `Option<Result<String, RichtextDecodeError>>` (the projection twin of `field_richtext`). Rule: absence returns, mismatch raises; read the raw value with `get` (#968)
20
+ - feat(core,wasm): typed, anchor-preserving field revise — `TypedWriter::revise_field` / `CardWriter::revise_field` and `writer.reviseField` / `writer.card(i).reviseField` wrap core `Card::revise_field_checked` (diff-rebase surviving anchors, then schema-conform the result); the schema-bound verb lives on the writer, where the schema is (#957, #966)
21
+ - **breaking** wasm: the quill-taking `Document` methods become the hidden ABI under the writer — `commitField` / `commitFields` / `addCard` → `_commitField` / `_commitFields` / `_addCard`, dropped from the `.d.ts`; remove `doc.reviseChecked` (no runtime consumer — use `writer.reviseField`). The visible `Document` class then carries zero quill-taking methods (#966)
22
+ - **breaking** core: rename `EditError::BodyImport` → `EditError::Import` (message `body import failed:` → `markdown import failed:`) — the variant also fires on field-path imports (`revise_field`), where "body" misnamed it (#966)
23
+ - **breaking** wasm: fold `pushCard` into `insertCard(card, at?)` — one insertion verb per lane, absent `at` appends; `insertCard`'s parameters reorder to `(card, at?)`. Delete the deprecated `replaceBody` alias (use `revise({}, md)` or `writer.setBody`) (#961)
24
+ - feat(core,wasm): positioned card insert — `TypedWriter::add_card` / `writer.addCard` and the `addCard` ABI take an `at` position, so a positioned typed insert is one atomic call instead of `addCard` + `moveCard`; add `TypedWriter::remove_card` (mirrors JS `writer.removeCard`) and a JS `CardWriter.kind` getter (mirrors core `CardWriter::kind()`) (#961)
25
+ - **breaking** core: `Payload::insert` / `insert_fill` now validate the field-name and value-depth invariant at the boundary and return `Result<_, FieldViolation>`, closing the `payload_mut().insert(...)` hole that let a direct caller build an invalid document; pre-validated internal callers use the new `pub(crate)` `insert_unchecked` / `insert_fill_unchecked` (#958)
26
+ - feat(wasm,core): single-card reads — `doc.card(i)` (throws out of range), `doc.cardIndexById(id)` (first match; `$id` is non-unique), and `doc.seedOverlay(kind)`, backed by core `Document::card(i)` / `find_card(id)`. Reading one card, resolving a `$id`, or fetching a `$seed` overlay no longer serializes the whole `cards` array or main card (#956)
27
+ - **breaking** core: parse warnings live only on `ParseOutput` — the redundant `Document::warnings` field + `warnings()` getter are dropped and `Document::from_main_and_cards` no longer takes a `warnings` param (`Document` `PartialEq` is now a plain derive) (#959)
28
+ - **breaking** core: collapse the two parse functions into one entry — `Document::from_markdown` and `Document::from_markdown_with_warnings` are removed in favor of `Document::parse(md) -> Result<Parsed, ParseError>`, and `ParseOutput` is renamed `Parsed`. A document-only caller writes `parse(md)?.document`. Bindings are unaffected: WASM `Document.fromMarkdown` / Python `Document.from_markdown` keep their names and their `doc.warnings` getter (#964)
29
+ - feat(wasm,python): keyed card reads `getCardField(index, name)` / `getCardMarkdown(index, name?)` (py `get_card_field` / `get_card_markdown`) — the card-indexed twins of `get` / `getMarkdown`, mirroring the `commitCardField` / `setCardField` write verbs so card reads no longer require a `payloadItems` walk (#953)
30
+ - feat(content,wasm,python): `LineOp::SetContinues { line, continues }` — hard breaks lower op-wise. Split, join, and a text-delta `\n` all mint `continues: false` lines, so a within-block hard break (a paragraph hard break, a code fence's interior line) had no op and fell back to a whole-install, losing that edit's identity anchors. Threaded through the wire codec into WASM `applyChange` (TS union updated) and Python; `continues: true` on line 0 is rejected with `ApplyError::FirstLineContinues` before the write, leaving the content untouched (#949)
31
+ - feat(wasm): the runtime root re-exports the edit vocabulary its own signatures reference — `Content` / `ContentLine` / `ContentContainer` / `ContentMark` / `ContentIsland`, `Addr` / `Delta` / `Assoc` / `LineOp` / `MarkOp` / `ChangeBundle`, `CardInput` / `PathStep` — as type-only exports (single entry point preserved; no `/core` subpath), with a presence guard so a dropped re-export fails `npm run typecheck` (#948)
32
+
33
+ <!-- seed: commits since v0.94.0 — confirm the entries above cover them, then delete this comment
34
+ - chore: prune redundant logic and duplicate tests (post-0.94.0 residue scan) (#996)
35
+ - release: unbreak the crates.io publish lane; fold curated notes into the seed (#995)
36
+ - Emit date fields as click-to-edit value-objects (#990) (#994)
37
+ - Split `datetime` into strict `date` and `datetime` types (#991) (#993)
38
+ - Python binding: commit to the Tier-1 surface (#970) (#992)
39
+ - audit #982: complete the Content-genus residual sweep (retire "corpus") (#989)
40
+ - feat: schema-bound read view — `quill.view(doc)` and `TypedReader` (#988)
41
+ - Rebase marks through line ops; collapse bundle normalize (#987)
42
+ - core: retire the V0_81_0 and V0_82_0 storage read shims (#929) (#986)
43
+ - Document binding build performance guidance in CLAUDE.md (#984)
44
+ - richtext: to_markdown projects a value, not a file — no trailing newline (#965) (#977)
45
+ - docs(markdown-spec): scope $body wire claim, fix lossless→lossy projection (#983)
46
+ - Delete prose/review directory
47
+ - rename: content genus off its codec's name — RichText → Content, crate → quillmark-content (#976) (#981)
48
+ - Add note to not run cargo fmt (#980)
49
+ - fix(core,wasm,python): getMarkdown surfaces present-but-not-richtext instead of blanking (#968) (#979)
50
+ - docs: purge rogue .qmd file-extension mentions (#975)
51
+ - Rewrite CLAUDE.md for density (#974)
52
+ - docs: fix mkdocs strict build — drop cross-tree link to prose/canon
53
+ - wasm: name the main-card address (MAIN_CARD_ADDR), reject unknown addr keys (#969)
54
+ - core: collapse the two parse functions into one `Document::parse` -> `Parsed` (#964)
55
+ - core,wasm: writer-level reviseField; hide the quill-taking Document ABI (#966)
56
+ - core,wasm,docs: dense-prose pass over the #963 write surface
57
+ - docs: document the write-surface reshape (#955, #957, #960)
58
+ - python: rename opaque store verbs set_* → store_* (#960)
59
+ - wasm: unify Document on Addr addressing; store_* verbs; reviseChecked (#955, #960, #957)
60
+ - core: rename opaque store verbs set_* → store_*; add revise_field_checked (#960, #957)
61
+ - core: collapse the Payload insert helpers; tighten prose
62
+ - core: use plain code spans for pub(crate) refs in Payload::insert docs
63
+ - docs: 0.94→0.95 migration guide and BINDINGS parity refresh (#956, #958, #959, #961)
64
+ - wasm,core: writer/card-surface parity cleanups (#961)
65
+ - wasm,core: single-card, $id, and seed-overlay reads (#956)
66
+ - core: enforce field invariants at the Payload::insert boundary (#958)
67
+ - core: make ParseOutput the single owner of parse warnings (#959)
68
+ - docs(bindings): densify the card-read doc comments
69
+ - feat(wasm,python): keyed card reads mirroring the card write verbs (#953)
70
+ - docs: remove prose/simplifications backlog for greenfield re-analysis
71
+ - Add setContinues line op so hard breaks lower op-wise (#949)
72
+ - Re-export corpus edit vocabulary from @quillmark/wasm root (#948)
73
+ -->
74
+
75
+
76
+ ## v0.94.0 - 2026-07-15
77
+
78
+ These notes cover everything since v0.92.1. No 0.93.x was separately
79
+ published — the 0.93 milestone folds into this release, so the upgrade path
80
+ from 0.92.1 is the `0.92-to-0.93` and `0.93-to-0.94` guides read in sequence.
81
+
82
+ - feat(wasm): the live-session / canvas-paint surface graduates from
83
+ `@experimental` to stable — `Engine.open`, `LiveSession`, `apply` /
84
+ `ChangeSet`, `paint` / `PaintOptions` / `PaintResult`, `PageSize`, and the
85
+ `supportsCanvas` probe are now the committed preview API. The tag is dropped
86
+ from the runtime `.d.ts` / `.js`, the wasm README, and `PREVIEW.md`; further
87
+ shape changes follow the normal deprecation path rather than landing in any
88
+ 0.x. `Engine.render` / `supportedFormats` remain the one-shot path
89
+ - refactor(core)!: field ordering becomes fully structural — `ui.order` is
90
+ removed and an authored `order:` is a load error. Field and card-kind display
91
+ order is now the key order of the emitted schema (declaration order, backed by
92
+ an `IndexMap`), and the auto-stamped `order:` integer disappears from
93
+ `QuillConfig::schema()`; consumers walk the maps in key order instead of
94
+ sorting on a stamped index. Typed-dictionary / typed-table-row properties
95
+ render in declaration order, not alphabetically (#941). See
96
+ `docs/migrations/0.93-to-0.94.md`
97
+ - feat(core)!: a card-level `ui.groups` registry gives groups identity and
98
+ order. `ui.group` becomes a validated reference to a snake_case id
99
+ (`quill::unknown_group` for a dangling ref); the registry's declaration order
100
+ fixes group display order, labels derive from the id with a `title:` override,
101
+ and a bare label-as-identity group is deprecated (`quill::implicit_group`). A
102
+ nested `ui.group` is a load error (`quill::nested_group_not_supported`) (#941).
103
+ See `docs/migrations/0.93-to-0.94.md`
104
+ - feat(core,typst,wasm,python)!: `plaintext` and a first-class `enum` join the
105
+ schema. `plaintext` is navigable unformatted prose carried over the richtext
106
+ corpus (a literal codec, with a `plaintext(field)` helper on the Typst side);
107
+ `enum` is promoted to `type: enum` + `values:`, and the `enum:` modifier on
108
+ `string` is deprecated for one release. `string` narrows to open scalar data
109
+ (#938). See `docs/migrations/0.93-to-0.94.md`
110
+ - refactor(core)!: `type: richtext(inline)` retires — declare `type: richtext`
111
+ with `inline: true`. The old token is a hard `quill::field_parse_error`, and
112
+ `inline: true` on a non-richtext field is likewise rejected. Blueprint still
113
+ emits `richtext(inline)<markdown>` and `build_transform_schema` gains
114
+ `quillmark:inline: true`, both derived from the flag; documents and corpus
115
+ wire shapes are unaffected. See `docs/migrations/0.93-to-0.94.md`
116
+ - refactor(pdfform)!: `form.json` slims to a binding layer (`form@0.2.0`). Bound
117
+ `fields` drop `type` / `options` / `multiline` (derived from the schema
118
+ field's kind, `enum` values, and `ui.multiline`); unbound widgets move to a
119
+ `widgets` section; binding runs at load, so a bad `schema_field` fails with
120
+ `pdfform::dangling_binding` / `pdfform::unbindable_field` instead of a silent
121
+ blank. `form@0.1.0` is rejected and `$cards` absolute-index addressing is
122
+ removed. Widget geometry is placed once at bind, not per render (#940). See
123
+ `docs/migrations/0.93-to-0.94.md`
124
+ - refactor(core,richtext,wasm,python)!: the binding write surface settles into
125
+ two tiers over a document-free corpus codec. `quill.writer(doc)` (wasm and
126
+ Python alike) is the documented default — typed `set` / `set_all` / `setBody`
127
+ / `addCard` / `card(i)` and quill-free `get` / `getMarkdown` reads — layered
128
+ over the corpus lane (`importMarkdown` / `exportMarkdown` / `rebase` / `mapPos`
129
+ plus the addressed `install` / `revise` / `applyChange` verbs) and the opaque
130
+ `setField` primitive. The eager `bodyMarkdown` / `fieldMarkdown` projections
131
+ and the per-address body writers retire pre-release; `replaceBody` /
132
+ `replace_body` / `update_card_body` alias for one cycle; richtext fields gain
133
+ the anchor-preserving `revise_field`; the addressed `commit(addr, …)` is
134
+ deleted (subsumed by the writer). A core-vs-bindings parity table governs
135
+ drift (#925, #932). See `docs/migrations/0.93-to-0.94.md`
136
+ - refactor(wasm)!: the `Card` shape splits by direction — a read `Card` always
137
+ carries `body: RichText`, while `pushCard` / `insertCard` take a `CardInput`
138
+ whose `body` still accepts a markdown string and whose non-`kind` fields are
139
+ optional (#917). The card-write verbs become mechanical twins of their
140
+ main-card names: `updateCardField` / `updateCardFields` rename to
141
+ `setCardField` / `setCardFields` (#895). See `docs/migrations/0.93-to-0.94.md`
142
+ - fix(typst/overlay): underline / strike decoration ink no longer truncates
143
+ `$body` field regions — the region geometry is taken before decoration strokes
144
+ extend the glyph ink box, so a highlighted body field's box matches the text
145
+ instead of the overrun (#937)
146
+ - chore: migrate org references `quillmark-org` → `borb-sh` across the tree
147
+ - fix(richtext): the markdown-export codec never leaks a delimiter into the
148
+ corpus. An editor `apply_mark_ops` mark can wrap a span markdown can't
149
+ represent (a `strong`/`emph`/`strike` edge on punctuation/symbols/whitespace,
150
+ or abutting a literal `*`) — the run would re-import as literal `**`/`*`/`~~`
151
+ text (bolding `a.` used to export `**a.**b`). `to_markdown` now verifies each
152
+ rendered line by re-parse and drops any mark whose emission would alter the
153
+ text, so the text always round-trips; only the unrepresentable formatting is
154
+ lost. Import-domain corpora are unaffected (still an exact fixed point).
155
+ - feat(core,wasm,python)!: typed field writes via schema-carried types. One
156
+ per-type write dispatch (`conform_value(value, schema, mode)`) unifies the
157
+ render floor's coercion with a strict-write mode behind a `Leniency` flag; one
158
+ typed writer per address, `Card::commit_field(name, value, &FieldSchema)`,
159
+ dispatches on the schema — the write surface stays O(1) in field types. Adds
160
+ `EditError::FieldConform` for non-richtext mismatches (richtext keeps
161
+ `FieldRichtextDecode` / `FieldRichtextNotInline`). A schema-bound
162
+ `TypedWriter` (`Quill::writer(&mut doc)`) is the front door: `set` / `set_all`
163
+ resolve field types and strict-commit; a name the schema does not declare is a
164
+ typo on the typed path, so it fails with `EditError::UnknownField` instead of
165
+ falling to the opaque store (#918) — opaque storage stays available through the
166
+ raw `set_field` / `setField` / `setCardField` verbs. Bindings gain
167
+ `commitField` / `commitCardField` (wasm) and `commit_field` /
168
+ `commit_card_field` (Python, net-new — Python had no richtext field writer).
169
+ The pre-release richtext-specific writers are removed in the same cycle:
170
+ `Card::set_field_richtext`, wasm `setRichtextField` / `updateCardRichtextField`
171
+ — use the typed writer, which carries the `inline` constraint in the schema.
172
+ Strict writes drop the render floor's cross-type `Boolean`↔`Number` coercions
173
+ and fail a shape mismatch at the write, not at a later render (#893)
174
+ - remove(core,richtext,wasm)!: delete the incremental-edit surface — the
175
+ per-field change log and everything layered on it: `richtext::ChangeLog` /
176
+ `FieldChange` / `StaleRevision`; `LiveSession::revision` /
177
+ `record_field_delta_at` / `record_field_change_at` / `ensure_base_revision` /
178
+ `map_field_pos` / `apply_for_field_delta`; the WASM `applyFieldDelta` /
179
+ `mapFieldPos` / `revision` and the `Delta` DTO; and the `revision` stamp on
180
+ `RenderedRegion` / `CorpusHit` (and `FieldRegion` / `CorpusHit` on the wire).
181
+ Anchoring a caret or selection across edits belongs to the editor's own
182
+ transaction mapping (a ProseMirror / CodeMirror `StepMap`), not a parallel
183
+ core-side position map: the bidirectional preview↔editor cursor bridge is
184
+ `positionAt` / `locate` over the current compile, exact inverses that never
185
+ consulted the change log. Whole-document `apply(doc)` stays the one edit verb.
186
+ This dissolves #886's anchor-stranding half outright and drops the
187
+ half-built delta path behind its per-keystroke-marshalling half; `Delta` /
188
+ `diff` / `diff_import` / the mark & line op channels remain as the corpus
189
+ writers' substrate (`replace_body`, `import_body_delta`, `apply_body_change`)
190
+ (#886)
191
+ - feat(core,wasm): `field_boxes(field)` / `LiveSession.fieldBoxes(field)` derive
192
+ the whole-field highlight — one union rect per page over the field's
193
+ `span`-bearing content segments — so a "highlight the focused field" consumer
194
+ stops reimplementing the span-filter + per-page union by hand. `regions()`
195
+ stays the low-level disjoint truth (#829); the helper owns the union, and is
196
+ content-only (a scalar-reference/widget-only field returns `[]`, its box being
197
+ a single `regions()` rect). Core `field_boxes(&[RenderedRegion], field)` is a
198
+ pure function so the one-shot `RenderResult.regions` sidecar gets it too (#884)
199
+ - feat(core,wasm): `CorpusHit.granularity` (`HitGranularity` = `cluster` |
200
+ `segment`) reports whether `positionAt`'s `pos` resolved cluster-exact or
201
+ floored to the containing segment's start (origin-less ink, a multi-line code
202
+ fence's interior), so a caret UI trusts a `cluster` offset for the caret and
203
+ treats a `segment` one as a segment selection instead of guessing. Additive-
204
+ optional, omitted from the wire when the backend does not report it (#884)
205
+ - fix(wasm): `Engine.supportsCanvas` and `LiveSession.supportsCanvas` gain doc
206
+ comments cross-referencing each other: the two are spelled identically but
207
+ answer different questions (a pre-session backend estimate vs. this compile's
208
+ authoritative answer, which can diverge — e.g. a 0-page document) — the
209
+ divergence is now visible where each is used instead of only discoverable at
210
+ runtime (#883)
211
+ - fix(core): drop two rustdoc intra-doc links from public items
212
+ (`RichtextDecodeError`, `Card::set_field_richtext`) to the private
213
+ `decode_richtext_value`, which `-D rustdoc::private-intra-doc-links` (part of
214
+ the lint gate) rejects since the link can never resolve for a doc reader;
215
+ reworded to a plain code span, matching the existing convention elsewhere in
216
+ the same file for referencing a private helper from public docs
217
+ - fix(wasm): drop the `revision?` field from the public `CorpusHit`/`FieldRegion`
218
+ types and the broken `{@link LiveSession.mapFieldPos}` / `.revision` references
219
+ in `runtime.d.ts`. The delta API (`applyFieldDelta`/`revision`/`mapFieldPos`) is
220
+ not forwarded through `runtime.js`, so no published consumer could reach the
221
+ methods those fields pointed at, and the stamped `revision` was always `0` on
222
+ the reachable read paths (whole-doc `apply` is revision-neutral). The public
223
+ types no longer advertise a capability the shipped `LiveSession` doesn't expose
224
+ (#850)
225
+ - refactor(core)!: `RenderSession` collapses into `LiveSession` — a persistent,
226
+ incremental compiler that owns preview (#778). Reads (`render`, the canvas
227
+ seam, `regions`) serve the session's current compile; the new transactional
228
+ `apply(json_data)` recompiles in place (on `Err` every read keeps serving the
229
+ last-good compile) and returns `ChangeSet { page_count, dirty_pages }` so a
230
+ preview repaints `dirty ∩ visible`. Typst applies incrementally: the session
231
+ persists its `QuillWorld` (fonts/packages/assets parsed once), swaps document
232
+ data via `Source::replace`, and fingerprints visible page content for the
233
+ dirty set; pdfform re-resolves + re-flattens (cheap by construction). New
234
+ `RenderError::ApplyUnsupported` is the seam default. The callerless
235
+ `typst_session_of` is removed. WASM: the `RenderSession` class is renamed
236
+ `LiveSession` and gains `apply(doc): ChangeSet`; don't re-open per edit. The
237
+ Typst backend now evicts `comemo`'s process-global cache after every compile,
238
+ bounding memory over long editing sessions. See
239
+ `docs/migrations/0.92-to-0.93.md`
240
+ - remove(dotnet)!: drop the .NET binding (`crates/bindings/dotnet`, the
241
+ `quillmark-dotnet` crate, its `csharp/` managed layer, CI job, and NuGet
242
+ publish workflow). Second-class and unmaintained relative to WASM/Python;
243
+ removed rather than carried as bloat. Python and WASM are unaffected.
244
+ - refactor(core)!: field regions move from `RenderResult` to a session-level
245
+ query, `RenderSession::regions()` (WASM `session.regions()`), and are keyed on
246
+ the quill schema field path, not the backend widget. Only the interactive
247
+ preview path wants region geometry; a one-shot byte render (PDF/PNG/SVG) does
248
+ not, so `RenderResult.regions` is removed and the geometry is read once off the
249
+ compiled session without a render. `RenderedRegion` (and the WASM
250
+ `FieldRegion`) drop `name`/`kind`/`fieldType`/`value` for a single `field`
251
+ carrying the schema address (e.g. `signature_block`); the pdfform AcroForm
252
+ widget name no longer leaks. A region is emitted only for a schema-bound field
253
+ — an unbound widget produces none. `RegionKind` is removed; the `quillmark-pdf`
254
+ `FieldSpec` gains `schema_field` and `stamp`/`flatten` return plain bytes
255
+ (`StampResult` is gone). Regions are geometry for overlays and canvas↔editor
256
+ cross-navigation, never a compositing input (#773). See
257
+ `docs/migrations/0.92-to-0.93.md`
258
+ - feat(pdfform)!: the `pdfform` backend now exports PNG and SVG as first-class
259
+ `render()` output formats (`SUPPORTED_FORMATS == [Pdf, Svg, Png]`); PNG
260
+ rasters at `RenderOptions::ppi` (default 144). The `preview` cargo feature is
261
+ removed — the hayro raster/SVG/PNG seam is always linked, so SVG/PNG/canvas
262
+ work out of the box rather than behind a flag. The `quillmark` crate's
263
+ `pdfform-preview` feature is folded into `pdfform`; in the wasm crate both the
264
+ `typst` and `pdfform` build variants link the `web-sys` canvas painter directly
265
+ - fix(quillmark-pdf): `find_dict_value` now walks the dict as strict
266
+ key→value pairs, so a Name in *value* position (e.g. `/Subtype /Producer`)
267
+ is no longer mis-matched as a key; the object/dict scanners also skip
268
+ `%`-comments, so `endobj` or a key token inside a comment can't derail
269
+ parsing of a base PDF. The `<<…>>`/`[…]` depth walkers (`extract_outer_dict`
270
+ and `read_value_end`'s nested-dict/array branches) skip `%`-comments and
271
+ literal `(…)` strings uniformly, so a `>>`/`]` carried inside a comment or
272
+ string no longer truncates a dict/array and drops the keys after it
273
+ - feat(pdfform): add the Typst-free `pdfform` backend + shared `quillmark-pdf`
274
+ AcroForm stamping spine; rewire Typst signatures onto the spine; thread a
275
+ `regions` sidecar through `RenderResult` and generalize the raster-preview
276
+ seam (#749, #750). See `prose/canon/ARCHITECTURE.md` and
277
+ `docs/quills/pdfform-backend.md` for the shipped design.
278
+ - refactor(pdfform): PDF output is always an interactive AcroForm (Technique A).
279
+ Value-flattening is internal machinery backing the SVG/PNG/canvas raster
280
+ outputs, never a PDF deliverable. The public `RenderOptions.flatten` knob is
281
+ removed across core and all four bindings (it was wired only in wasm, hardcoded
282
+ `false` in Python, and ignored in .NET)
283
+ - fix(pdfform): the flatten path transcodes values to WinAnsi (with a
284
+ `WinAnsiEncoding` font) so accented/Latin-1 text renders correctly in the
285
+ raster output, and clips each value to its field box so long values can't
286
+ overflow
287
+ - refactor(quillmark-pdf): hoist the shared PDF byte-serialization (object/text
288
+ writers, `/Info /Producer` stamp) into `quillmark_pdf::writer`, consumed by
289
+ both the stamp and flatten paths; `find_object_bytes` now matches any object
290
+ generation and returns the live (last) revision
291
+ - docs(canon): canonize `$ext.editor.title` as the slot for a per-card display name
292
+ - refactor(core)!: remove the hand-set `Backend::supports_canvas()`; derive
293
+ canvas capability from the one seam instead. `RenderSession::supports_canvas()`
294
+ (authoritative, from `page_size_pt`) and `formats_support_canvas()`
295
+ (pre-session hint, from output formats) replace it, so the capability can no
296
+ longer disagree with what `paint` does. The engine and WASM `supportsCanvas`
297
+ surfaces are unchanged in shape. See `docs/migrations/0.92-to-0.93.md`
298
+ - build(wasm)!: rename the WASM engine feature `render` → `typst` (now the
299
+ default) and add a `pdfform` build variant, so a Typst-free
300
+ PDF-form bundle can ship without Typst. From-source builders pass
301
+ `--features typst` where they used `--features render`; the published JS API is
302
+ unchanged. See `docs/migrations/0.92-to-0.93.md`
303
+
3
304
  ## v0.92.1 - 2026-06-22
4
305
 
5
306
  - Accept uppercase field names; reserve only `$`-prefixed keys (#730)
6
307
  - docs: canonize $ext.editor.title as per-card display name slot (#729)
7
308
 
8
309
 
9
- ## Unreleased
10
-
11
- - docs(canon): canonize `$ext.editor.title` as the slot for a per-card display name
12
-
13
310
  ## v0.92.0 - 2026-06-22
14
311
 
15
312
  - 0.92 technical-debt sweep: correctness, $seed hardening, de-duplication (#727)