@quillmark/wasm 0.99.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,93 @@
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
+
3
91
  ## v0.99.0 - 2026-08-01
4
92
 
5
93
  The 1.0.0 API freeze lands ahead of the tag, and the content codec closes its
@@ -119,109 +207,6 @@ loads byte-identically and `0.99` writes the same bytes for the same content.
119
207
  `serde_saphyr` types stay out of `quillmark-core`'s public API (see the YAML
120
208
  engine entry above), so nothing downstream moves
121
209
 
122
- <!-- seed: commits since v0.98.0, confirm the entries above cover them, then delete this comment
123
- - chore(core): move the time format descriptions to parse_borrowed::<3>
124
- - build(deps): bump lopdf from 0.42.0 to 0.44.0
125
- - build(deps): bump thiserror in the cargo group across 1 directory
126
- - build(deps): bump taiki-e/install-action
127
- - chore(core): bump serde-saphyr to 1.0
128
- - Split lint back out of test
129
- - Link every shipped artifact in CI, and carry v0.99.0's notes forward
130
- - dense-prose pass over the CI scope
131
- - Cut CI to one job per shipped surface
132
- - Stop the release from pushing to a branch that takes only PRs
133
- - Fix three claims the independent review falsified
134
- - Correct the release notes' exhaustive-type count
135
- - dense-prose pass over the review's scope
136
- - Close the two payload doors the depth guard missed
137
- - Ship the license the workspace actually grants
138
- - Correct the release docs against the tree they describe
139
- - CLAUDE.md: tighten the Tests section to house voice
140
- - CLAUDE.md: drop `uv run` from the Python binding flow
141
- - CLAUDE.md: gate local binding builds, name the cheap profiles
142
- - chore(release): v0.99.0
143
- - Split serial.rs's fixed-point claim into the two it conflated
144
- - Make Fidelity the one place a loss class is spelled
145
- - Make an island's loss class injective instead of guarded
146
- - dense-prose and simplify passes over the handle-check work
147
- - Keep the colon out of embedded YAML values
148
- - Sweep the em-dash out of comments and prose
149
- - Refuse a core handle from a second copy, everywhere
150
- - Replace the em-dash ban with the rule the exemplars follow
151
- - dense-prose pass over the foreign-handle prose
152
- - Extend the policy to the writer/reader lane, and draw the read/write line
153
- - Keep the patched method names, and stop over-claiming in the warning
154
- - Tolerate foreign core handles on the by-reference methods
155
- - Correct the quill file-size cap doc and MiB units
156
- - Collapse the rustdoc gate onto --workspace
157
- - dense-prose pass over the release-readiness fixes
158
- - Close the release-readiness gaps found against v0.98.0
159
- - Drop the semver job
160
- - Trim the Python typing work to its load-bearing parts
161
- - Revert "Record KnownIslandType::ALL's slice shape in the 0.99 guide"
162
- - Record KnownIslandType::ALL's slice shape in the 0.99 guide
163
- - Close the Python Tier-1 gaps: single-card reads and a typed surface
164
- - Drop the top-level-await plugin the vite 8 test config cannot load
165
- - Shape the reserved-name lists as slices
166
- - Shape the island ALL as a slice; list the 0.99 guide as published
167
- - Prune rottable duplication from CLAUDE.md
168
- - Adopt the editor's dense-prose rewrite; compress CLAUDE.md
169
- - build(deps): bump the cargo group across 1 directory with 12 updates
170
- - build(deps): bump the actions group across 1 directory with 11 updates
171
- - build(deps): bump tsify from 0.4.5 to 0.5.6
172
- - build(deps): bump toml from 0.8.23 to 1.1.3+spec-1.1.0
173
- - build(deps): bump similar from 2.7.0 to 3.1.1
174
- - build(deps): bump serde-saphyr from 0.0.23 to 0.0.29
175
- - Run the semver job on release PRs, not on every one
176
- - Drop the dead check-cfg rustflag that crashed the semver job
177
- - docs: dense-prose pass over the new test comments
178
- - dense-prose pass over the branch
179
- - Gate the compatibility promise mechanically, and write it down
180
- - test(bindings): cover the WASM and Python exports nothing exercised
181
- - Close the struct half of the 1.0.0 non_exhaustive sweep, and seal Backend
182
- - test(pdfform): drive unbound text/checkbox/choice through the fixture
183
- - Move the workspace to edition 2024
184
- - build(deps-dev): bump vitest in /crates/bindings/wasm
185
- - build(deps-dev): bump typescript in /crates/bindings/wasm
186
- - prose: drop the readiness proposal and SECURITY.md
187
- - ci: drop the Windows matrix from the test job
188
- - Finish the coerce_value_strict cleanup in the fuzz crate
189
- - dense-prose pass across the code and docs
190
- - Revert the wasm job to `npm install`
191
- - Trim CI to the checks v1.0.0 actually needs
192
- - Cut the two CI costs that buy nothing
193
- - Clear the RUSTSEC vulnerabilities the audit job actually found
194
- - Pin the tree to LF so the Windows runner reads what the test wrote
195
- - Cleanup pass: cut the machinery the fix did not need
196
- - prose: record what landed against the v1.0.0 readiness findings
197
- - fuzz: cover the four JSON decode lanes the bindings expose
198
- - Declare and hold an MSRV
199
- - Ship the license text, verify the packages, scan the dependencies
200
- - Open the engine crates' public enums ahead of the 1.0.0 freeze
201
- - Hide the workspace-internal seams from the published surface
202
- - typst: quill-load defects become session warnings, not stderr
203
- - core: own the YAML boundary error, and budget the third entry point
204
- - prose: index the v1.0.0 readiness findings against their issues
205
- - prose: v1.0.0 production-readiness review
206
- - Drop issue markers; dense-prose the unreleased 0.99 guide
207
- - dense-prose pass over the PR 1097 scope
208
- - docs: record the op-wire key-order change in the unreleased guide
209
- - content: carry the `<u>` bit on the event, not in a shared set
210
- - content: one key sorter for the canonical form
211
- - content: bound opaque JSON payload depth on the Value lane
212
- - content: make a vocabulary promotion carry its own legacy form
213
- - content: simplify pass over the codec-gap changes
214
- - content: house voice on the codec-gap comments
215
- - content: close the three v1.0.0 codec gaps before the freeze
216
- - content: cut the enumerated halves of the two open-set tests
217
- - content: prune tests that assert what a neighbour already pins
218
- - content: fold the authored-lane scan into the shared cell walk
219
- - content: split the wire lanes on reserved-name reuse (#1084, #1085, #1086)
220
- - docs: trim the migration index to a routing table; drop duplicate links
221
- - refactor(typst): re-export span_scan/extract instead of forwarding
222
- -->
223
-
224
-
225
210
  ## v0.98.0 - 2026-07-28
226
211
 
227
212
  Five breaking changes, all covered by `docs/migrations/0.97-to-0.98.md`.
@@ -370,7 +355,7 @@ Stored documents are unaffected: a `0.97` blob loads byte-identically and
370
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)
371
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)
372
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)
373
- - **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)
374
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)
375
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)
376
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/README.md CHANGED
@@ -94,10 +94,37 @@ For an example-filled starter use `quill.seedDocument()`. Throws on an
94
94
  invalid quill reference.
95
95
 
96
96
  ### `Document.fromMarkdown(markdown)`
97
- Parse markdown to a parsed document. Throws a JS `Error` (with `.diagnostics`
98
- attached, see [Errors](#errors)) on any parse failure, including a missing
99
- root `$quill` metadata line, malformed YAML, and inputs over the 10 MiB
100
- `parse::input_too_large` limit.
97
+ Parse markdown to a parsed document, quill-free: the **transport door**
98
+ (migrations, `$ext` stamping, a quill that will not load, opening a document to
99
+ fix its `$quill`). Throws a JS `Error` (with `.diagnostics` attached, see
100
+ [Errors](#errors)) on any parse failure, including a missing root `$quill`
101
+ metadata line, malformed YAML, and inputs over the 10 MiB
102
+ `parse::input_too_large` limit. A content field rests as authored; `quill.parse`
103
+ below is the bound door that lands it at its canonical rest.
104
+
105
+ ### `quill.parse(markdown)` / `quill.conform(doc)`
106
+ The **bound door**, and the primary ingestion path. `quill.parse` is
107
+ `Document.fromMarkdown` followed by `conform`: the returned document's declared
108
+ content fields rest at one form per codec (a `richtext` field as the canonical
109
+ content object, a `plaintext` field as its literal string), so `getStored`
110
+ answers "corpus or string?" by the field's declared codec rather than by how the
111
+ document was built. Parse warnings and the `conform::*` warnings both ride
112
+ `doc.warnings`.
113
+
114
+ `quill.conform(doc)` is the same walk in place on a document that arrived any
115
+ other way (`fromJson`, a stored row), returning the `conform::*` `Diagnostic[]`
116
+ (`[]` when everything rested). It is idempotent and a byte no-op on an
117
+ already-canonical document, YAML comments included, so calling it on every load
118
+ is safe. A `!must_fill` marker anywhere in a field's value skips that field; a
119
+ value the strict write refuses stays as authored under a warning. Both throw
120
+ when the document declares a `$quill` this quill does not answer to, before any
121
+ mutation.
122
+
123
+ ```ts
124
+ const doc = quill.parse(markdown); // rests canonical
125
+ const stale = Document.fromJson(row);
126
+ const diags = quill.conform(stale); // converges in place
127
+ ```
101
128
 
102
129
  ### `doc.toMarkdown()`
103
130
  Emit canonical Quillmark Markdown. Type-fidelity round-trip safe:
@@ -259,7 +286,11 @@ card's. Reads are total over the field axis (`getStored` → `undefined`, `isFil
259
286
  an absent field; only an out-of-range card throws); field writes throw on a body
260
287
  address. `getStored` is the verbatim transport read, distinct from the interpreted
261
288
  `quill.reader(doc).get`; `getMarkdown` is the body markdown read (a `CardAddr`; a field's
262
- markdown is read through `quill.reader(doc).get(field)`). Card-scoped verbs take a
289
+ markdown is read through `quill.reader(doc).get(field)`). A content field's stored
290
+ form follows how the document was built (a canonical content object when the
291
+ typed writer committed it, the authored string when a markdown parse produced
292
+ it), so for the corpus either way read `quill.reader(doc).getContent(addr)`, which
293
+ decodes through the codec the field's declared type names. Card-scoped verbs take a
263
294
  `CardAddr` (`{ card? }`) first: `doc.getExt({ card: 2 })`, and the batch below.
264
295
 
265
296
  Batch mutation: `doc.storeFields({}, {...})` / `doc.storeFields({ card: index }, {...})`
@@ -324,6 +355,24 @@ ed.card(2).set("body", "**note**"); // composable card, resolved
324
355
  is lazy: it never throws; an out-of-range index throws `IndexOutOfRange` at the
325
356
  write.
326
357
 
358
+ #### `DocumentReader` / `CardReader`: the read twin
359
+
360
+ `quill.reader(doc)` carries the writer's ephemerality and its schema authority:
361
+
362
+ ```ts
363
+ const v = quill.reader(doc);
364
+ v.get("subject"); // by declared type: richtext → markdown, plaintext → literal text
365
+ v.getContent("subject"); // the same read as a Content corpus, whichever lane stored it
366
+ v.getBody(); // the main body markdown (quill-free)
367
+ v.card(0).get("body"); // a card field, resolved by its $kind
368
+ ```
369
+
370
+ `get` projects and `getContent` returns the corpus; both decode through the codec
371
+ the field's **declared type** names, which is why they bind the quill and the
372
+ verbatim `doc.getStored` does not. An undeclared name throws `UnknownField`, a
373
+ type that is not a content leaf throws `FieldNotContent`, and an undecodable
374
+ value throws `FieldRichtextDecode`; an absent field reads back `undefined`.
375
+
327
376
  ### `engine.render(quill, parsed, opts?)` vs. `engine.open(quill, parsed)`
328
377
 
329
378
  Use **`engine.render`** for one-shot exports (PDF/SVG/PNG): compiles, emits
@@ -32,14 +32,13 @@ export type PayloadItem =
32
32
  *
33
33
  * `$` system entries are hoisted to named fields: `kind` (the `$kind`, empty
34
34
  * string when none), optional `quill` (the `$quill` `name@version`, main card
35
- * only), optional `id` (`$id`), optional `ext` (`$ext`), and optional `seed`
35
+ * only), optional `ext` (`$ext`), and optional `seed`
36
36
  * (the `$seed` per-kind overlay map, main card only). `payloadItems` carries
37
37
  * user fields and comments in order.
38
38
  */
39
39
  export interface Card {
40
40
  kind: string;
41
41
  quill?: string;
42
- id?: string;
43
42
  ext?: Record<string, unknown>;
44
43
  seed?: Record<string, unknown>;
45
44
  payloadItems: PayloadItem[];
@@ -63,7 +62,6 @@ export interface Card {
63
62
  export interface CardInput {
64
63
  kind: string;
65
64
  quill?: string;
66
- id?: string;
67
65
  ext?: Record<string, unknown>;
68
66
  seed?: Record<string, unknown>;
69
67
  payloadItems?: PayloadItem[];
@@ -593,6 +591,16 @@ export interface Diagnostic {
593
591
  */
594
592
  path?: string;
595
593
  hint?: string;
594
+ /**
595
+ * The facts `message` interpolates, keyed by name. With `code`, the
596
+ * substitution unit needed to word this diagnostic in another language;
597
+ * `prose/canon/ERROR.md` § \"Diagnostic args\" tabulates the keys per code.
598
+ *
599
+ * Declared optional explicitly because `tsify` does not read
600
+ * `skip_serializing_if`: without this, a field the runtime omits is
601
+ * declared required. `sourceChain` carries that mismatch.
602
+ */
603
+ args?: Record<string, unknown>;
596
604
  sourceChain?: string[];
597
605
  }
598
606
 
@@ -739,13 +747,6 @@ export class Document {
739
747
  * verbs.
740
748
  */
741
749
  card(index: number): Card;
742
- /**
743
- * The index of the composable card whose `$id` equals `id`, or
744
- * `undefined` when none carries it. Resolves the durable card handle
745
- * without a hand-rolled scan over [`cards`](Self::cards); `$id` is
746
- * unique per document, so at most one card matches.
747
- */
748
- cardIndexById(id: string): number | undefined;
749
750
  clone(): Document;
750
751
  /**
751
752
  * Schema version this build writes via [`toJson`](Document::to_json).
@@ -813,8 +814,7 @@ export class Document {
813
814
  getMarkdown(addr?: CardAddr): string;
814
815
  /**
815
816
  * Read the **verbatim stored value** at `addr`: the raw payload value of a
816
- * field (a content object for a richtext field, a scalar/array/object
817
- * otherwise), or the **body content** when `addr.field` is absent. A bare
817
+ * field, or the **body content** when `addr.field` is absent. A bare
818
818
  * string is `Addr` shorthand for `{ field }`. Reads are total over the field
819
819
  * axis: an absent field is `undefined`; only an out-of-range `addr.card`
820
820
  * throws `edit::index_out_of_range`. Needs no schema, so it lives on
@@ -822,6 +822,16 @@ export class Document {
822
822
  * the interpreted schema-plane [`reader.get`](Self::reader_get). For the
823
823
  * markdown projection use [`getMarkdown`](Self::get_markdown) (body) or
824
824
  * `reader.get` (a field's declared type).
825
+ *
826
+ * **A content field at rest has one stored form per codec**: a `richtext`
827
+ * field holds the canonical content object, a `plaintext` field its literal
828
+ * string. A document that came through the bound door (`quill.parse` /
829
+ * `quill.conform`) is at rest, so this read no longer depends on which lane
830
+ * built it. A document that came through the transport door
831
+ * (`Document.fromMarkdown`, a legacy stored row) may rest as authored until
832
+ * it is conformed, and this read reports what is there. For the corpus
833
+ * either way, use the schema-plane `reader.getContent`, which decodes
834
+ * through the codec the field's declared type names.
825
835
  */
826
836
  getStored(addr: Addr | string): unknown;
827
837
  /**
@@ -1054,6 +1064,12 @@ export class Document {
1054
1064
  */
1055
1065
  readonly main: Card;
1056
1066
  readonly quillRef: string;
1067
+ /**
1068
+ * The non-fatal diagnostics of the load that produced this document: parse
1069
+ * warnings, plus the `conform::*` warnings when it came through
1070
+ * `quill.parse`. Session state, not document value: `equals` and the
1071
+ * storage DTO exclude it, and `fromJson` / `loadJson` clear it.
1072
+ */
1057
1073
  readonly warnings: Diagnostic[];
1058
1074
  }
1059
1075
 
@@ -1185,6 +1201,23 @@ export class Quill {
1185
1201
  private constructor();
1186
1202
  free(): void;
1187
1203
  [Symbol.dispose](): void;
1204
+ /**
1205
+ * Land `doc`'s declared content fields at their canonical rest **in
1206
+ * place**, returning the `conform::*` diagnostics for the values that would
1207
+ * not commit (an empty array when everything rested).
1208
+ *
1209
+ * The read-repair verb: a document that arrived through the transport door
1210
+ * (`fromMarkdown`, `fromJson`, a stored row) converges here, and is then
1211
+ * eligible for rewrite under its current schema tag. Idempotent, and a
1212
+ * no-op on an already-canonical document: an equal value is not rewritten,
1213
+ * so YAML comments and stored bytes survive.
1214
+ *
1215
+ * A `!must_fill` marker anywhere in a field's value skips that field (the
1216
+ * marker is the state), and a value the strict write refuses stays as
1217
+ * authored with a diagnostic. Throws when `doc` declares a different
1218
+ * `$quill`, before any mutation.
1219
+ */
1220
+ conform(doc: Document): Diagnostic[];
1188
1221
  /**
1189
1222
  * Build a quill from a file tree. Pure: no backend, no engine; the
1190
1223
  * declared backend is resolved later, at render time.
@@ -1195,6 +1228,21 @@ export class Quill {
1195
1228
  * canonical shape.
1196
1229
  */
1197
1230
  static fromTree(tree: Map<string, Uint8Array>): Quill;
1231
+ /**
1232
+ * Parse `markdown` and conform it against this quill: the **primary
1233
+ * ingestion path**, and the bound twin of the schema-free
1234
+ * `Document.fromMarkdown`. The returned document rests at its canonical
1235
+ * form (a `richtext` field as a content object, a `plaintext` field as its
1236
+ * literal string), so `getStored` no longer answers "corpus or string?"
1237
+ * with "depends how this document was built".
1238
+ *
1239
+ * Parse warnings and the `conform::*` diagnostics both land on
1240
+ * `doc.warnings`. Throws on a parse failure, or when `markdown` declares a
1241
+ * `$quill` this quill does not answer to: nothing conforms under the wrong
1242
+ * schema. To open a document whose `$quill` is stale, use the transport
1243
+ * door (`Document.fromMarkdown`, `setQuillRef`, then `quill.conform`).
1244
+ */
1245
+ parse(markdown: string): Document;
1198
1246
  /**
1199
1247
  * The resolved-value view of `doc` against this quill's schema: for every
1200
1248
  * declared field the value the render projection would use and the
@@ -169,7 +169,48 @@ export class Document {
169
169
  }
170
170
  }
171
171
  /**
172
- * Revise the richtext field at `addr` from markdown, typed *and*
172
+ * Interpreted **corpus** read at `addr`: the stable ABI under the runtime
173
+ * `reader.getContent` / `reader.card(i).getContent`. The corpus twin of
174
+ * [`reader.get`](Self::reader_get), which projects; this decodes the stored
175
+ * value through the codec the field's declared type names (`richtext` as
176
+ * markdown, `plaintext` as literal text) and returns the canonical `Content`.
177
+ *
178
+ * Total over the storage form: a committed field holds a content object and
179
+ * a parsed one holds the authored string, and both read back as a corpus
180
+ * here, so a consumer mounting a corpus editor stops branching on how the
181
+ * document was built.
182
+ *
183
+ * A bare string is `Addr` shorthand for `{ field }`; `{ card, field }`
184
+ * targets a composable card. Returns `undefined` for an **absent** field. An
185
+ * absent `addr.field` reads the **body** corpus, quill-free, mirroring
186
+ * [`getStored`](Self::get_stored). Throws `edit::unknown_field` for a name
187
+ * the schema does not declare, `edit::field_not_content` for a declared type
188
+ * that is not a content leaf (`array<richtext>` carries content and still has
189
+ * no one corpus), `edit::field_richtext_decode` for a stored value
190
+ * that decodes under neither encoding, and `edit::index_out_of_range` for a
191
+ * bad `addr.card`.
192
+ * @param {Quill} quill
193
+ * @param {Addr | string} addr
194
+ * @returns {Content | undefined}
195
+ */
196
+ _readerGetContent(quill, addr) {
197
+ try {
198
+ const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
199
+ _assertClass(quill, Quill);
200
+ wasm.document__readerGetContent(retptr, this.__wbg_ptr, quill.__wbg_ptr, addHeapObject(addr));
201
+ var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
202
+ var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
203
+ var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true);
204
+ if (r2) {
205
+ throw takeObject(r1);
206
+ }
207
+ return takeObject(r0);
208
+ } finally {
209
+ wasm.__wbindgen_add_to_stack_pointer(16);
210
+ }
211
+ }
212
+ /**
213
+ * Revise the content field at `addr` from authored text, typed *and*
173
214
  * anchor-preserving: the ABI under `writer.reviseField`. Resolves the
174
215
  * field's schema from `quill` (main card, or the addressed card's `$kind`)
175
216
  * and defers to [`TypedWriter::revise_field`](quillmark_core::TypedWriter::revise_field):
@@ -178,6 +219,9 @@ export class Document {
178
219
  * multi-block result with `edit::field_richtext_not_inline`. Returns the
179
220
  * text `Delta`.
180
221
  *
222
+ * The codec is the declared type's: `richtext` diffs markdown, `plaintext`
223
+ * the literal text.
224
+ *
181
225
  * `addr` must name a field (a bare string is `{ field }`); a body address
182
226
  * throws (a body carries no field schema: use [`revise`](Self::revise)). A
183
227
  * name the schema does not declare throws `edit::unknown_field`. Throws
@@ -185,14 +229,14 @@ export class Document {
185
229
  * `writer.reviseField` in the runtime layer.
186
230
  * @param {Quill} quill
187
231
  * @param {Addr | string} addr
188
- * @param {string} markdown
232
+ * @param {string} text
189
233
  * @returns {Delta}
190
234
  */
191
- _reviseField(quill, addr, markdown) {
235
+ _reviseField(quill, addr, text) {
192
236
  try {
193
237
  const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
194
238
  _assertClass(quill, Quill);
195
- const ptr0 = passStringToWasm0(markdown, wasm.__wbindgen_export, wasm.__wbindgen_export2);
239
+ const ptr0 = passStringToWasm0(text, wasm.__wbindgen_export, wasm.__wbindgen_export2);
196
240
  const len0 = WASM_VECTOR_LEN;
197
241
  wasm.document__reviseField(retptr, this.__wbg_ptr, quill.__wbg_ptr, addHeapObject(addr), ptr0, len0);
198
242
  var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
@@ -289,20 +333,6 @@ export class Document {
289
333
  const ret = wasm.document_cardCount(this.__wbg_ptr);
290
334
  return ret >>> 0;
291
335
  }
292
- /**
293
- * The index of the composable card whose `$id` equals `id`, or
294
- * `undefined` when none carries it. Resolves the durable card handle
295
- * without a hand-rolled scan over [`cards`](Self::cards); `$id` is
296
- * unique per document, so at most one card matches.
297
- * @param {string} id
298
- * @returns {number | undefined}
299
- */
300
- cardIndexById(id) {
301
- const ptr0 = passStringToWasm0(id, wasm.__wbindgen_export, wasm.__wbindgen_export2);
302
- const len0 = WASM_VECTOR_LEN;
303
- const ret = wasm.document_cardIndexById(this.__wbg_ptr, ptr0, len0);
304
- return takeObject(ret);
305
- }
306
336
  /**
307
337
  * @returns {Card[]}
308
338
  */
@@ -536,8 +566,7 @@ export class Document {
536
566
  }
537
567
  /**
538
568
  * Read the **verbatim stored value** at `addr`: the raw payload value of a
539
- * field (a content object for a richtext field, a scalar/array/object
540
- * otherwise), or the **body content** when `addr.field` is absent. A bare
569
+ * field, or the **body content** when `addr.field` is absent. A bare
541
570
  * string is `Addr` shorthand for `{ field }`. Reads are total over the field
542
571
  * axis: an absent field is `undefined`; only an out-of-range `addr.card`
543
572
  * throws `edit::index_out_of_range`. Needs no schema, so it lives on
@@ -545,6 +574,16 @@ export class Document {
545
574
  * the interpreted schema-plane [`reader.get`](Self::reader_get). For the
546
575
  * markdown projection use [`getMarkdown`](Self::get_markdown) (body) or
547
576
  * `reader.get` (a field's declared type).
577
+ *
578
+ * **A content field at rest has one stored form per codec**: a `richtext`
579
+ * field holds the canonical content object, a `plaintext` field its literal
580
+ * string. A document that came through the bound door (`quill.parse` /
581
+ * `quill.conform`) is at rest, so this read no longer depends on which lane
582
+ * built it. A document that came through the transport door
583
+ * (`Document.fromMarkdown`, a legacy stored row) may rest as authored until
584
+ * it is conformed, and this read reports what is there. For the corpus
585
+ * either way, use the schema-plane `reader.getContent`, which decodes
586
+ * through the codec the field's declared type names.
548
587
  * @param {Addr | string} addr
549
588
  * @returns {unknown}
550
589
  */
@@ -1254,6 +1293,10 @@ export class Document {
1254
1293
  return ret === 0 ? undefined : Document.__wrap(ret);
1255
1294
  }
1256
1295
  /**
1296
+ * The non-fatal diagnostics of the load that produced this document: parse
1297
+ * warnings, plus the `conform::*` warnings when it came through
1298
+ * `quill.parse`. Session state, not document value: `equals` and the
1299
+ * storage DTO exclude it, and `fromJson` / `loadJson` clear it.
1257
1300
  * @returns {Diagnostic[]}
1258
1301
  */
1259
1302
  get warnings() {
@@ -1649,6 +1692,40 @@ export class Quill {
1649
1692
  wasm.__wbindgen_export4(deferred1_0, deferred1_1, 1);
1650
1693
  }
1651
1694
  }
1695
+ /**
1696
+ * Land `doc`'s declared content fields at their canonical rest **in
1697
+ * place**, returning the `conform::*` diagnostics for the values that would
1698
+ * not commit (an empty array when everything rested).
1699
+ *
1700
+ * The read-repair verb: a document that arrived through the transport door
1701
+ * (`fromMarkdown`, `fromJson`, a stored row) converges here, and is then
1702
+ * eligible for rewrite under its current schema tag. Idempotent, and a
1703
+ * no-op on an already-canonical document: an equal value is not rewritten,
1704
+ * so YAML comments and stored bytes survive.
1705
+ *
1706
+ * A `!must_fill` marker anywhere in a field's value skips that field (the
1707
+ * marker is the state), and a value the strict write refuses stays as
1708
+ * authored with a diagnostic. Throws when `doc` declares a different
1709
+ * `$quill`, before any mutation.
1710
+ * @param {Document} doc
1711
+ * @returns {Diagnostic[]}
1712
+ */
1713
+ conform(doc) {
1714
+ try {
1715
+ const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
1716
+ _assertClass(doc, Document);
1717
+ wasm.quill_conform(retptr, this.__wbg_ptr, doc.__wbg_ptr);
1718
+ var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
1719
+ var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
1720
+ var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true);
1721
+ if (r2) {
1722
+ throw takeObject(r1);
1723
+ }
1724
+ return takeObject(r0);
1725
+ } finally {
1726
+ wasm.__wbindgen_add_to_stack_pointer(16);
1727
+ }
1728
+ }
1652
1729
  /**
1653
1730
  * Build a quill from a file tree. Pure: no backend, no engine; the
1654
1731
  * declared backend is resolved later, at render time.
@@ -1697,6 +1774,39 @@ export class Quill {
1697
1774
  wasm.__wbindgen_add_to_stack_pointer(16);
1698
1775
  }
1699
1776
  }
1777
+ /**
1778
+ * Parse `markdown` and conform it against this quill: the **primary
1779
+ * ingestion path**, and the bound twin of the schema-free
1780
+ * `Document.fromMarkdown`. The returned document rests at its canonical
1781
+ * form (a `richtext` field as a content object, a `plaintext` field as its
1782
+ * literal string), so `getStored` no longer answers "corpus or string?"
1783
+ * with "depends how this document was built".
1784
+ *
1785
+ * Parse warnings and the `conform::*` diagnostics both land on
1786
+ * `doc.warnings`. Throws on a parse failure, or when `markdown` declares a
1787
+ * `$quill` this quill does not answer to: nothing conforms under the wrong
1788
+ * schema. To open a document whose `$quill` is stale, use the transport
1789
+ * door (`Document.fromMarkdown`, `setQuillRef`, then `quill.conform`).
1790
+ * @param {string} markdown
1791
+ * @returns {Document}
1792
+ */
1793
+ parse(markdown) {
1794
+ try {
1795
+ const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
1796
+ const ptr0 = passStringToWasm0(markdown, wasm.__wbindgen_export, wasm.__wbindgen_export2);
1797
+ const len0 = WASM_VECTOR_LEN;
1798
+ wasm.quill_parse(retptr, this.__wbg_ptr, ptr0, len0);
1799
+ var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
1800
+ var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
1801
+ var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true);
1802
+ if (r2) {
1803
+ throw takeObject(r1);
1804
+ }
1805
+ return Document.__wrap(r0);
1806
+ } finally {
1807
+ wasm.__wbindgen_add_to_stack_pointer(16);
1808
+ }
1809
+ }
1700
1810
  /**
1701
1811
  * The resolved-value view of `doc` against this quill's schema: for every
1702
1812
  * declared field the value the render projection would use and the
Binary file