@quillmark/wasm 0.88.0 → 0.90.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,132 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.90.0 - 2026-06-10
4
+
5
+
6
+
7
+ ## v0.89.1 - 2026-06-10
8
+
9
+ - chore(release): v0.89.1-rc.1 (#714)
10
+ - feat(wasm)!: 0.90 canonical API — engine-free Quill, single root export, typed errors; Python parity (#713)
11
+ - Proposal: WASM bindings split (core + render) via backend-decoupled Quill (#710)
12
+ - Add version selector matching and mismatch warnings (#708)
13
+ - docs: density-optimization pass on user-facing docs (#703)
14
+ - Remove role annotation from root block metadata header (#707)
15
+ - canon: audit and correct all prose/canon/ docs (#704)
16
+ - Fix makeCard fields/body typed as required in WASM .d.ts (#702)
17
+ - Update CLAUDE.md
18
+
19
+
20
+ ## v0.89.1-rc.1 - 2026-06-10
21
+
22
+ - feat(wasm)!: 0.90 canonical API — engine-free Quill, single root export, typed errors; Python parity (#713)
23
+ - Proposal: WASM bindings split (core + render) via backend-decoupled Quill (#710)
24
+ - Add version selector matching and mismatch warnings (#708)
25
+ - docs: density-optimization pass on user-facing docs (#703)
26
+ - Remove role annotation from root block metadata header (#707)
27
+ - canon: audit and correct all prose/canon/ docs (#704)
28
+ - Fix makeCard fields/body typed as required in WASM .d.ts (#702)
29
+ - Update CLAUDE.md
30
+
31
+
32
+ ## Unreleased
33
+
34
+ - **Breaking (Rust API + bindings):** `Quill` is now engine-free, validated
35
+ data. It no longer holds a backend; the `Quillmark` engine becomes a backend
36
+ registry + render dispatcher. Rendering and capability move onto the engine:
37
+ `render` / `open` / `supported_formats` / `supports_canvas` take `&quill`
38
+ (JS: `engine.render(quill, doc)` etc.). The `engine.quill` / `quill_from_path`
39
+ factory is removed — construct with `Quill::from_tree` (JS `Quill.fromTree`)
40
+ or `quillmark::quill_from_path`. The backend-existence
41
+ check moves from load time to render time (`UnsupportedBackend` now surfaces
42
+ from the first engine call). `supportedFormats` leaves `Quill.metadata` (now
43
+ pure config) for `engine.supportedFormats(quill)`. `Backend` gains a
44
+ `supports_canvas()` capability method (default `false`; Typst `true`),
45
+ retiring the `backend_id == "typst"` magic string. See
46
+ [migration guide](docs/migrations/0.89-to-0.90.md).
47
+ - **Breaking (WASM/JS types):** `QuillMetadata` drops its `[key: string]: unknown`
48
+ index signature. Code reading removed or unknown metadata properties (e.g.
49
+ `quill.metadata.supportedFormats`) now fails at compile time with "Property
50
+ does not exist" instead of silently returning `undefined` at runtime. Cast to
51
+ `Record<string, unknown>` to reach extra `quill:` YAML keys if needed.
52
+ - **Breaking (Python API):** the Python binding adopts the engine-free shape.
53
+ Render and capability move onto the `Quillmark` engine, taking a quill:
54
+ `engine.render(quill, doc)` / `engine.open(quill, doc)` /
55
+ `engine.supported_formats(quill)` / `engine.supports_canvas(quill)` (were
56
+ `quill.render(doc)` etc.). `Quill.from_path(path)` replaces
57
+ `Quillmark.quill_from_path(path)` — the engine is no longer a loader, and the
58
+ loaded `Quill` is engine-free. `quill.metadata` no longer contains
59
+ `supportedFormats` (read `engine.supported_formats(quill)`) and is now a pure,
60
+ infallible config read. Backend resolution moves from load to render time:
61
+ `UnsupportedBackend` surfaces from the first engine call, not from `from_path`.
62
+ See the [migration guide](docs/migrations/0.89-to-0.90.md#python).
63
+ - **Breaking (Rust API):** `QuillSource` and the orchestration `Quill` collapse
64
+ into one core type, `quillmark_core::Quill` (held by value; the vestigial
65
+ `Arc` is dropped). `Backend::open` now takes `&Quill`; the consumer methods
66
+ and the `seed` module move into core; `quill.source()` is gone
67
+ (`quill.config()` is direct). Bindings already hid `QuillSource`, so JS/Python
68
+ consumers are unaffected by the rename.
69
+ - **WASM packaging (single root export):** the root `@quillmark/wasm` import is
70
+ now a hand-written **canonical layer** (`pkg/runtime/`) — it re-exports the
71
+ Typst-less core's `Quill` / `Document` **verbatim** (same classes, no wrappers)
72
+ and adds an async **`Engine`** (`render` / `open` / `supportedFormats` /
73
+ `supportsCanvas`) as the canonical render API. The package `exports` map has
74
+ exactly **one** public entry point, `.` (the canonical layer); the old
75
+ `./render` and `./core` subpath exports are both **removed**. Engine-free
76
+ editor/validation code (`Quill.fromTree`, `Document.fromMarkdown`) still loads
77
+ only the small internal core binary (~0.66 MB gzip) — no backend is loaded
78
+ until you render. The Typst backend binary is **private**
79
+ (`pkg/backends/typst/`, not in the `exports` map): the `Engine`
80
+ lazy-`import()`s it on first render, clones the quill/document into its memory as
81
+ data (`Quill.toTree` → `fromTree`, `doc.toJson` → `fromJson`), and manages
82
+ those clones internally (the validated quill clone is cached per instance;
83
+ per-render document clones are freed) — consumers never import the backend or
84
+ cross a WASM memory boundary themselves. `Quill.toTree()` is added to core for that crossing. A release-time
85
+ size budget still guards the core artifact against Typst regressions.
86
+ - **WASM `Engine` (descriptor-only backend registry):** `new Engine({ backends })`
87
+ takes backend entries in **descriptor form only** — `{ load, formats, canvas }`
88
+ with `formats` and `canvas` **required**. The constructor validates each entry
89
+ and throws (naming the backend id) at construction. The capability probes
90
+ `supportedFormats` / `supportsCanvas` answer from this required manifest
91
+ **unconditionally**, never loading a backend binary or cloning the quill. The
92
+ bare-thunk loader form and its load+clone fallback path are removed.
93
+ - **WASM `Engine` (no invalidation API):** the unreleased
94
+ `Engine.invalidate(quill)` / `invalidateAll()` methods are removed before
95
+ release. The backend-clone cache is keyed on the canonical `Quill` instance in
96
+ a `WeakMap`; a quill's contents never change after construction, so the only
97
+ invalidation semantic is to drop/replace the instance (the clone is freed with
98
+ it via the `WeakMap` + wasm-bindgen weak-refs). An explicit invalidation API
99
+ will ship with its first real consumer. The load-bearing invariant — a
100
+ canonical ref is immutable content within a runtime's lifespan — is now
101
+ recorded in `prose/canon/VERSIONING.md` (Ref Immutability).
102
+ - **WASM `Engine` (session/canvas surface marked experimental):** `Engine.open`,
103
+ `RenderSession`, `paint`, `PaintOptions`, `PaintResult`, `PageSize`, and the
104
+ `supportsCanvas` probe are tagged `@experimental` in the shipped types and
105
+ README: they ship ahead of their first production consumer (the designed
106
+ canvas live-preview path) and may change shape in any 0.x release.
107
+ `Engine.render` and `supportedFormats` are the stable surface.
108
+ - **WASM (typed error contract):** the root exports `QuillmarkError` — a
109
+ structural interface (`Error & { diagnostics: Diagnostic[] }`) naming the
110
+ shape every fallible method already throws — and an `isQuillmarkError(e)`
111
+ guard to narrow caught `unknown`s. No runtime behavior change: the WASM
112
+ layer still throws a plain `Error` with `diagnostics` attached (there is
113
+ deliberately no error class — a structural check works across builds and
114
+ WASM instances). Consumers can delete their hand-rolled
115
+ `.diagnostics`-extraction casts.
116
+ - **Breaking (Rust API + bindings):** a document's `$quill` reference is now
117
+ **enforced** against the loaded quill. Rendering with a quill whose *name*
118
+ differs (`quill::name_mismatch`) or whose *version* falls outside the selector
119
+ (`quill::version_mismatch`) is a hard error via the new
120
+ `RenderError::QuillMismatch`, in both `render` and `dry_run`. Previously a name
121
+ mismatch was only the `quill::ref_mismatch` warning and the version selector
122
+ was unchecked. See [migration guide](docs/migrations/0.88-to-0.89.md).
123
+ - **Fix (WASM bindings):** `Document.makeCard`'s generated TypeScript now marks
124
+ `fields` (and `body`) as optional (`fields?: Record<string, unknown>`,
125
+ `body?: string`), matching the doc comment and runtime behavior. They were
126
+ typed as required because `unchecked_param_type` drops the `?` marker; the
127
+ bindings now use `unchecked_optional_param_type`. Callers can build a bare
128
+ card with `Document.makeCard('kind')`.
129
+
3
130
  ## v0.88.0 - 2026-06-05
4
131
 
5
132
  - **Breaking (bindings + Rust API):** a single canonical **`Card` wire shape** now
package/README.md CHANGED
@@ -8,6 +8,20 @@ Maintained by [TTQ](https://tonguetoquill.com).
8
8
 
9
9
  Use Quillmark in browsers/Node.js with explicit in-memory trees (`Map<string, Uint8Array>` / `Record<string, Uint8Array>`).
10
10
 
11
+ The package exposes **one import surface**:
12
+
13
+ - `@quillmark/wasm` (the root) — the **canonical API**: `Quill`, `Document`, and
14
+ an `Engine` that renders them.
15
+
16
+ `Quill` and `Document` are re-exported verbatim from the internal Typst-less
17
+ core build, so engine-free editor/validation code (`Quill.fromTree`,
18
+ `Document.fromMarkdown`) loads only that small core binary — no backend is
19
+ loaded until you render. The `Engine` hides everything else: each backend (Typst
20
+ today) is a separate, private WASM binary with its own linear memory, lazily
21
+ loaded on the first render. The Engine clones a `Quill` / `Document` into the
22
+ backend's memory as data and frees the clones — you never hold a backend object
23
+ or cross a memory boundary yourself.
24
+
11
25
  ## Build
12
26
 
13
27
  ```bash
@@ -29,10 +43,10 @@ npm test
29
43
  ## Usage
30
44
 
31
45
  ```ts
32
- import { Document, Quillmark } from "@quillmark-test/wasm";
46
+ import { Document, Quill, Engine } from "@quillmark/wasm";
33
47
 
34
- const engine = new Quillmark();
35
- const quill = engine.quill(tree);
48
+ const quill = Quill.fromTree(tree); // engine-free: build + validate
49
+ const engine = new Engine(); // loads a backend lazily on first render
36
50
 
37
51
  const markdown = `~~~
38
52
  $quill: my_quill
@@ -43,16 +57,31 @@ title: My Document
43
57
  # Hello`;
44
58
 
45
59
  const parsed = Document.fromMarkdown(markdown);
46
- const result = quill.render(parsed, { format: "pdf" });
60
+ const result = await engine.render(quill, parsed, { format: "pdf" });
47
61
  ```
48
62
 
49
63
  ## API
50
64
 
51
- ### `new Quillmark()`
52
- Create engine.
53
-
54
- ### `engine.quill(tree)`
55
- Build + validate + attach backend. Returns a render-ready `Quill`.
65
+ ### `new Engine(options?)`
66
+ Create the render dispatcher. Routes each quill to its backend by
67
+ `quill.backendId`, lazily loads that backend binary, and renders — cloning the
68
+ quill/document into the backend's memory and freeing the clones internally.
69
+ `render`, `open`, `supportedFormats`, and `supportsCanvas` are **async** (the
70
+ first call may load a backend). Pass `{ backends }` to register or override
71
+ backend descriptors. Each entry is a descriptor
72
+ (`{ [backendId]: { load, formats, canvas } }`) where `load` is the lazy thunk
73
+ returning the backend module and `formats`/`canvas` are the **required** static
74
+ capability manifest. A malformed descriptor throws at `new Engine(...)`, naming
75
+ the backend id.
76
+
77
+ **Capability probes are always free.** `supportedFormats` and `supportsCanvas`
78
+ depend only on `quill.backendId`, and answer from the descriptor's required
79
+ `formats`/`canvas` manifest — never loading the multi-MB backend binary and
80
+ never cloning the quill. Use them as non-failing pre-render probes.
81
+
82
+ ### `Quill.fromTree(tree)`
83
+ Build + validate a `Quill` from an in-memory tree. Pure — the declared backend
84
+ is resolved at render time, not here. Loads no backend binary.
56
85
 
57
86
  ### `Document.fromMarkdown(markdown)`
58
87
  Parse markdown to a parsed document. Throws a JS `Error` (with `.diagnostics`
@@ -206,18 +235,25 @@ There is one `Card` shape in both directions — `pushCard` / `insertCard` take
206
235
  exactly what `cards` / `removeCard` / `seedCard` return. Build a fresh card
207
236
  from a flat field map with `Document.makeCard(kind, fields?, body?)`.
208
237
 
209
- ### `quill.render(parsed, opts?)` vs. `quill.open(parsed)`
238
+ ### `engine.render(quill, parsed, opts?)` vs. `engine.open(quill, parsed)`
239
+
240
+ > **Experimental:** the entire session surface — `engine.open`,
241
+ > `RenderSession`, `paint`, `PaintOptions`, `PaintResult`, `PageSize`, and the
242
+ > `supportsCanvas` probe — ships ahead of its first production consumer and
243
+ > may change shape in any 0.x release. `engine.render` is the stable path.
210
244
 
211
- Use **`Quill.render`** for one-shot exports (PDF/SVG/PNG) — compiles, emits
212
- artifacts, done. Use **`RenderSession`** (returned by `Quill.open`) for
213
- reactive previews where you'll paint or re-emit pages multiple times: the
214
- session retains the compiled snapshot so subsequent `paint` / `render`
215
- calls skip recompilation. Don't open a session per export.
245
+ Use **`engine.render`** for one-shot exports (PDF/SVG/PNG) — compiles, emits
246
+ artifacts, done. Use **`RenderSession`** (returned by `engine.open`) for
247
+ reactive previews where
248
+ you'll paint or re-emit pages multiple times: the session retains the compiled
249
+ snapshot so subsequent `paint` / `render` calls skip recompilation. Don't open
250
+ a session per export.
216
251
 
217
- ### `quill.render(parsed, opts?)`
218
- Render with a pre-parsed `Document`.
252
+ ### `engine.render(quill, parsed, opts?)`
253
+ Render a pre-parsed `Document` against `quill`. Throws `UnsupportedBackend` if
254
+ no registered backend matches the quill's declared backend.
219
255
 
220
- ### `quill.open(parsed)` + `session.render(opts?)`
256
+ ### `engine.open(quill, parsed)` + `session.render(opts?)`
221
257
  Open once, render all or selected pages (`opts.pages`).
222
258
 
223
259
  The session also exposes `pageCount`, `backendId`, `supportsCanvas`,
@@ -273,8 +309,8 @@ canvas.style.height = `${result.layoutHeight}px`;
273
309
  `densityScale`. Loading the WASM module inside a Worker is the host's
274
310
  responsibility.
275
311
  - Backend support: gated by `supportsCanvas`. Probe upfront with
276
- `quill.supportsCanvas` (or `session.supportsCanvas`) before mounting a
277
- canvas-based UI; the throw on `paint` / `pageSize` remains the
312
+ `engine.supportsCanvas(quill)` (or `session.supportsCanvas`) before mounting
313
+ a canvas-based UI; the throw on `paint` / `pageSize` remains the
278
314
  enforcement contract and includes the resolved `backendId` for
279
315
  debugging.
280
316
 
@@ -287,7 +323,7 @@ A field's *cell* is inferred from whether its schema declares a `default:`:
287
323
  (`validation::field_absent`) — the render path zero-fills it
288
324
  silently. A surviving `<must-fill>` sentinel is fatal
289
325
  (`validation::must_fill_sentinel`). Partial documents are
290
- first-class; `quill.render(doc)` only throws for malformed input.
326
+ first-class; `engine.render(quill, doc)` only throws for malformed input.
291
327
  - **Endorsed** (with `default:`) — `quill.blueprint` renders the
292
328
  default value followed by a `; delete-ok` annotation, and the default
293
329
  is used when the document omits the field.
@@ -299,12 +335,28 @@ code covers unreplaced sentinels.
299
335
 
300
336
  ### Errors
301
337
 
302
- Every method that can fail throws a JS `Error` with `.diagnostics` attached:
338
+ Every method that can fail throws a **`QuillmarkError`** — a JS `Error` with
339
+ `.diagnostics` attached. The type and a guard are exported from the root:
303
340
 
304
341
  ```ts
305
- { message: string, diagnostics: Diagnostic[] }
342
+ import { isQuillmarkError, type QuillmarkError } from "@quillmark/wasm";
343
+
344
+ try {
345
+ const result = await engine.render(quill, doc);
346
+ } catch (e) {
347
+ if (isQuillmarkError(e)) {
348
+ for (const d of e.diagnostics) console.error(d.severity, d.message);
349
+ } else {
350
+ throw e; // not a quillmark failure — programming error, re-throw
351
+ }
352
+ }
306
353
  ```
307
354
 
355
+ `QuillmarkError` is a **structural interface, not a class** — the WASM layer
356
+ throws a real `Error` and attaches the property, so there is no constructor to
357
+ `instanceof` against; narrow with `isQuillmarkError` (which also works on
358
+ errors from any build or WASM instance in the page).
359
+
308
360
  `diagnostics` is always non-empty — length 1 for most failures, length N for
309
361
  backend compilation errors. `message` is derived from `diagnostics`
310
362
  (`diagnostics[0].message` for single-diagnostic errors; an aggregate
@@ -319,7 +371,7 @@ compilation failures. The same shape applies to every throw site:
319
371
  variants (`InvalidFieldName`, `InvalidKindName`, `ReservedKind`,
320
372
  `IndexOutOfRange`) appear in `diagnostics[0].message` with the
321
373
  `[EditError::<Variant>]` prefix.
322
- - `quill.render` / `session.render` — backend compilation failures and
374
+ - `engine.render` / `session.render` — backend compilation failures and
323
375
  validation errors.
324
376
 
325
377
  ### Lifecycle
@@ -338,7 +390,7 @@ For environments where `using` (the [explicit resource management][erm]
338
390
  proposal) hasn't landed, use an explicit `try` / `finally`:
339
391
 
340
392
  ```ts
341
- const session = quill.open(doc);
393
+ const session = engine.open(quill, doc);
342
394
  try {
343
395
  for (let p = 0; p < session.pageCount; p++) {
344
396
  session.paint(ctx, p);
@@ -356,8 +408,8 @@ try {
356
408
  the legacy `~~~card-yaml` opener is still accepted but non-canonical)
357
409
  with a `$quill` system-metadata line. Empty input surfaces a dedicated
358
410
  "Empty markdown input cannot be parsed" message.
359
- - QUILL mismatch during `quill.render(parsed)` is a warning (`quill::ref_mismatch`), not an error.
360
- - Output schema APIs are no longer engine-level in WASM.
411
+ - A `$quill` mismatch during `engine.render(quill, parsed)` is a thrown error, not a warning: rendering with a quill whose *name* differs (`quill::name_mismatch`) or whose *version* falls outside the selector (`quill::version_mismatch`) is rejected.
412
+ - Output schema APIs live on `Quill`, not the engine.
361
413
 
362
414
  ## Changelog
363
415
 
@@ -164,8 +164,9 @@ export interface QuillSchema {
164
164
 
165
165
  /**
166
166
  * Identity snapshot mirroring the `quill:` section of `Quill.yaml`.
167
- * The schema lives on `Quill.schema`.
168
- * Extra `quill:` keys appear as `unknown`.
167
+ * The schema lives on `Quill.schema`; the backend's output formats are a
168
+ * resolved-backend capability read from the engine (`Quillmark.supportedFormats`),
169
+ * not part of this pure-config snapshot.
169
170
  */
170
171
  export interface QuillMetadata {
171
172
  name: string;
@@ -173,8 +174,6 @@ export interface QuillMetadata {
173
174
  backend: string;
174
175
  author: string;
175
176
  description: string;
176
- supportedFormats: OutputFormat[];
177
- [key: string]: unknown;
178
177
  }
179
178
 
180
179
 
@@ -282,7 +281,7 @@ export class Document {
282
281
  * insertion order); `body` defaults to `""`. Kind validity is checked by
283
282
  * `pushCard` / `insertCard`, not here.
284
283
  */
285
- static makeCard(kind: string, fields: Record<string, unknown>, body?: string | null): Card;
284
+ static makeCard(kind: string, fields?: Record<string, unknown>, body?: string): Card;
286
285
  /**
287
286
  * Move the card at `from` to position `to`. `from == to` is a no-op.
288
287
  */
@@ -445,8 +444,16 @@ export class Quill {
445
444
  private constructor();
446
445
  free(): void;
447
446
  [Symbol.dispose](): void;
448
- open(doc: Document): RenderSession;
449
- render(doc: Document, opts?: RenderOptions | null): RenderResult;
447
+ /**
448
+ * Build a quill from a file tree. Pure — no backend, no engine; the
449
+ * declared backend is resolved later, at render time.
450
+ *
451
+ * Accepts either a `Map<string, Uint8Array>` or a plain object
452
+ * (`Record<string, Uint8Array>`). Plain objects are walked via
453
+ * `Object.entries` at the boundary; the Rust side sees a single
454
+ * canonical shape.
455
+ */
456
+ static fromTree(tree: Map<string, Uint8Array>): Quill;
450
457
  /**
451
458
  * Seed a starter composable `Card` of the given kind (carries `$kind`),
452
459
  * committing its fields' `example:` values and leaving every other field
@@ -471,6 +478,19 @@ export class Quill {
471
478
  * `Card` shape as the `Document.main` getter.
472
479
  */
473
480
  seedMain(): Card;
481
+ /**
482
+ * Flatten this quill back into its canonical file tree — the inverse of
483
+ * [`fromTree`](Self::from_tree). Round-trips: `Quill.fromTree(q.toTree())`
484
+ * reproduces an equivalent quill.
485
+ *
486
+ * This is how a quill crosses a WASM linear-memory boundary as data: a
487
+ * `Quill` built in one build (e.g. the Typst-less `@quillmark/wasm/core`)
488
+ * cannot be passed to an engine in another (separate linear memories), so
489
+ * `@quillmark/wasm/runtime` re-feeds this tree to the backend build's
490
+ * `Quill.fromTree` on demand. Keys are `"/"`-joined relative paths,
491
+ * matching what `fromTree` accepts.
492
+ */
493
+ toTree(): Map<string, Uint8Array>;
474
494
  /**
475
495
  * Validate `doc` against this quill's schema, returning every diagnostic
476
496
  * (an empty array when the document is valid).
@@ -484,13 +504,17 @@ export class Quill {
484
504
  */
485
505
  validate(doc: Document): Diagnostic[];
486
506
  /**
487
- * The resolved backend identifier (e.g. `"typst"`).
507
+ * The *declared* backend identifier (`config.backend`, e.g. `"typst"`).
508
+ * Intent, not a resolved capability — capability (`supportedFormats` /
509
+ * `supportsCanvas`) is read from the engine.
488
510
  */
489
511
  readonly backendId: string;
490
512
  readonly blueprint: string;
491
513
  /**
492
- * Identity snapshot of the `quill:` section of `Quill.yaml`, plus
493
- * `supportedFormats` and any extra `quill:` keys.
514
+ * Identity snapshot of the `quill:` section of `Quill.yaml` plus any extra
515
+ * `quill:` keys. Pure config the backend's output formats are a
516
+ * resolved-backend capability read from the engine
517
+ * (`Quillmark.supportedFormats`), not part of this snapshot.
494
518
  */
495
519
  readonly metadata: QuillMetadata;
496
520
  /**
@@ -500,27 +524,38 @@ export class Quill {
500
524
  * `QuillSchema` shape.
501
525
  */
502
526
  readonly schema: QuillSchema;
503
- /**
504
- * `true` iff `RenderSession.paint` and `RenderSession.pageSize` will
505
- * succeed for sessions opened by this quill. Use as a precondition
506
- * probe before mounting a canvas-based preview UI.
507
- */
508
- readonly supportsCanvas: boolean;
509
527
  }
510
528
 
529
+ /**
530
+ * Render engine: a backend registry and render dispatcher. Render build only —
531
+ * the core build constructs and validates quills without it.
532
+ */
511
533
  export class Quillmark {
512
534
  free(): void;
513
535
  [Symbol.dispose](): void;
514
536
  constructor();
515
537
  /**
516
- * Load a quill from a file tree and attach the appropriate backend.
517
- *
518
- * Accepts either a `Map<string, Uint8Array>` or a plain object
519
- * (`Record<string, Uint8Array>`). Plain objects are walked via
520
- * `Object.entries` at the boundary; the Rust side sees a single
521
- * canonical shape.
538
+ * Open an iterative render session for `doc` against `quill`'s backend.
539
+ */
540
+ open(quill: Quill, doc: Document): RenderSession;
541
+ /**
542
+ * Render `doc` against `quill` in one shot. Convenience over `open` +
543
+ * `RenderSession.render`: an unset `output_format` falls back to the
544
+ * backend's first supported format.
545
+ */
546
+ render(quill: Quill, doc: Document, opts?: RenderOptions | null): RenderResult;
547
+ /**
548
+ * The output formats `quill`'s backend can emit. Static capability —
549
+ * resolves the backend but compiles nothing. Throws `UnsupportedBackend`
550
+ * if no registered backend matches the quill's declared backend.
551
+ */
552
+ supportedFormats(quill: Quill): OutputFormat[];
553
+ /**
554
+ * `true` iff `quill`'s backend can paint sessions to a canvas. Asked of
555
+ * the real backend; `false` when the backend is unsupported or non-canvas.
556
+ * Use as a precondition probe before mounting a canvas-based preview UI.
522
557
  */
523
- quill(tree: Map<string, Uint8Array>): Quill;
558
+ supportsCanvas(quill: Quill): boolean;
524
559
  }
525
560
 
526
561
  /**
@@ -558,7 +593,8 @@ export class RenderSession {
558
593
  readonly backendId: string;
559
594
  readonly pageCount: number;
560
595
  /**
561
- * `true` iff `paint` and `pageSize` will succeed for this session.
596
+ * `true` iff `paint` and `pageSize` will succeed for this session. The
597
+ * backend's canvas capability, captured at open time.
562
598
  */
563
599
  readonly supportsCanvas: boolean;
564
600
  /**