@quillmark/wasm 0.98.0 → 0.100.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,212 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.100.0 - 2026-08-03
4
+
5
+ A content field gets one resting form, and the last reserved `$` key with no
6
+ reader is removed. All breaking changes are covered by
7
+ `docs/migrations/0.99-to-0.100.md`. Stored documents load unchanged, but a row
8
+ read through the bound door converges once: read-repair, not a schema-version
9
+ event. One ordering matters, and the guide's "Legacy data" section states it —
10
+ conform a stored population before exporting markdown from it.
11
+
12
+ - refactor(core,wasm,python)!: content fields have one resting form, enforced at
13
+ load. `Quill::conform(&mut doc)` is the primitive and `Quill::parse(md)`
14
+ (parse, then conform) the convenience — the documented primary ingestion path,
15
+ `quill.parse` / `quill.conform` on both bindings. A `richtext` field rests as
16
+ the canonical content object, a `plaintext` field as its **literal string**, so
17
+ the stored shape is a property of the codec instead of the construction lane:
18
+ `equals` and content hashes stop separating semantically identical documents.
19
+ The typed writer commits `plaintext` as a string, and `revise_field` diffs it
20
+ through the literal codec (a byte-identical revise of `a \*b\*` used to commit
21
+ `a *b*`). `Document::parse` / `Document.fromMarkdown` stay exactly as they
22
+ were, demoted to the transport/repair door. Conform is idempotent, a byte no-op
23
+ on an already-canonical document, and reports a `conform::*` warning where the
24
+ strict write refuses rather than retyping or rejecting; a `$quill` naming
25
+ another quill errors before any mutation (#1160, #1162). See
26
+ `docs/migrations/0.99-to-0.100.md`
27
+ - fix(core)!: markdown exported from a `plaintext` field resting as a content
28
+ object is markdown-escaped. Emit is schema-free and cannot tell a `plaintext`
29
+ content from a `richtext` one, so `a *literal* line` leaves as
30
+ `a \*literal\* line` and re-parses with the backslashes as characters — one
31
+ more layer per save cycle. Only the typed writer produced that rest, and the
32
+ string rest above deletes it rather than managing it: load, conform, and
33
+ re-store a population before exporting markdown from it. Markdown already
34
+ exported under ≤0.99 is corrupt at rest, its escapes indistinguishable from
35
+ authored ones, so re-export it from the conformed rows (#1159). See
36
+ `docs/migrations/0.99-to-0.100.md`
37
+ - fix(core,wasm,python)!: a `plaintext` field resting as a string reads through
38
+ the **literal** codec, not markdown — `note: 'a *literal* line'` read back as
39
+ `a literal line` while render and validation kept the asterisks. Only the
40
+ string lane was wrong; the committed-object lane always decoded correctly, so
41
+ a consumer that pre-escaped a `plaintext` field to survive the read drops the
42
+ escaping. Alongside it, `reader.get_content` / `reader.getContent` returns a
43
+ content field's `Content` corpus whichever lane stored it, so a consumer
44
+ holding a corpus editor stops branching on the wire shape. `EditError` gains
45
+ `FieldNotContent` (`edit::field_not_content`) for a declared type that is not a
46
+ content leaf; core adds `Card::field_plaintext_content` (#1154). See
47
+ `docs/migrations/0.99-to-0.100.md`
48
+ - refactor(core,wasm,python)!: card `$id` is removed — the reserved key, its
49
+ resolver (`Document::find_card` / `doc.cardIndexById` / `doc.card_index_by_id`),
50
+ the uniqueness contract (`EditError::CardIdCollision` / `EmptyCardId`, the
51
+ `parse::card_id_*` warnings, the storage rejection), `Card::id` /
52
+ `Payload::{id, set_id, take_id}` / `Document::{set_card_id, remove_card_id}`,
53
+ the `PayloadItem::Id` and `CardWire.id` wire members, and the projected `id` on
54
+ both bindings' card shape. Nothing in the engine read it and it never reached a
55
+ backend, so what is left after removing the machinery that served the resolver
56
+ is `$ext` with a reserved name. A block declaring `$id` no longer parses and a
57
+ blob carrying an `id` payload item no longer loads: a hard cutover, no
58
+ tolerate-and-ignore window. Per-card consumer keys move to `$ext` under a
59
+ namespace you own, with no uniqueness, no collision check, and no repair
60
+ (#1151). See `docs/migrations/0.99-to-0.100.md`
61
+ - refactor(content)!: `Content`, `Line`, `Mark`, and `Island` take
62
+ `#[non_exhaustive]` — the four public structs the 0.99 sweep missed, that pass
63
+ having run as two issues split by crate. Their literals give way to `new` plus
64
+ the `with_*` setters on the same terms as the rest of the API; every field stays
65
+ `pub`, so reading and assigning are unchanged. `Delta`, `Segment`, and
66
+ `BaseLengthMismatch` stay open deliberately and now say so in their rustdoc.
67
+ A Rust source break only: nothing about the wire, the canonical bytes, or the
68
+ bindings moves (#1146). See `docs/migrations/0.99-to-0.100.md`
69
+ - feat(core,wasm,python): `Diagnostic.args` — the facts `message` interpolates,
70
+ keyed by name, so a consumer with its own string table selects a sentence by
71
+ `code` and fills it itself. Values keep their JSON shape (a list arrives as a
72
+ list, a count as a number), engine prose never rides under a key, and a
73
+ formatter missing a key falls back to `message` wholesale. `prose/canon/ERROR.md`
74
+ § "Diagnostic args" tabulates the keys per code and a test fails when code and
75
+ canon disagree (#1130)
76
+ - fix(core): the `$quill` mismatch message and hint name the pairing rather than
77
+ the verb. `check_quill_reference` gates every schema-bound door now, not the
78
+ render path alone, so a `quill.parse` failure no longer reads "was rendered
79
+ with". The codes (`quill::name_mismatch` / `quill::version_mismatch`) are
80
+ unchanged
81
+ - test(fuzz): the resting-form invariant gains a target, stated as three
82
+ properties — conform is a fixed point, parse-then-conform equals typed-write
83
+ per content field, and a document through the markdown surface and back settles
84
+ after one pass (exactly, for `plaintext`, whose codec is lossless both ways)
85
+ - docs: the cycle's stale pages are repaired. Both binding READMEs gain the bound
86
+ door and the corpus read, `revise_field` is documented per declared type on all
87
+ four surfaces instead of as a markdown-only richtext verb, and four canon claims
88
+ that outran the tree are corrected
89
+
90
+
91
+ ## v0.99.0 - 2026-08-01
92
+
93
+ The 1.0.0 API freeze lands ahead of the tag, and the content codec closes its
94
+ last open gaps. All breaking changes are covered by
95
+ `docs/migrations/0.98-to-0.99.md`. Stored documents are unaffected: a `0.98` blob
96
+ loads byte-identically and `0.99` writes the same bytes for the same content.
97
+
98
+ - refactor(core,content,pdf,pdfform)!: the public API opens. 75 public types take
99
+ `#[non_exhaustive]` — nothing in the workspace carried it before — so an
100
+ exhaustive `match` needs a `_` arm and a struct literal gives way to `new` plus
101
+ `with_*` setters. `Backend` is sealed, `OutputFormat::ALL` and
102
+ `Content::RESERVED_{MARK_TYPES,LINE_KINDS,CONTAINERS}` become slices, and
103
+ `RenderOptions { .., ..Default::default() }` becomes
104
+ `RenderOptions::default().with_output_format(fmt)`. Four stay exhaustive and
105
+ say so: the storage DTOs, frozen per schema version, plus
106
+ `quillmark_pdf::FieldType`, `KnownIslandType`, and `Fidelity`, where an
107
+ out-of-crate `_` arm is silent (a field that draws nothing, an island dropped
108
+ from the projection, a fidelity rung nothing warns about). The rules are
109
+ canonized in `prose/canon/COMPATIBILITY.md` (#1090, #1103). See
110
+ `docs/migrations/0.98-to-0.99.md`
111
+ - refactor(core)!: the YAML engine leaves the public API. `QuillValue::from_yaml_str`
112
+ and `QuillConfig::schema_yaml` return `quillmark_core::YamlError` instead of
113
+ `serde_saphyr` types, so a `0.0.x` dependency release is no longer a break to
114
+ `quillmark-core`. The message is sanitized (the engine's own Rust API names are
115
+ stripped), and `from_yaml_str` gains the `MAX_YAML_DEPTH` budget its siblings
116
+ already carried (#1099, #1101). See `docs/migrations/0.98-to-0.99.md`
117
+ - fix(content)!: the reserved-name rule reaches the wire. `attrs` beside a
118
+ built-in discriminator resolved to the built-in and dropped the payload in
119
+ silence; the authored lane now refuses it on all four axes, where a host writes
120
+ it. Reading never got stricter — a blob from before a promotion still opens.
121
+ A table cell keeps its own unknown keys too, canonicalization now rewriting it
122
+ in place rather than minting a fresh `{text, marks}` (#1084, #1085, #1086,
123
+ #1092). See `docs/migrations/0.98-to-0.99.md`
124
+ - fix(content)!: opaque payload depth is bounded at `MAX_JSON_DEPTH` (128) on the
125
+ `Value` lane, where an unbounded one took the WASM module down with a
126
+ stack-overflow trap rather than a catchable error. The WASM guard sits on the
127
+ JS side of the boundary, since `serde_wasm_bindgen` recurses while building the
128
+ value, and covers every door that takes opaque host JSON: `install` and
129
+ `applyChange`, plus `makeCard`'s field values and `insertCard`'s payload items
130
+ (#1093). See `docs/migrations/0.98-to-0.99.md`
131
+ - fix(content)!: island `loss` becomes the fifth open set. An unrecognized class
132
+ round-trips verbatim instead of being rewritten to `unrepresentable`, so merely
133
+ opening a document no longer moves its content hash. `Loss` opens on the island
134
+ `type` axis' terms rather than the block axes': it becomes an opaque string
135
+ wrapper with `LOSSLESS` / `DEGRADED` / `UNREPRESENTABLE` consts, one value per
136
+ wire string, so a built-in's name has no second spelling and needs no
137
+ reserved-name rule. `Fidelity` is the closed view `Loss::fidelity` returns, and
138
+ is where a consumer switches; `Loss` consequently loses its `Copy` derive
139
+ (#1091, #1142). See `docs/migrations/0.98-to-0.99.md`
140
+ - refactor(core,typst,pdf)!: workspace-internal seams leave the published
141
+ surface. `quillmark-pdf`'s `reader`/`writer` modules and `quillmark_typst::emit`
142
+ become `#[doc(hidden)]`; the op-wire encoders emit an unknown's `attrs` in
143
+ caller key order, the redundant per-encoder sort having been dropped (canonical
144
+ content bytes are unchanged, the terminal sort still running) (#1095). See
145
+ `docs/migrations/0.98-to-0.99.md`
146
+ - fix(wasm)!: a `Quill` or `Document` from a second copy of `@quillmark/wasm` is
147
+ refused everywhere, as a `QuillmarkError` coded `runtime::foreign_handle` that
148
+ names the cause and hints `npm ls @quillmark/wasm`. 0.98 half-worked there:
149
+ `Engine` was duck-typed, so a quill from copy A rendered on an engine from copy
150
+ B at a per-copy clone cache nobody could see, while `Document.equals`,
151
+ `Quill.validate`, `Quill.resolve` and the typed writer met wasm-bindgen's bare
152
+ `expected instance of Document` at a value that *is* a `Document`. The check
153
+ covers `Engine` (`render`, `open`, `supportedFormats`, `supportsCanvas`),
154
+ `LiveSession.apply`, the writer and reader binds, and the three by-reference
155
+ core methods; a value that is not a handle at all keeps its own
156
+ `runtime::not_a_document` / `runtime::not_a_quill`. Nothing changes for a
157
+ one-copy install (#1132, #1136). See `docs/migrations/0.98-to-0.99.md`
158
+ - feat(wasm): `isUnknownLine` / `isUnknownContainer` / `isUnknownMark` /
159
+ `isUnknownIsland` answer known-vs-unknown on each open set, so a consumer no
160
+ longer enumerates built-in names in its own source. `ContentLineKind` is
161
+ re-exported from the package entry point, so a `setKind` op type-checks without
162
+ a cast
163
+ - feat(python): the Tier-1 gaps close. `doc.card(i)`, `doc.card_index_by_id(id)`,
164
+ and `doc.seed_overlay(kind)` are the single-card, `$id`, and seed reads WASM
165
+ already had, and the wheel ships `py.typed` plus stubs, so mypy and Pyright see
166
+ real signatures where the surface used to resolve to `Any` (#1011)
167
+ - fix(typst): four quill-load defects — a skipped asset, an unparseable
168
+ `typst.toml`, a skipped package file, a declared-but-absent entrypoint —
169
+ become `RenderResult` warnings (`typst::path_skipped`,
170
+ `typst::package_manifest`, `typst::package_entrypoint_missing`) instead of
171
+ `eprintln!` that wasm32 has nowhere to print (#1102)
172
+ - fix(wasm): the npm package states the license the workspace actually grants.
173
+ `package.json` declared `MIT OR Apache-2.0` where every Rust crate, the
174
+ workspace manifest, and the only `LICENSE` file in the tree are `Apache-2.0`,
175
+ and the package shipped no license text at all: `build-wasm.sh` copied
176
+ `LICENSE-MIT` and `LICENSE-APACHE`, neither of which exists. It now copies
177
+ `LICENSE`, or refuses to produce a package
178
+ - ci: the release gates the tag actually needs. New `package` (builds every
179
+ publishable crate from its own archive and asserts each ships its `LICENSE`),
180
+ `msrv` (holds `rust-version` to something true), and `audit` (bare `cargo
181
+ audit` over the lockfile) jobs; the workspace moves to edition 2024 and
182
+ declares MSRV 1.92. The `semver` job is dropped — it compared the tree's
183
+ unbumped version against itself — and `COMPATIBILITY.md` names the writer and
184
+ reviewer as what holds the promise instead (#1105, #1106, #1107, #1108)
185
+ - ci: the rustdoc gate covers the whole workspace. A bare `cargo doc` walks
186
+ default-members and never lints a crate outside it — the blind spot that let
187
+ the `Delta` links rot on the WASM surface and four more in the published
188
+ `quillmark-content`. `--workspace` needs no `--exclude` and covers the next
189
+ such crate on the day it lands
190
+ - test(fuzz): the four JSON decode lanes the bindings expose gain coverage
191
+ (#1104)
192
+ - docs(canon): `COMPATIBILITY.md` states the crate-API promise — what
193
+ `#[non_exhaustive]` does and does not buy, when to mark an enum, and what no
194
+ attribute sweep catches
195
+ - docs(all): the em-dash leaves comments and prose, folded to a colon, comma,
196
+ semicolon, or parentheses across ~2900 sites. `dense-prose` banned it while
197
+ every exemplar it named used it; the corpus now matches the rule. A handful of
198
+ diagnostic and CLI message strings repunctuate with it (`edit::body_only`,
199
+ `validation::must_fill`, the pdfform bind errors, `--help`); codes, severities,
200
+ and paths are unchanged. The character stays where it is the subject rather
201
+ than punctuation: the WinAnsi encoding table, the YAML en/em-dash fixtures, and
202
+ `docs/migrations/` (#1135)
203
+ - chore(core): `serde-saphyr` moves to `1.0`. The two call sites that built
204
+ `Options`/`SerializerOptions` with struct-literal-plus-`..Default::default()`
205
+ now go through the crate's own `options!`/`ser_options!`/`budget!` macros,
206
+ which the 1.0 release requires since both structs are `#[non_exhaustive]`.
207
+ `serde_saphyr` types stay out of `quillmark-core`'s public API (see the YAML
208
+ engine entry above), so nothing downstream moves
209
+
3
210
  ## v0.98.0 - 2026-07-28
4
211
 
5
212
  Five breaking changes, all covered by `docs/migrations/0.97-to-0.98.md`.
@@ -148,7 +355,7 @@ Stored documents are unaffected: a `0.97` blob loads byte-identically and
148
355
  - **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)
149
356
  - **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)
150
357
  - **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)
151
- - **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)
358
+ - **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"`. The content fixed point is unchanged (import is newline-insensitive) (#965)
152
359
  - **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)
153
360
  - **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)
154
361
  - 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)
package/LICENSE ADDED
@@ -0,0 +1,13 @@
1
+ Copyright (c) 2025 Nibs
2
+
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, software
10
+ distributed under the License is distributed on an "AS IS" BASIS,
11
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ See the License for the specific language governing permissions and
13
+ limitations under the License.