@quillmark/wasm 0.105.0 → 0.107.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,393 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.107.0 - 2026-08-17
4
+
5
+ - fix(typst): `display(field, ..)` validates its address against the schema, the
6
+ assert `form-field` and `field-region` already carry. It is the one helper keyed
7
+ by address rather than by value, and it was the one accepting an address the
8
+ schema does not have: `display("issed", "[year]")` compiled, drew nothing, and
9
+ reported nothing — the failure a plate author is least placed to see, a card
10
+ address being a string the plate builds by concatenating `$path`. `_qm-display`
11
+ cannot catch it, carrying an entry per *present* date, so a blank date and a
12
+ typo are absent from it alike; `_qm-known-path` answers about the schema and
13
+ tells the two apart. A known address carrying no date still returns `none`, so
14
+ a `== none` fallback is unchanged: the assert is about the address, the `none`
15
+ about the value.
16
+ - fix: **a container's own `default:` reaches the plate as content.** The render
17
+ floor read `default_content` only for a `richtext`/`plaintext` leaf. An
18
+ `object` or `array` carrying its `default:` on the container fell through to
19
+ the raw literal instead, crossing as unimported markdown where every other
20
+ content position delivers a canonical content object. The companion was
21
+ already cached and never read. The floor now keys off the cache — present is
22
+ the form to commit, absent over a content-bearing tree blank-fills — which
23
+ covers leaf and container alike and drops the type test. `usaf_memo`'s
24
+ `references` (`array<richtext(inline)>`, `default: []`) carried the same
25
+ defect, invisible only because the list was empty.
26
+ - **breaking** a container-shaped `default:`/`example:` on a variant-bearing
27
+ enum is a load error (`quill::default_type_mismatch`,
28
+ `quill::example_type_mismatch`). A quill declaring one loaded before, so the
29
+ upgrade reads as a quill that stopped loading rather than as a fix.
30
+ The container is the shape a *document* writes. As a schema literal it cached
31
+ no content form and yielded no discriminant, so the field blank-filled in
32
+ silence as if nothing were declared. The diagnostic names the discriminant
33
+ spelling instead, which is where a world's cells carry their own literals.
34
+ Scalar literals are unaffected.
35
+
36
+ - feat: **every type nests at every depth**. A property or an element is an
37
+ ordinary field, so it carries whatever a card-level field carries, itself
38
+ included: `object<array<string>>`, `array<array<integer>>`, a typed table whose
39
+ row holds a typed dictionary, and a variant cell holding either.
40
+ `quill::nested_object_not_supported` and `quill::nested_array_not_supported`
41
+ are gone, and `ShapePosition`'s three positions collapse to the one question
42
+ the walk still asks — is this card level, where `variants:` and `ui.group` are
43
+ the two keys that live. A widening: every quill that loaded before loads
44
+ unchanged.
45
+ The depth budget was what the flat address tables existed for.
46
+ `SchemaMeta` carried six name-keyed tables (`array_fields`, `object_fields`
47
+ and their card twins) so the helper's `_qm-known-path` could enumerate two
48
+ suffix steps rather than derive a grammar; they are replaced by one address
49
+ tree — the schema pruned to the steps it offers — that the helper and the span
50
+ scan both walk, converging on the unbounded descent `pdfform::bind` always
51
+ had. Three components deriving an address become one walk each side of the
52
+ seam, and `quillmark/tests/address_grammar.rs` pins the two against the deep
53
+ shapes as well as the shallow ones.
54
+ `variants:` stays card-level, now on its own reasoning rather than by
55
+ inheriting the depth ban: a variant's shape is a function of the schema *and*
56
+ the discriminant, and the union projection, the once-bound form, the plate's
57
+ single branch and `validation::out_of_variant` each hold because that gap is
58
+ one level deep ([SCHEMAS.md](prose/canon/SCHEMAS.md) §"Enum variants"). An
59
+ array-valued variant cell used to report `nested_array_not_supported`, whose
60
+ message named array elements and object properties — neither the situation;
61
+ the shape is now legal and `quill::variant_placement` is left saying only what
62
+ it means.
63
+ - fix: `blueprint()` expands a container at every depth. `build_property_mapping`
64
+ spent each property through the scalar builder, so a nested `object` or typed
65
+ table rendered as `key: null # object` — its own properties, their markers and
66
+ their annotations absent — where the same shape one level up expanded. It now
67
+ recurses, and the card-level and nested paths are one implementation, so a
68
+ `default:` covers its subtree identically wherever it is declared. No
69
+ document changes shape: the shapes this fixes could not be declared before.
70
+ - fix: a content leaf's `default:` reaches the plate from **every** position it
71
+ can be declared in, not only card level. The load pass that imports each
72
+ richtext/plaintext literal into its companion cache walked the card's field
73
+ map, so an `object` property, a typed-table row property and a variant cell
74
+ each kept their authored `default:` and cached nothing; the render floor read
75
+ the leaf's empty companion and blank-filled. A document authoring only the
76
+ container (`dict: {}`, `rows: [{}]`, `c: {value: CUI}`) rendered correctly
77
+ with the author's default missing, and nothing upstream had anything to
78
+ report. The walk now recurses `properties` / `items` / `variants`, the shapes
79
+ `field_contains_content` already descended, so such a document now renders
80
+ **with** the author's default — a render-output change for any quill that
81
+ declared one. Importing a literal is also what checks it, so a nested
82
+ `richtext(inline)` violation — in a `default:` or an `example:` — now fails
83
+ load as a card-level one always has, naming the leaf's declaration path.
84
+ **Breaking on that second count**: the literal loaded before, so a quill
85
+ carrying one stops loading. Nested `example:` *surfacing* was never broken:
86
+ the blueprint prints the raw literal at every depth.
87
+ - **breaking** typst: a plate's direct read of a typed-table row cell regions on
88
+ the cell (`refs.0.org`), where it regioned on the whole array before — a
89
+ *wrong* address, not a missing one, routing a click on the org cell to the
90
+ entire table. The span scan was the third component deriving a schema address
91
+ and the one left at the one-level ceiling: 0.106 lifted the lowering walk and
92
+ `_qm-known-path` to the row property, so the three no longer agreed, and the
93
+ scan is the one that decides what a *read* is attributed to. It now takes the
94
+ index step (`.at(n)`, the only spelling Typst has for an array index) and then
95
+ the row property, gated on the `array_fields` table. Each step is its own
96
+ address, so a whole-row read names the row (`refs.0`) and a primitive
97
+ element's read names the element (`tags.0`); a negative index and an
98
+ undeclared row key mint nothing and fall back as before. Consumers keying on
99
+ the array's address for element ink see the narrower address instead. Explicit
100
+ `field-region` / `form-field` claims are unchanged, and the alias lane keeps
101
+ parity: `#let row = data.refs.at(0)` … `#row.org` regions on `refs.0.org`.
102
+ - test: one table pins the schema address grammar on both backends
103
+ (`quillmark/tests/address_grammar.rs`), covering every position the nesting
104
+ contract admits, each position's card twin, and the rejects that bound each
105
+ step. `PLATE_DATA.md` promises a plate author that one address binds on
106
+ either backend, and the grammar is written twice to keep it — an unbounded
107
+ schema walk in `pdfform::bind`, an enumeration of the suffix forms in the
108
+ Typst helper's `_qm-known-path` — reading two different projections of the
109
+ same `QuillConfig`, so either side can move alone. `pdfform` exports
110
+ `resolves_schema_address` (`#[doc(hidden)]`) so the pin can ask both the same
111
+ question. A body address is the plate grammar's alone and is pinned as such.
112
+
113
+ - **breaking** typst: lowering dispatches on the schema node beside each value
114
+ rather than on tables of top-level field names, so a declared type means the
115
+ same thing wherever it is declared. A `date`, `richtext` or `plaintext`
116
+ declared inside an `object` or an `array` row reached the plate as its raw
117
+ wire value before — a bare string for a date, and for a rich field the
118
+ *internal canonical-content JSON*, rendered as a Typst dict — while the same
119
+ type one level up lowered correctly. Ten of the twelve nested positions the
120
+ schema admits degraded that way, silently: core coerced and validated the
121
+ value correctly, so nothing upstream had anything to report. `contact.note`
122
+ is now a markup block, `contact.reply_by` and `rows.0.on` are `datetime`s,
123
+ and `_qm-plaintext` gains the nested entries that closed the
124
+ `plaintext(field)` escape hatch. The walk is the inverse of the one
125
+ `build_transform_schema` builds the node with, so it cannot be shallower than
126
+ the schema is.
127
+ - **breaking** typst: a `date` / `datetime` field lowers to a **native**
128
+ `datetime`, not the `(value:, display:)` wrapper. `data.issued.year()`,
129
+ `data.issued < data.due` and handing the field to a datetime-consuming
130
+ package are ordinary Typst; `.value` and the paren form `(data.issued.display)(..)`
131
+ are hard Typst compile errors, never a silent degrade, and all consumers are
132
+ first party. A date has no canonical rendering the way authored text does —
133
+ every rendering of `2026-01-02` is a typographic decision the plate owns — so
134
+ it lowers to its value and reaches ink by address instead.
135
+ - feat(typst): `display(field, ..args)`, a date field's content projection,
136
+ keyed by schema address rather than carried on the value. It places rendered
137
+ ink whose glyphs are born in generated source, so a date formatted through a
138
+ `#let` binding, a per-card loop variable, or a vendored package keeps a
139
+ region on its schema field — the affordance the value-object existed to buy,
140
+ now available to any date at any depth without shaping the value. `none` for
141
+ a blank date, so a `== none` fallback still fires. The rule plates follow:
142
+ want a value → `data.<field>`; want clickable ink → `display("<field>", ..)`.
143
+ - feat: a variant cell may carry **any type a card field may**, prose and dates
144
+ included — `quill::variant_field_type` is gone. The load error existed because
145
+ lowering read flat top-level name tables that could not descend into a
146
+ container, so a `date` or `richtext` cell inside a variant would have loaded
147
+ clean and reached the plate as its raw wire value; the schema-node walk reads
148
+ the cell's own declaration, leaving the ceiling nothing to protect. Every value
149
+ surface already descended per live-world cell — coercion through
150
+ `conform_value`, validation through `validate_value`, the render floor through
151
+ `resolve_value` — so the widening needed one real fix:
152
+ `field_contains_content` returned `false` for a variant container on the
153
+ strength of this very guard, which would have silently skipped the content
154
+ companion caches, the resting-form conversion and the seed path for a variant
155
+ content cell. It now answers on the union of the worlds' cells. Containers
156
+ are included: "every type nests at every depth" lands in this same release, so
157
+ a variant cell holds a typed table or a typed dictionary like any other
158
+ position. `variants:` itself stays card-level (`quill::variant_placement`) on
159
+ the reasoning stated there.
160
+
161
+ - **breaking** typst: the `plaintext(field)` helper and its `_qm-plaintext`
162
+ table are removed. Shipped in 0.94 as the sanctioned content→`str` coercion,
163
+ it never acquired a caller: no plate, no vendored package, and no binding
164
+ surface referenced it, and the `create-auto-grid` consumer its own docstring
165
+ cited passes an `array<string>` rather than a content field, so the only
166
+ things exercising it were its three tests. It also carried a three-way name
167
+ collision with the `plaintext` field type and that type's document-layer
168
+ resting shape, which took a standing caveat in canon and the template to hold
169
+ down. A plate that needs a `str` from a content field now has no route, which
170
+ is the honest state of the requirement: reinstating it is additive and cheap
171
+ when a plate actually asks.
172
+ - feat(typst): a typed table's row property (`refs.0.org`) is a writable
173
+ schema address. `form-field(field:)` and `field-region` capped at one suffix
174
+ step while pdfform's resolver descended unboundedly, so a shape
175
+ `ShapePosition` explicitly admits bound on one backend only, against
176
+ `PLATE_DATA.md`'s claim that one address binds on either. `array_fields` now
177
+ carries each array's row property names, the same shape `object_fields`
178
+ already had.
179
+ - fix(typst): a non-blank date the shared parsers reject raises
180
+ `backend::invalid_date` from codegen rather than a pre-pass over top-level
181
+ name tables, so the check covers every depth. Only a direct `apply` can
182
+ deliver one; coercion parses the same way.
183
+ - feat(typst): a scalar read through a `let` alias regions on the address the
184
+ chain it names would carry, so `#let c = data.classification` … `#c.poc`
185
+ surfaces `classification.poc` where it surfaced nothing at all — not the
186
+ container's address, absent. Binding a container once and stepping into it
187
+ three times is the refactor 0.106's property addressing invites, and it cost
188
+ the address silently, the document still rendering correctly. An alias holds
189
+ only where the plate binds the name exactly once to one whole `data` chain: a
190
+ name a second `let`, a closure parameter, a loop pattern, an import, or an
191
+ assignment could rebind is dropped rather than risk attributing another
192
+ value's ink to the field, and a wildcard import disqualifies every alias.
193
+ Which name is followed is half the rule; which *occurrence* is the other half,
194
+ since a schema field name collides freely with the parameter names of a callee
195
+ the plate never defines (`date`, `title`, `caption`, `align`, `subject`). Only
196
+ an occurrence that reads the binding anchors: an identifier spelling the alias
197
+ as a named argument (`#text(size: 12pt)`), a dict key, another value's field
198
+ (`#styles.subject`) or an imported item's path draws no ink off the field, and
199
+ a window minted over one would carry a *wrong* address rather than a missing
200
+ one.
201
+ Laundering past that — a function parameter, a destructured binding, a
202
+ per-card loop variable — is unchanged and still needs a `field-region` claim,
203
+ now stated for plate authors under "Which Reads Get Regions" in the Typst
204
+ backend guide. Content and date fields are unaffected: their ink is born in
205
+ generated code.
206
+
207
+ - feat(wasm): `VARIANT_DISCRIMINANT_KEY` joins the runtime's static exports,
208
+ beside `MAIN_CARD_ADDR`. v0.106.0 announced the constant as new API but shipped
209
+ it to Rust only, leaving a JS consumer reading or writing a variant container
210
+ to spell `"value"` itself — a hardcoded copy of the one value whose purpose is
211
+ to not be hardcoded, at the seam where the two can drift unobserved, since the
212
+ key crosses the boundary inside untyped container data. The `.d.ts` types it as
213
+ the string *literal*, which `string` would stop narrowing an index into the
214
+ container. `known_names_drift.rs` pins both spellings against the Rust
215
+ constant, the guard the hand-spelled name tables beside it already carry.
216
+ `VariantFields` stays Rust-only: it is a type alias the TypeScript surface
217
+ already inlines as `QuillFieldSchema.variants`, naming no shape a consumer
218
+ builds.
219
+ - docs: `0.105-to-0.106.md`, the migration guide v0.106.0 shipped without. It
220
+ leads with the region-address shift rather than the three `!` entries: the
221
+ changelog is organized by feature, and the address step is one clause inside a
222
+ long entry while being the item most likely to break a working consumer, since
223
+ `FieldRegion.field` is a bare `string` that no type checker reports a grammar
224
+ change under. The guide states both gates on the step, and points a consumer at
225
+ `doc.pathFor` for the prefix grammar it would otherwise match as a literal —
226
+ with that helper's limit stated, a field name passing through it verbatim.
227
+ `CONTRIBUTING.md` gains the two rules that would have caught the gap: `!` marks
228
+ an observable-contract shift even where no type changes, and a release carrying
229
+ one ships its guide.
230
+ - docs: `0.106-to-0.107.md`, this release's guide, under the rule the entry above
231
+ adds. It leads with the region-address index step — `main.refs[0].org` where
232
+ `main.refs` stood, a wrong address rather than a missing one — and then with
233
+ the two schema literals that stop a quill loading, since those read as a build
234
+ that broke rather than as a fix. `ERROR.md` names `display(..)` as the
235
+ address-keyed template-author contract, `plaintext(..)` having been removed
236
+ here.
237
+
238
+ <!-- seed: commits since v0.106.0, confirm the entries above cover them, then delete this comment
239
+ - fix: a container's own `default:` crosses as content, or is refused
240
+ - fix(typst): anchor a `let` alias only where the name is read
241
+ - fix(typst): display validates its schema address
242
+ - fix(docs): point the variants cross-reference at this page's own anchor
243
+ - fix(docs): link canon by URL, not by a path outside the docs tree
244
+ - docs: dense-prose pass over the nesting collapse
245
+ - feat: every type nests at every depth
246
+ - docs: dense-prose pass over the two address walks
247
+ - feat(typst)!: the span scan takes the index step, so a row cell read anchors on the cell
248
+ - fix: a nested content leaf's `default:` reaches the plate
249
+ - docs: dense-prose pass over the address-grammar pin
250
+ - test: pin the schema address grammar across both backends
251
+ - Carry the prose style in CLAUDE.md at minimum size
252
+ - Drop two CLAUDE.md lines that steer toward defaults
253
+ - Cut the rules that steer toward defaults
254
+ - docs: dense-prose pass over the lowering walk
255
+ - Cut two lines that restated defaults
256
+ - Rewrite dense-prose as a project-agnostic skill
257
+ - feat!: a variant carries any leaf type, prose and dates included
258
+ - refactor(typst)!: delete the `plaintext(field)` projection
259
+ - feat(typst)!: depth-invariant lowering, and `date` as a native `datetime`
260
+ - docs: dense-prose pass over the alias pass
261
+ - docs: a bound read keeps its address, and what still needs a claim
262
+ - fix(typst): disqualify aliases on a wildcard import in either order
263
+ - feat(typst): follow a single-assignment `let` alias to its schema address
264
+ - feat(wasm): export VARIANT_DISCRIMINANT_KEY to the runtime surface
265
+ - docs: the 0.105 → 0.106 migration guide, and the rules that missed it
266
+ -->
267
+
268
+
269
+ ## v0.106.0 - 2026-08-16
270
+
271
+ - feat(typst,pdfform): a schema address may step one property into a declared
272
+ container, so `form-field(field: "classification.poc")` and
273
+ `field-region("address.city")` name a cell rather than the container holding
274
+ it. Two generated address tables gate the step the way `array_fields` gates
275
+ the index step, and a typed dictionary and a variant container reach both
276
+ alike: `classification.value` addresses the discriminant, `classification.poc`
277
+ a variant cell in any world. The pdfform binder descends a variant container
278
+ to match, so one address binds on either backend where `address.city` bound
279
+ only on pdfform and asserted on Typst. **Region addresses shift** for a plate
280
+ that reads a container property directly: `#data.classification.poc` regions
281
+ as `classification.poc` where it regioned as `classification`, and `fieldAt`
282
+ answers the same. A container read whole is unchanged, as is a read of a key
283
+ the container does not declare.
284
+ - feat(core,wasm)!: an `enum` may declare `variants:`, a per-member field set
285
+ that exists only in the world where the discriminant holds that member. This
286
+ is the DSL's first cross-field shape, and it replaces the `cui_`-prefix
287
+ convention with one the engine checks: `must_fill` inside a variant keeps its
288
+ ordinary `default:`-presence derivation, so it reads *required in this world* —
289
+ a `poc` obliged on a CUI memo and silent on every other one, the thing
290
+ `must_fill` alone could not say. **Breaking**: declaring `variants:` changes
291
+ the field's resting shape at every projection, from a bare string to a
292
+ container, `{value: <member>, …that member's fields}`; the bare scalar
293
+ (`classification: CUI`) is still accepted as the spelling of a world carrying
294
+ no answers, and coercion normalizes both. The wire carries exactly the live
295
+ world, so a plate reads a variant field inside the `values ∪ blank` branch it
296
+ already owes the enum, and inside that branch every declared field is present
297
+ and needs no guard. A value stranded by a discriminant flip is kept and warned
298
+ (`validation::out_of_variant`), never dropped at coercion or gated at render.
299
+ The ceiling is enforced at load, not discovered at render: a variant carries
300
+ plain data only, sits at card level only, and cannot declare `value`. The
301
+ transform schema projects the container with every world's fields flattened
302
+ under `properties`, since a binding built once against a schema must address a
303
+ field today's document has not selected; member scoping stays on the
304
+ declaration view, where `schema()` emits `variants:` keyed by member.
305
+ `FieldSchema` gains `variants` and `variant_field` (the cell a name declares
306
+ under any world); `VariantFields` and `VARIANT_DISCRIMINANT_KEY` are new.
307
+ - feat(fixtures)!: `usaf_memo`'s four `cui_*` fields move under
308
+ `classification`'s `CUI` variant as `controlled_by`, `poc`, `category`, and
309
+ `limited_dissemination`. `controlled_by` and `poc` drop their `default: ""`
310
+ and are therefore obliged — on a CUI memo only, which is what DoDM 5200.48
311
+ actually requires and what the flat spelling could state only in
312
+ `description:` prose. A document writes `classification: {value: CUI, …}` and
313
+ a plate reads `data.classification.value`.
314
+ - feat(typst): `field-region(field, body)` claims the ink `body` draws for a
315
+ schema field, so a plate can tie content it *composes* — a banner keyed on a
316
+ field, a package-built block, a computed table — to `session.regions()` and
317
+ `session.fieldAt(..)`. Layout-neutral: `body` is returned untouched between two
318
+ invisible `metadata` markers. It is a **fallback** claim, never an override:
319
+ ink already tracked to a field keeps that field, so wrapping is purely
320
+ additive and cannot retarget. Each *call* claims independently, so a wrapper
321
+ invoked once per card yields one region per card — the way a card's scalar
322
+ fields get regions at all, reading as they do from a loop variable that carries
323
+ no per-instance identity. The marker stack persists across pages so a claim can
324
+ span a page break, which leaves a claim whose closing marker never reaches a
325
+ frame bounded by nothing: it would take every unattributed piece of ink to the
326
+ end of the document. Those are found before the scan and suppressed in both the
327
+ region and point queries — an unbounded claim yields nothing rather than
328
+ everything — and reported as a `typst::unclosed_field_region` warning naming
329
+ the field, since only the plate author can act on it. Typst does not separate
330
+ the two markers on its own — they are siblings in content flow — but a plate
331
+ emitting the call's return value in parts can.
332
+ - feat(typst,pdf): `form-field` takes `font`, `size`, and `align`, so an
333
+ injected widget's value can be set to match the type around it. A widget was
334
+ fixed at Helvetica, auto-size, left: auto-size makes the rendered size a
335
+ function of both box height and how much the user has typed, and left
336
+ justification cannot be overcome by geometry, because a fillable box is sized
337
+ for the longest plausible value rather than the value in it. A right-aligned
338
+ fill-in — a USAF memo's date, say — was unreachable. `font` is one of
339
+ `"helvetica"`/`"times"`/`"courier"`, a widget being unable to carry a font
340
+ program; `size` is an absolute length or `auto` for the old behavior; `align`
341
+ is `"left"`/`"center"`/`"right"` and lands in `/Q`. All three are rejected on
342
+ `"checkbox"` and `"signature"`, which carry no variable text. `FieldSpec`
343
+ gains `font`, `font_size`, and `align` (`FormFont` and `TextAlign` are new).
344
+ A field that sets none of them stamps byte-identically to before, and
345
+ `pdfform` is untouched: `form.json` still carries no styling, so the flatten
346
+ path and canvas preview are unchanged.
347
+ - fix(fixtures): the `usaf_memo` indorsement date widget is set in the memo's
348
+ own 12pt Times and ends on the right margin, where the date it stands in for
349
+ would have ended. It was auto-sized Helvetica starting at the fill-in rule's
350
+ left end. Sizing it exposed that the rule-width box clips a real date — "28
351
+ September 2026" sets 93pt at 12pt Times against a 72pt box, and a fixed size
352
+ clips where auto-size had silently shrunk — so the widget is now 10em wide
353
+ and hangs off the rule's right edge, overrunning leftwards into the
354
+ whitespace a printed date grows into. Sized in ems of its own face rather
355
+ than inches because `font_size` is a document field with no ceiling: an inch
356
+ width would stay put while the text inside it grew. 10em clears both
357
+ orderings at any body size (DAF's "September 28, 2026" is the widest at
358
+ 8.03em; USAF's "28 September 2026" is 7.78em). `date-placeholder-line` seats
359
+ it with a measured `dx` rather than `place(bottom + right)`, Typst clamping
360
+ an overflowing alignment back to zero, which leaves `right` indistinguishable
361
+ from `left`. That helper draws no rule now and is named `date-placeholder`
362
+ rather than `date-placeholder-line`: the widget carries the date, and a rule
363
+ under a widget wider than it underlines only the fraction of the value narrow
364
+ enough to sit over it. It is package-internal, not exported from `lib.typ`.
365
+ - fix(core): a name two variants declare *differently* is a load error,
366
+ `quill::variant_field_collision`. The name is one cell of the container
367
+ whichever world brings it into play — neither the coercion lookup nor the
368
+ transform schema consults the discriminant to fill it — so two readings of it
369
+ coerced a live value under the other world's type: a document selecting a
370
+ world whose `note` is `integer` had its `42` coerced to `"42"` by a sibling
371
+ world's `string` and then failed `validation::type_mismatch`, undraftable and
372
+ blamed for a string it never wrote. Identical declarations, which is what
373
+ repeating a shared field set or sharing a YAML anchor produces, collapse to
374
+ that one cell without loss and stay legal.
375
+ - fix(core,wasm,python)!: `EditError::UnknownField` carries the in-field path
376
+ `FieldDecode` and `FieldNotContent` carry. A property an `object` field does
377
+ not declare — `get_content_at("address", [Key("zip")])` against an `address`
378
+ with no `zip` — reported `field 'zip' is not declared in the schema`, which
379
+ reads as a claim about a top-level field and collides outright when a real
380
+ top-level field shares the name. It now reports
381
+ `field 'address.zip' is not declared in the schema` and anchors the
382
+ diagnostic at `main.address.zip`, so the caller can tell an undeclared
383
+ property from an undeclared field. **Breaking**: the variant is a struct
384
+ variant, `UnknownField { field, at }`, matching the two siblings; `field`
385
+ stays a bare field name, and the `edit::unknown_field` code, its `field` arg
386
+ and the whole-field message are unchanged.
387
+ - fix(python): declaring `license-files` ships the `LICENSE` the sdist metadata
388
+ names, which PyPI rejected the sdist for lacking. v0.104.0 and v0.105.0 are
389
+ wheels only.
390
+
3
391
  ## v0.105.0 - 2026-08-14
4
392
 
5
393
  - feat(core,wasm,python)!: a `Content` nested inside a composite field is
@@ -462,6 +462,11 @@ export interface QuillFieldSchema {
462
462
  /** The closed set of allowed values. Required on `type: "enum"`, and valid
463
463
  * nowhere else. */
464
464
  values?: string[];
465
+ /** Per-member field sets on a card-level `type: "enum"` field, keyed by
466
+ * member: the fields that exist only where the discriminant holds that
467
+ * member. Declaring it makes the field rest as a container,
468
+ * `{value: <member>, …that member's fields}`, rather than a bare string. */
469
+ variants?: Record<string, Record<string, QuillFieldSchema>>;
465
470
  /** Whether a human must author the field. Absent, it derives from
466
471
  * `default`: a defaulted field is unobliged, a defaultless one obliged. */
467
472
  must_fill?: boolean;
Binary file
@@ -462,6 +462,11 @@ export interface QuillFieldSchema {
462
462
  /** The closed set of allowed values. Required on `type: "enum"`, and valid
463
463
  * nowhere else. */
464
464
  values?: string[];
465
+ /** Per-member field sets on a card-level `type: "enum"` field, keyed by
466
+ * member: the fields that exist only where the discriminant holds that
467
+ * member. Declaring it makes the field rest as a container,
468
+ * `{value: <member>, …that member's fields}`, rather than a bare string. */
469
+ variants?: Record<string, Record<string, QuillFieldSchema>>;
465
470
  /** Whether a human must author the field. Absent, it derives from
466
471
  * `default`: a defaulted field is unobliged, a defaultless one obliged. */
467
472
  must_fill?: boolean;
Binary file
package/core/wasm.d.ts CHANGED
@@ -407,6 +407,11 @@ export interface QuillFieldSchema {
407
407
  /** The closed set of allowed values. Required on `type: "enum"`, and valid
408
408
  * nowhere else. */
409
409
  values?: string[];
410
+ /** Per-member field sets on a card-level `type: "enum"` field, keyed by
411
+ * member: the fields that exist only where the discriminant holds that
412
+ * member. Declaring it makes the field rest as a container,
413
+ * `{value: <member>, …that member's fields}`, rather than a bare string. */
414
+ variants?: Record<string, Record<string, QuillFieldSchema>>;
410
415
  /** Whether a human must author the field. Absent, it derives from
411
416
  * `default`: a defaulted field is unobliged, a defaultless one obliged. */
412
417
  must_fill?: boolean;
package/core/wasm_bg.wasm CHANGED
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quillmark/wasm",
3
- "version": "0.105.0",
3
+ "version": "0.107.0",
4
4
  "description": "WebAssembly bindings for Quillmark, a schema-driven document engine",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -84,6 +84,16 @@ import type { CardAddr } from '../core/wasm.js';
84
84
  */
85
85
  export declare const MAIN_CARD_ADDR: CardAddr;
86
86
 
87
+ /**
88
+ * The key carrying the discriminant inside a variant-bearing enum's value. A
89
+ * field declaring `variants:` rests as `{value: <member>, …that member's
90
+ * fields}`, so reading or writing one means naming this key; it crosses the
91
+ * boundary inside untyped container data, with no type to read it off.
92
+ * Reserved: no variant may declare a field under it, and
93
+ * {@link QuillFieldSchema.variants}, keyed by member, never contains it.
94
+ */
95
+ export declare const VARIANT_DISCRIMINANT_KEY: 'value';
96
+
87
97
  // Core-build types consumers read off `Quill`/`Document`.
88
98
  export type {
89
99
  Card,
@@ -206,6 +206,23 @@ async function instantiateCore(source) {
206
206
  */
207
207
  export const MAIN_CARD_ADDR = Object.freeze({});
208
208
 
209
+ // ── The variant discriminant key ────────────────────────────────────────────
210
+ /**
211
+ * The key carrying the discriminant inside a variant-bearing enum's value.
212
+ *
213
+ * A field declaring `variants:` rests as a container, `{value: <member>, …that
214
+ * member's fields}`, so reading or writing one means naming this key:
215
+ * `doc.storeFields(MAIN_CARD_ADDR, { classification: { [VARIANT_DISCRIMINANT_KEY]: 'CUI' } })`.
216
+ * It crosses the boundary inside untyped container data, with no type to read
217
+ * it off.
218
+ *
219
+ * Reserved: no variant may declare a field under it
220
+ * (`quill::variant_reserved_field_name`), and `QuillFieldSchema.variants`,
221
+ * keyed by member, never contains it.
222
+ * @type {'value'}
223
+ */
224
+ export const VARIANT_DISCRIMINANT_KEY = 'value';
225
+
209
226
  /**
210
227
  * Narrow an unknown caught value to a `QuillmarkError`, the error every
211
228
  * fallible method in this package throws: a real `Error` with a non-empty