@quillmark/wasm 0.104.0 → 0.106.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,249 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.106.0 - 2026-08-16
4
+
5
+ - feat(typst,pdfform): a schema address may step one property into a declared
6
+ container, so `form-field(field: "classification.poc")` and
7
+ `field-region("address.city")` name a cell rather than the container holding
8
+ it. Two generated address tables gate the step the way `array_fields` gates
9
+ the index step, and a typed dictionary and a variant container reach both
10
+ alike: `classification.value` addresses the discriminant, `classification.poc`
11
+ a variant cell in any world. The pdfform binder descends a variant container
12
+ to match, so one address binds on either backend where `address.city` bound
13
+ only on pdfform and asserted on Typst. **Region addresses shift** for a plate
14
+ that reads a container property directly: `#data.classification.poc` regions
15
+ as `classification.poc` where it regioned as `classification`, and `fieldAt`
16
+ answers the same. A container read whole is unchanged, as is a read of a key
17
+ the container does not declare.
18
+ - feat(core,wasm)!: an `enum` may declare `variants:`, a per-member field set
19
+ that exists only in the world where the discriminant holds that member. This
20
+ is the DSL's first cross-field shape, and it replaces the `cui_`-prefix
21
+ convention with one the engine checks: `must_fill` inside a variant keeps its
22
+ ordinary `default:`-presence derivation, so it reads *required in this world* —
23
+ a `poc` obliged on a CUI memo and silent on every other one, the thing
24
+ `must_fill` alone could not say. **Breaking**: declaring `variants:` changes
25
+ the field's resting shape at every projection, from a bare string to a
26
+ container, `{value: <member>, …that member's fields}`; the bare scalar
27
+ (`classification: CUI`) is still accepted as the spelling of a world carrying
28
+ no answers, and coercion normalizes both. The wire carries exactly the live
29
+ world, so a plate reads a variant field inside the `values ∪ blank` branch it
30
+ already owes the enum, and inside that branch every declared field is present
31
+ and needs no guard. A value stranded by a discriminant flip is kept and warned
32
+ (`validation::out_of_variant`), never dropped at coercion or gated at render.
33
+ The ceiling is enforced at load, not discovered at render: a variant carries
34
+ plain data only, sits at card level only, and cannot declare `value`. The
35
+ transform schema projects the container with every world's fields flattened
36
+ under `properties`, since a binding built once against a schema must address a
37
+ field today's document has not selected; member scoping stays on the
38
+ declaration view, where `schema()` emits `variants:` keyed by member.
39
+ `FieldSchema` gains `variants` and `variant_field` (the cell a name declares
40
+ under any world); `VariantFields` and `VARIANT_DISCRIMINANT_KEY` are new.
41
+ - feat(fixtures)!: `usaf_memo`'s four `cui_*` fields move under
42
+ `classification`'s `CUI` variant as `controlled_by`, `poc`, `category`, and
43
+ `limited_dissemination`. `controlled_by` and `poc` drop their `default: ""`
44
+ and are therefore obliged — on a CUI memo only, which is what DoDM 5200.48
45
+ actually requires and what the flat spelling could state only in
46
+ `description:` prose. A document writes `classification: {value: CUI, …}` and
47
+ a plate reads `data.classification.value`.
48
+ - feat(typst): `field-region(field, body)` claims the ink `body` draws for a
49
+ schema field, so a plate can tie content it *composes* — a banner keyed on a
50
+ field, a package-built block, a computed table — to `session.regions()` and
51
+ `session.fieldAt(..)`. Layout-neutral: `body` is returned untouched between two
52
+ invisible `metadata` markers. It is a **fallback** claim, never an override:
53
+ ink already tracked to a field keeps that field, so wrapping is purely
54
+ additive and cannot retarget. Each *call* claims independently, so a wrapper
55
+ invoked once per card yields one region per card — the way a card's scalar
56
+ fields get regions at all, reading as they do from a loop variable that carries
57
+ no per-instance identity. The marker stack persists across pages so a claim can
58
+ span a page break, which leaves a claim whose closing marker never reaches a
59
+ frame bounded by nothing: it would take every unattributed piece of ink to the
60
+ end of the document. Those are found before the scan and suppressed in both the
61
+ region and point queries — an unbounded claim yields nothing rather than
62
+ everything — and reported as a `typst::unclosed_field_region` warning naming
63
+ the field, since only the plate author can act on it. Typst does not separate
64
+ the two markers on its own — they are siblings in content flow — but a plate
65
+ emitting the call's return value in parts can.
66
+ - feat(typst,pdf): `form-field` takes `font`, `size`, and `align`, so an
67
+ injected widget's value can be set to match the type around it. A widget was
68
+ fixed at Helvetica, auto-size, left: auto-size makes the rendered size a
69
+ function of both box height and how much the user has typed, and left
70
+ justification cannot be overcome by geometry, because a fillable box is sized
71
+ for the longest plausible value rather than the value in it. A right-aligned
72
+ fill-in — a USAF memo's date, say — was unreachable. `font` is one of
73
+ `"helvetica"`/`"times"`/`"courier"`, a widget being unable to carry a font
74
+ program; `size` is an absolute length or `auto` for the old behavior; `align`
75
+ is `"left"`/`"center"`/`"right"` and lands in `/Q`. All three are rejected on
76
+ `"checkbox"` and `"signature"`, which carry no variable text. `FieldSpec`
77
+ gains `font`, `font_size`, and `align` (`FormFont` and `TextAlign` are new).
78
+ A field that sets none of them stamps byte-identically to before, and
79
+ `pdfform` is untouched: `form.json` still carries no styling, so the flatten
80
+ path and canvas preview are unchanged.
81
+ - fix(fixtures): the `usaf_memo` indorsement date widget is set in the memo's
82
+ own 12pt Times and ends on the right margin, where the date it stands in for
83
+ would have ended. It was auto-sized Helvetica starting at the fill-in rule's
84
+ left end. Sizing it exposed that the rule-width box clips a real date — "28
85
+ September 2026" sets 93pt at 12pt Times against a 72pt box, and a fixed size
86
+ clips where auto-size had silently shrunk — so the widget is now 10em wide
87
+ and hangs off the rule's right edge, overrunning leftwards into the
88
+ whitespace a printed date grows into. Sized in ems of its own face rather
89
+ than inches because `font_size` is a document field with no ceiling: an inch
90
+ width would stay put while the text inside it grew. 10em clears both
91
+ orderings at any body size (DAF's "September 28, 2026" is the widest at
92
+ 8.03em; USAF's "28 September 2026" is 7.78em). `date-placeholder-line` seats
93
+ it with a measured `dx` rather than `place(bottom + right)`, Typst clamping
94
+ an overflowing alignment back to zero, which leaves `right` indistinguishable
95
+ from `left`. That helper draws no rule now and is named `date-placeholder`
96
+ rather than `date-placeholder-line`: the widget carries the date, and a rule
97
+ under a widget wider than it underlines only the fraction of the value narrow
98
+ enough to sit over it. It is package-internal, not exported from `lib.typ`.
99
+ - fix(core): a name two variants declare *differently* is a load error,
100
+ `quill::variant_field_collision`. The name is one cell of the container
101
+ whichever world brings it into play — neither the coercion lookup nor the
102
+ transform schema consults the discriminant to fill it — so two readings of it
103
+ coerced a live value under the other world's type: a document selecting a
104
+ world whose `note` is `integer` had its `42` coerced to `"42"` by a sibling
105
+ world's `string` and then failed `validation::type_mismatch`, undraftable and
106
+ blamed for a string it never wrote. Identical declarations, which is what
107
+ repeating a shared field set or sharing a YAML anchor produces, collapse to
108
+ that one cell without loss and stay legal.
109
+ - fix(core,wasm,python)!: `EditError::UnknownField` carries the in-field path
110
+ `FieldDecode` and `FieldNotContent` carry. A property an `object` field does
111
+ not declare — `get_content_at("address", [Key("zip")])` against an `address`
112
+ with no `zip` — reported `field 'zip' is not declared in the schema`, which
113
+ reads as a claim about a top-level field and collides outright when a real
114
+ top-level field shares the name. It now reports
115
+ `field 'address.zip' is not declared in the schema` and anchors the
116
+ diagnostic at `main.address.zip`, so the caller can tell an undeclared
117
+ property from an undeclared field. **Breaking**: the variant is a struct
118
+ variant, `UnknownField { field, at }`, matching the two siblings; `field`
119
+ stays a bare field name, and the `edit::unknown_field` code, its `field` arg
120
+ and the whole-field message are unchanged.
121
+ - fix(python): declaring `license-files` ships the `LICENSE` the sdist metadata
122
+ names, which PyPI rejected the sdist for lacking. v0.104.0 and v0.105.0 are
123
+ wheels only.
124
+
125
+ <!-- seed: commits since v0.105.0, confirm the entries above cover them, then delete this comment
126
+ - chore(repo): union-merge CHANGELOG.md
127
+ - feat: migrate usaf_memo to enum variants, document the axis
128
+ - feat(core): enum variants — fields that exist only for one enum value
129
+ - fix(fixtures): drop the rule under the indorsement date
130
+ - fix(fixtures): size the indorsement date widget in ems, not inches
131
+ - fix(fixtures): set the memo's indorsement date widget like a date
132
+ - feat(typst,pdf): font, size, and align on injected form fields
133
+ - Make the usaf_memo indorsement's blank date a bound fill-in widget
134
+ - Compress the field-region prose
135
+ - Add `field-region` so plates can tie composed content to a field
136
+ - fix(python): declare license-files so the sdist ships its LICENSE
137
+ - Anchor an undeclared property at the property, not at a bare name
138
+ -->
139
+
140
+
141
+ ## v0.105.0 - 2026-08-14
142
+
143
+ - feat(core,wasm,python)!: a `Content` nested inside a composite field is
144
+ readable at its own codec. `TypedReader::get_content_at(name, path)` (and the
145
+ `CardReader` twin, `reader.getContentAt(addr, path)` in JS,
146
+ `reader.get_content_at(name, path)` in Python) walks a `PathSegment` path
147
+ through the field schema — `items` for an index, `properties` for a key — to
148
+ the leaf whose declared type names the codec, then decodes through the same
149
+ dispatch the whole-field read uses. So an `array<richtext>` element, an
150
+ `object`'s content property and a leaf under both each read back the same
151
+ `Content` whatever their resting form, where before every one of them
152
+ answered `FieldNotContent` and the consumer had to decide for itself what the
153
+ stored bytes meant (#1243). The empty path *is* `get_content`. A path naming
154
+ nothing in the stored value reads absent rather than throwing: an editor's
155
+ row index goes stale between derive and read, and that is the axis a repeater
156
+ mutates. `Addr` deliberately gains no element axis — the path is the read's
157
+ own argument, since `storeField` / `isFill` / `applyChange` could not answer
158
+ one. **Breaking**: `EditError::FieldDecode` and `EditError::FieldNotContent`
159
+ each gain an `at: Vec<PathSegment>` field carrying the in-field path, so the
160
+ diagnostic anchors at `main.paragraphs[1]` and parses back to those segments;
161
+ `field` stays a bare field name and `args` is unchanged. `FieldNotContent`
162
+ now names the type *reached*, so a `string[]` element reports `string` rather
163
+ than the field's `array`.
164
+ - feat(core,wasm)!: `must_fill:` on a field declares the **obligation** axis.
165
+ `default:` carried the fill value and the obligation signal on one bit, so
166
+ only that 2x2's diagonal was reachable; a safe value that still wants a
167
+ human's confirmation (`default: UNCLASSIFIED` with `must_fill: true`) and a
168
+ genuinely optional field with nothing to suggest (`must_fill: false`) now
169
+ each have a spelling. Left unset it derives `default.is_none()`, so no
170
+ existing quill's blueprint marker set changes. `Quill::validate` gains a
171
+ second trigger under the one `validation::must_fill` code, named by a
172
+ `trigger` arg: `marker` for a `!must_fill` tag the document carries, and
173
+ `unauthored` where the schema obliges a cell the document leaves absent or
174
+ present-null. The second closes a hole — `validate_fills` walked only the
175
+ payload, so a hand-written or programmatically built document drew no
176
+ completeness signal whatever `Quill.yaml` declared. **Breaking**: a merely
177
+ incomplete document no longer validates clean. Absence is still never
178
+ *malformed* and still never gates render, but a consumer reading "any
179
+ diagnostic ⇒ not done" now sees a warning per unauthored obliged cell on
180
+ documents that were silent. The obligation keys on cell presence rather than
181
+ the resolved source rung, so a must-fill leaf inside a container someone
182
+ touched still warns; a typed dict is never itself a cell and recurses to its
183
+ leaves, while an array is one cell, `[]` being a real answer. Authoring the
184
+ field's blank discharges it and `field: null` does not: null ≡ absent stays
185
+ unqualified on the value ladder, but obligation asks whether a human made a
186
+ call. Seeding stamps the marker on example-seeded obliged cells, so a fresh
187
+ seed and an empty document report the same cells, and the transform schema
188
+ carries `quillmark:must_fill` (#1255).
189
+ - fix(core,wasm,pdfform)!: a field's **blank** — its spelling of "explicitly
190
+ nothing" — is a property of the field rather than a member of its type's
191
+ domain, and an `enum`'s is `""`. The render floor for a defaultless enum
192
+ returned `values.first()`: a choice nobody made, indistinguishable at the
193
+ plate from a deliberate one and reachable from a cosmetic `values:` reorder.
194
+ An unanswered enum now renders `""`, so a reorder is render-safe for every
195
+ document and only removing or renaming a member breaks
196
+ ([VERSIONING.md](prose/canon/VERSIONING.md)). **The accepted domain widens to
197
+ `values ∪ blank` for *every* enum**, defaulted ones included: `format: ""`
198
+ was a fatal `EnumViolation` and now coerces, validates and reaches the plate.
199
+ **A plate must therefore branch exhaustively over `values ∪ blank`** — an
200
+ `else` fallback re-opens exactly the fabrication the blank closes, and a
201
+ downstream package that asserts membership fails the compile outright. Note
202
+ `data.at(key, default: X)` is not a guard here: blank-filled render makes
203
+ every declared key present, so its `default:` is dead code and the blank
204
+ flows through. **Breaking**: `zero_value` → `blank`, `FieldSource::Zero` →
205
+ `Blank` and its wire token `"zero"` → `"blank"`; `""` declared in `values:`
206
+ is a load error (`quill::enum_blank_member`), the engine supplying the blank
207
+ instead; and `date: ""` renders blank rather than falling back to a
208
+ `default:`, settling a three-way disagreement between coercion, validation
209
+ and the floor. `default: ""` stays valid and keeps its meaning — `values:`
210
+ enumerates choices, `default:` is a value, and the blank is a legal value
211
+ that is never a choice. Additive: `ui.blank_title` labels an enum's blank and
212
+ rides the transform schema as `quillmark:blank_title`; that schema's `enum:`
213
+ leads with the blank, so a standard JSON-Schema validator accepts what the
214
+ engine accepts, and pdfform Choice widgets lead their options with it too. A
215
+ consumer's picker must keep the blank selectable and re-selectable — returning
216
+ to it is how an author clears a cell back to unset.
217
+ `integer`, `number` and `boolean` keep `0` / `false` as their blank,
218
+ indistinguishable from an authored zero — a permanent seam, since a wire
219
+ `none` would cost the totality the floor exists to buy. Full guide:
220
+ [0.104 → 0.105](docs/migrations/0.104-to-0.105.md) (#1254).
221
+ - fix(fixtures,docs): the three fixture plates that dispatch on `$kind` read it
222
+ with a bare `card.at("$kind")`, which panics on a kindless card, and guarded
223
+ declared fields against an absence blank-filled render makes impossible.
224
+ Plate authors copy the fixtures rather than `PLATE_DATA.md`, so the fixtures
225
+ were teaching both the unsafe metadata read and a dead presence check.
226
+ `classic_resume` carried the live consequence: `url` is declared with no
227
+ `default:`, so the floor delivers `""`, `default: none` never fires, and the
228
+ package's `url != none` test always passes — an empty Courier element where
229
+ the block should have been skipped. Its `subheading-*` guard cost an empty
230
+ grid row the same way. Declared fields now guard their *value*, and
231
+ `docs/quills/typst-backend.md` states the rule as a table over the three key
232
+ kinds. `fixture_quills_render_test` renders every fixture quill's seed
233
+ document — the net that was missing, since `classic_resume`'s plate had no
234
+ test reaching it (#1256, #1257).
235
+ - test(typst): `plaintext` reaches regions and navigation by inheritance — the
236
+ render floor coerces its resting literal to a content object, the backend
237
+ classifies that object by `contentMediaType` alone, and the shared lowering
238
+ emits it with a segment map — and every step was load-bearing and untested,
239
+ with the classification predicates named for richtext so the sharing read as
240
+ a coincidence. Pinned at engine altitude, because a test driving the backend
241
+ directly hand-builds the content object, bypassing the floor's coercion, and
242
+ would stay green through a regression that silently empties `regions()`. The
243
+ predicates are `is_content_field` / `is_content_array_field` /
244
+ `is_inline_content_field`, and `PREVIEW.md` names plaintext beside richtext
245
+ in its producer list (#1247, #1250).
246
+
3
247
  ## v0.104.0 - 2026-08-13
4
248
 
5
249
  - feat(core,wasm): a quill declares, per body, the block constructs its plate
@@ -93,33 +337,6 @@
93
337
  fixture suite reached neither path. A fourth carries a `!must_fill` tag on two
94
338
  example-seeded cells through seed → store → load → conform. Refs #1234
95
339
 
96
- <!-- seed: commits since v0.103.0, confirm the entries above cover them, then delete this comment
97
- - Carry the thematic-break and unsupported-construct work into the guide
98
- - Dense-prose pass over the new comments and docs
99
- - Let a quill declare the constructs its plate does not typeset
100
- - Drain the memo's heading buffer instead of dropping it
101
- - Canonicalize a thematic break to `***`
102
- - Close the 0.104 release gaps: the migration guide, and four changelog entries
103
- - Retire the enum: modifier; project a field's domain by capability
104
- - Pin the render floor's two type-domain edges and the seeded fill tag
105
- - Raise the Node floor to 24
106
- - dense-prose pass over the added prose
107
- - docs: dense-prose pass on the text-type sections
108
- - docs: give the text-type choice a decision procedure
109
- - docs: state the backend seam's scope and reserve its namespaces
110
- - docs: drop the private intra-doc link from pathFor
111
- - fix(core): state the unspellable-tail conditions instead of linking a private fn
112
- - chore(wasm): density pass, and gate a core-build dead function
113
- - docs: dense-prose pass over the geometry-address translation
114
- - docs: dense-prose pass over the pathFor / cardPath prose
115
- - test(wasm): execute the static-export exemption instead of listing it
116
- - feat(wasm): mint a DocPath from an Addr (#1225)
117
- - fix(core)!: parse geometry addresses segment-wise (#1223)
118
- - docs: carry the 0.103 CLI break, and the init gate into the quickstart (#1222)
119
- - SCHEMAS.md: a declared type change rewrites stored values (#1221)
120
- -->
121
-
122
-
123
340
  ## v0.103.0 - 2026-08-09
124
341
 
125
342
  - docs: `docs/integration/operations.md`, carrying what the other integration