@quillmark/wasm 0.99.0 → 0.101.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,112 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.101.0 - 2026-08-03
4
+
5
+ - refactor(core): gate the raw-plate seam behind a feature, and fixes from review
6
+ - docs(core): state the conform gate's codes instead of linking a private item
7
+ - refactor(core): bind a live session to its quill so apply takes a Document
8
+ - feat(quillmark): the facade names what the read and preview flows return
9
+ - docs(content,wasm): density pass over the island channel's prose
10
+ - feat(content,core,wasm)!: reach islands through the op vocabulary
11
+ - refactor(core)!: `Payload` becomes a read view
12
+ - refactor(core)!: collapse the schema-free field projection
13
+ - refactor(core)!: fold `Quill`'s file queries into `FileTreeNode`
14
+ - fix(core): repoint the doc references the `from_yaml` removal orphaned
15
+ - refactor(quillmark): move the facade gate off the front page, and stop tests reaching past it
16
+ - docs: migration guide for the 0.101 surface removals
17
+ - refactor(core)!: drop the lossy `QuillConfig::from_yaml`
18
+ - refactor(core)!: `Document::from_main_and_cards` becomes crate-internal
19
+ - feat(quillmark): the facade covers authoring, and the examples enter through the bound door
20
+
21
+
22
+ ## v0.100.0 - 2026-08-03
23
+
24
+ A content field gets one resting form, and the last reserved `$` key with no
25
+ reader is removed. All breaking changes are covered by
26
+ `docs/migrations/0.99-to-0.100.md`. Stored documents load unchanged, but a row
27
+ read through the bound door converges once: read-repair, not a schema-version
28
+ event. One ordering matters, and the guide's "Legacy data" section states it —
29
+ conform a stored population before exporting markdown from it.
30
+
31
+ - refactor(core,wasm,python)!: content fields have one resting form, enforced at
32
+ load. `Quill::conform(&mut doc)` is the primitive and `Quill::parse(md)`
33
+ (parse, then conform) the convenience — the documented primary ingestion path,
34
+ `quill.parse` / `quill.conform` on both bindings. A `richtext` field rests as
35
+ the canonical content object, a `plaintext` field as its **literal string**, so
36
+ the stored shape is a property of the codec instead of the construction lane:
37
+ `equals` and content hashes stop separating semantically identical documents.
38
+ The typed writer commits `plaintext` as a string, and `revise_field` diffs it
39
+ through the literal codec (a byte-identical revise of `a \*b\*` used to commit
40
+ `a *b*`). `Document::parse` / `Document.fromMarkdown` stay exactly as they
41
+ were, demoted to the transport/repair door. Conform is idempotent, a byte no-op
42
+ on an already-canonical document, and reports a `conform::*` warning where the
43
+ strict write refuses rather than retyping or rejecting; a `$quill` naming
44
+ another quill errors before any mutation (#1160, #1162). See
45
+ `docs/migrations/0.99-to-0.100.md`
46
+ - fix(core)!: markdown exported from a `plaintext` field resting as a content
47
+ object is markdown-escaped. Emit is schema-free and cannot tell a `plaintext`
48
+ content from a `richtext` one, so `a *literal* line` leaves as
49
+ `a \*literal\* line` and re-parses with the backslashes as characters — one
50
+ more layer per save cycle. Only the typed writer produced that rest, and the
51
+ string rest above deletes it rather than managing it: load, conform, and
52
+ re-store a population before exporting markdown from it. Markdown already
53
+ exported under ≤0.99 is corrupt at rest, its escapes indistinguishable from
54
+ authored ones, so re-export it from the conformed rows (#1159). See
55
+ `docs/migrations/0.99-to-0.100.md`
56
+ - fix(core,wasm,python)!: a `plaintext` field resting as a string reads through
57
+ the **literal** codec, not markdown — `note: 'a *literal* line'` read back as
58
+ `a literal line` while render and validation kept the asterisks. Only the
59
+ string lane was wrong; the committed-object lane always decoded correctly, so
60
+ a consumer that pre-escaped a `plaintext` field to survive the read drops the
61
+ escaping. Alongside it, `reader.get_content` / `reader.getContent` returns a
62
+ content field's `Content` corpus whichever lane stored it, so a consumer
63
+ holding a corpus editor stops branching on the wire shape. `EditError` gains
64
+ `FieldNotContent` (`edit::field_not_content`) for a declared type that is not a
65
+ content leaf; core adds `Card::field_plaintext_content` (#1154). See
66
+ `docs/migrations/0.99-to-0.100.md`
67
+ - refactor(core,wasm,python)!: card `$id` is removed — the reserved key, its
68
+ resolver (`Document::find_card` / `doc.cardIndexById` / `doc.card_index_by_id`),
69
+ the uniqueness contract (`EditError::CardIdCollision` / `EmptyCardId`, the
70
+ `parse::card_id_*` warnings, the storage rejection), `Card::id` /
71
+ `Payload::{id, set_id, take_id}` / `Document::{set_card_id, remove_card_id}`,
72
+ the `PayloadItem::Id` and `CardWire.id` wire members, and the projected `id` on
73
+ both bindings' card shape. Nothing in the engine read it and it never reached a
74
+ backend, so what is left after removing the machinery that served the resolver
75
+ is `$ext` with a reserved name. A block declaring `$id` no longer parses and a
76
+ blob carrying an `id` payload item no longer loads: a hard cutover, no
77
+ tolerate-and-ignore window. Per-card consumer keys move to `$ext` under a
78
+ namespace you own, with no uniqueness, no collision check, and no repair
79
+ (#1151). See `docs/migrations/0.99-to-0.100.md`
80
+ - refactor(content)!: `Content`, `Line`, `Mark`, and `Island` take
81
+ `#[non_exhaustive]` — the four public structs the 0.99 sweep missed, that pass
82
+ having run as two issues split by crate. Their literals give way to `new` plus
83
+ the `with_*` setters on the same terms as the rest of the API; every field stays
84
+ `pub`, so reading and assigning are unchanged. `Delta`, `Segment`, and
85
+ `BaseLengthMismatch` stay open deliberately and now say so in their rustdoc.
86
+ A Rust source break only: nothing about the wire, the canonical bytes, or the
87
+ bindings moves (#1146). See `docs/migrations/0.99-to-0.100.md`
88
+ - feat(core,wasm,python): `Diagnostic.args` — the facts `message` interpolates,
89
+ keyed by name, so a consumer with its own string table selects a sentence by
90
+ `code` and fills it itself. Values keep their JSON shape (a list arrives as a
91
+ list, a count as a number), engine prose never rides under a key, and a
92
+ formatter missing a key falls back to `message` wholesale. `prose/canon/ERROR.md`
93
+ § "Diagnostic args" tabulates the keys per code and a test fails when code and
94
+ canon disagree (#1130)
95
+ - fix(core): the `$quill` mismatch message and hint name the pairing rather than
96
+ the verb. `check_quill_reference` gates every schema-bound door now, not the
97
+ render path alone, so a `quill.parse` failure no longer reads "was rendered
98
+ with". The codes (`quill::name_mismatch` / `quill::version_mismatch`) are
99
+ unchanged
100
+ - test(fuzz): the resting-form invariant gains a target, stated as three
101
+ properties — conform is a fixed point, parse-then-conform equals typed-write
102
+ per content field, and a document through the markdown surface and back settles
103
+ after one pass (exactly, for `plaintext`, whose codec is lossless both ways)
104
+ - docs: the cycle's stale pages are repaired. Both binding READMEs gain the bound
105
+ door and the corpus read, `revise_field` is documented per declared type on all
106
+ four surfaces instead of as a markdown-only richtext verb, and four canon claims
107
+ that outran the tree are corrected
108
+
109
+
3
110
  ## v0.99.0 - 2026-08-01
4
111
 
5
112
  The 1.0.0 API freeze lands ahead of the tag, and the content codec closes its
@@ -119,109 +226,6 @@ loads byte-identically and `0.99` writes the same bytes for the same content.
119
226
  `serde_saphyr` types stay out of `quillmark-core`'s public API (see the YAML
120
227
  engine entry above), so nothing downstream moves
121
228
 
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
229
  ## v0.98.0 - 2026-07-28
226
230
 
227
231
  Five breaking changes, all covered by `docs/migrations/0.97-to-0.98.md`.
@@ -370,7 +374,7 @@ Stored documents are unaffected: a `0.97` blob loads byte-identically and
370
374
  - **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
375
  - **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
376
  - **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)
377
+ - **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
378
  - **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
379
  - **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
380
  - 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[];
@@ -242,13 +240,42 @@ export type LineOp =
242
240
  | { op: "setContainers"; line: number; containers: ContentContainer[] }
243
241
  | { op: "setContinues"; line: number; continues: boolean };
244
242
 
243
+ /**
244
+ * An island edit: the only channel that reaches an island's payload, a table's
245
+ * cells or an image's url.
246
+ *
247
+ * Both ops move one island entry and leave the field's text and marks alone, so
248
+ * an island edit keeps every identity anchor in the field. That is why a table
249
+ * edit lowers to `applyChange` rather than `install`, which drops them all.
250
+ *
251
+ * `set` addresses an existing island by `id`; an `id` no island carries throws
252
+ * rather than passing silently. `insert` places a new island's slot at `at` (a
253
+ * post-delta USV position) together with its entry, so a slot never exists
254
+ * without an island behind it; its `id` must be non-empty and unused. Deleting
255
+ * an island needs no op: a `delta` that removes its slot drops the island.
256
+ *
257
+ * A `set` stores the `loss` it is given: nothing re-derives the class from the
258
+ * new `props`, so a write that changes what markdown can carry must say so.
259
+ *
260
+ * An island is *inline* (a slot inside a paragraph) unless its line says
261
+ * otherwise. A **block** island is one bundle of all three channels, in the
262
+ * order they apply: `delta` inserts the `\n` that opens the line, `islandOps`
263
+ * inserts the slot, `lineOps` tags the line `{ op: "setKind", kind: "island" }`.
264
+ * `{ op: "split" }` cannot open that line, since line ops run after island ops.
265
+ */
266
+ export type IslandOp =
267
+ | ({ op: "set" } & ContentIsland)
268
+ | ({ op: "insert"; at: number } & ContentIsland);
269
+
245
270
  /**
246
271
  * A committed content edit bundle for `applyChange`: a text `delta` (default no
247
- * text change), then `lineOps`, then `markOps` (mark ranges are in post-delta
248
- * coordinates). Every field is optional.
272
+ * text change), then `islandOps`, then `lineOps`, then `markOps` (mark ranges
273
+ * are in final-text coordinates: every earlier channel applied). Every field is
274
+ * optional.
249
275
  */
250
276
  export interface ChangeBundle {
251
277
  delta?: Delta;
278
+ islandOps?: IslandOp[];
252
279
  lineOps?: LineOp[];
253
280
  markOps?: MarkOp[];
254
281
  }
@@ -593,6 +620,16 @@ export interface Diagnostic {
593
620
  */
594
621
  path?: string;
595
622
  hint?: string;
623
+ /**
624
+ * The facts `message` interpolates, keyed by name. With `code`, the
625
+ * substitution unit needed to word this diagnostic in another language;
626
+ * `prose/canon/ERROR.md` § \"Diagnostic args\" tabulates the keys per code.
627
+ *
628
+ * Declared optional explicitly because `tsify` does not read
629
+ * `skip_serializing_if`: without this, a field the runtime omits is
630
+ * declared required. `sourceChain` carries that mismatch.
631
+ */
632
+ args?: Record<string, unknown>;
596
633
  sourceChain?: string[];
597
634
  }
598
635
 
@@ -714,11 +751,16 @@ export class Document {
714
751
  free(): void;
715
752
  [Symbol.dispose](): void;
716
753
  /**
717
- * **Apply** a committed content edit `bundle` (`{ delta?, lineOps?, markOps? }`)
718
- * at `addr`, the editor splice: text delta first, then line ops, then mark
719
- * ops (mark ranges in final-text coordinates), each all-or-nothing. An absent
754
+ * **Apply** a committed content edit `bundle`
755
+ * (`{ delta?, islandOps?, lineOps?, markOps? }`) at `addr`, the editor
756
+ * splice: text delta first, then island ops, then line ops, then mark ops
757
+ * (mark ranges in final-text coordinates), each all-or-nothing. An absent
720
758
  * `addr.field` targets the body, an absent `addr.card` the main card.
721
759
  *
760
+ * The island channel keeps a table or image edit on the op path: it moves
761
+ * the island alone, so the anchors elsewhere in the field survive an edit
762
+ * `install` would clear.
763
+ *
722
764
  * Throws on an out-of-range card, a field that is not richtext, a malformed
723
765
  * bundle, or an op that applies out of bounds (the value is unchanged on a
724
766
  * failed apply).
@@ -739,13 +781,6 @@ export class Document {
739
781
  * verbs.
740
782
  */
741
783
  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
784
  clone(): Document;
750
785
  /**
751
786
  * Schema version this build writes via [`toJson`](Document::to_json).
@@ -813,8 +848,7 @@ export class Document {
813
848
  getMarkdown(addr?: CardAddr): string;
814
849
  /**
815
850
  * 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
851
+ * field, or the **body content** when `addr.field` is absent. A bare
818
852
  * string is `Addr` shorthand for `{ field }`. Reads are total over the field
819
853
  * axis: an absent field is `undefined`; only an out-of-range `addr.card`
820
854
  * throws `edit::index_out_of_range`. Needs no schema, so it lives on
@@ -822,6 +856,16 @@ export class Document {
822
856
  * the interpreted schema-plane [`reader.get`](Self::reader_get). For the
823
857
  * markdown projection use [`getMarkdown`](Self::get_markdown) (body) or
824
858
  * `reader.get` (a field's declared type).
859
+ *
860
+ * **A content field at rest has one stored form per codec**: a `richtext`
861
+ * field holds the canonical content object, a `plaintext` field its literal
862
+ * string. A document that came through the bound door (`quill.parse` /
863
+ * `quill.conform`) is at rest, so this read no longer depends on which lane
864
+ * built it. A document that came through the transport door
865
+ * (`Document.fromMarkdown`, a legacy stored row) may rest as authored until
866
+ * it is conformed, and this read reports what is there. For the corpus
867
+ * either way, use the schema-plane `reader.getContent`, which decodes
868
+ * through the codec the field's declared type names.
825
869
  */
826
870
  getStored(addr: Addr | string): unknown;
827
871
  /**
@@ -1054,6 +1098,12 @@ export class Document {
1054
1098
  */
1055
1099
  readonly main: Card;
1056
1100
  readonly quillRef: string;
1101
+ /**
1102
+ * The non-fatal diagnostics of the load that produced this document: parse
1103
+ * warnings, plus the `conform::*` warnings when it came through
1104
+ * `quill.parse`. Session state, not document value: `equals` and the
1105
+ * storage DTO exclude it, and `fromJson` / `loadJson` clear it.
1106
+ */
1057
1107
  readonly warnings: Diagnostic[];
1058
1108
  }
1059
1109
 
@@ -1185,6 +1235,23 @@ export class Quill {
1185
1235
  private constructor();
1186
1236
  free(): void;
1187
1237
  [Symbol.dispose](): void;
1238
+ /**
1239
+ * Land `doc`'s declared content fields at their canonical rest **in
1240
+ * place**, returning the `conform::*` diagnostics for the values that would
1241
+ * not commit (an empty array when everything rested).
1242
+ *
1243
+ * The read-repair verb: a document that arrived through the transport door
1244
+ * (`fromMarkdown`, `fromJson`, a stored row) converges here, and is then
1245
+ * eligible for rewrite under its current schema tag. Idempotent, and a
1246
+ * no-op on an already-canonical document: an equal value is not rewritten,
1247
+ * so YAML comments and stored bytes survive.
1248
+ *
1249
+ * A `!must_fill` marker anywhere in a field's value skips that field (the
1250
+ * marker is the state), and a value the strict write refuses stays as
1251
+ * authored with a diagnostic. Throws when `doc` declares a different
1252
+ * `$quill`, before any mutation.
1253
+ */
1254
+ conform(doc: Document): Diagnostic[];
1188
1255
  /**
1189
1256
  * Build a quill from a file tree. Pure: no backend, no engine; the
1190
1257
  * declared backend is resolved later, at render time.
@@ -1195,6 +1262,21 @@ export class Quill {
1195
1262
  * canonical shape.
1196
1263
  */
1197
1264
  static fromTree(tree: Map<string, Uint8Array>): Quill;
1265
+ /**
1266
+ * Parse `markdown` and conform it against this quill: the **primary
1267
+ * ingestion path**, and the bound twin of the schema-free
1268
+ * `Document.fromMarkdown`. The returned document rests at its canonical
1269
+ * form (a `richtext` field as a content object, a `plaintext` field as its
1270
+ * literal string), so `getStored` no longer answers "corpus or string?"
1271
+ * with "depends how this document was built".
1272
+ *
1273
+ * Parse warnings and the `conform::*` diagnostics both land on
1274
+ * `doc.warnings`. Throws on a parse failure, or when `markdown` declares a
1275
+ * `$quill` this quill does not answer to: nothing conforms under the wrong
1276
+ * schema. To open a document whose `$quill` is stale, use the transport
1277
+ * door (`Document.fromMarkdown`, `setQuillRef`, then `quill.conform`).
1278
+ */
1279
+ parse(markdown: string): Document;
1198
1280
  /**
1199
1281
  * The resolved-value view of `doc` against this quill's schema: for every
1200
1282
  * declared field the value the render projection would use and the