@quillmark/wasm 0.115.0 → 0.116.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,210 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.116.0 - 2026-09-26
4
+
5
+ Upgrade path: [0.115 → 0.116](docs/migrations/0.115-to-0.116.md).
6
+
7
+ ### What blocks a render
8
+
9
+ - feat(core)!: **a card no kind claims, and a body under `body.enabled: false`,
10
+ render and warn.** A render fails only where the engine would have to invent
11
+ what the author wrote: a value that will not read as its declared cell's
12
+ type, or markup the grammar cannot read. Input no declaration claims
13
+ displaces nothing, so it renders and warns; SCHEMAS.md § "What blocks a
14
+ render" sorts every validation code into incomplete, malformed and
15
+ unclaimed. `validation::unknown_card` and `validation::body_disabled` are
16
+ `Severity::Warning` from `Quill::validate`, `unknown_card` carries
17
+ `allowed`, and an unclaimed card keeps its place in `$cards` with its
18
+ fields verbatim. `ValidationError::UnknownCard` and `BodyDisabled` are
19
+ deleted. This is the release's silent break: a pipeline that relied on the
20
+ render failing catches nothing. (#1877)
21
+ - feat(core)!: **an undeclared key warns `validation::unknown_field`**, at
22
+ every depth of a claimed card: a card field, a typed dictionary's property,
23
+ an array element's, a matrix member's column, and a cell inside a variant
24
+ container no world declares. `args` carry `suggestion`, the closest declared
25
+ field the document leaves unwritten, or `container` and `variant` for a
26
+ variant cell written beside its discriminant rather than under it. (#1871)
27
+ - feat(quillmark,cli)!: **a one-shot render carries validate's warnings.**
28
+ `Quillmark::render`, WASM `engine.render` and Python `render` put every
29
+ `Quill::validate` warning on the result's `warnings` ahead of the compile's,
30
+ so the unclaimed input the page left out is on the result a pipeline reads.
31
+ A session's warnings stay its current compile's. (#1931)
32
+ - feat(core)!: **a card-yaml block with no `$kind` fails the parse** as
33
+ `parse::missing_kind`, located at its opener, with a hint naming both fixes:
34
+ a `$kind:` line, or a backtick fence. Such a block was a card missing its
35
+ kind line or tilde-fenced code, and reading it as either was a guess. Every
36
+ card now carries `$kind`: `ResolvedCard.kind` is `String` (`string` in
37
+ TypeScript), and a stored card with no `$kind` folds into the body above it
38
+ on load, its payload as a code block, then its body. (#1905)
39
+ - feat(core)!: **a card-yaml payload that is not a mapping fails at its
40
+ fence** as `parse::payload_not_mapping`, located at the opener, with `actual`
41
+ and `info` args and a hint naming the backtick fence, where it was an
42
+ unlocated `parse::invalid_structure` on a card and `parse::missing_quill` on
43
+ the root. (#1873)
44
+
45
+ ### `!must_fill` is removed
46
+
47
+ - refactor(core)!: **`!must_fill` and `validation::must_fill` are gone.** The
48
+ marker said a cell held a placeholder rather than an answer, and the schema
49
+ half of the warning read `default:`'s absence as an obligation. `default:`
50
+ is the value an unanswered cell renders, `?` moves the render floor, and
51
+ null ≡ absent holds on every surface. A value under the marker keeps its
52
+ value: in markdown the tag drops as any unknown tag does, warned
53
+ `parse::unsupported_yaml_tag` at the tagged node's rooted path (which that
54
+ code now carries), and a stored row of any earlier tag is read past its
55
+ `fill` keys. Deleted: `store_fill`,
56
+ `is_fill`, the `QuillValue` fill set, `FieldSchema::{must_fill,
57
+ example_content}`, `edit::fill_on_mapping`,
58
+ `parse::fill_marker_unsupported_position`, WASM `storeFill` / `isFill`, and
59
+ the card wire's `fill` / `nestedFills`. (#1906, #1933)
60
+ - refactor(core)!: **the storage tag is `quillmark/document@0.116.0`.** A
61
+ field item carries no `fill` or `nested_fills`, and a row carrying either,
62
+ or any key its type does not name at the document, card, payload or item
63
+ level, fails the load, as does a nested comment spanning lines, which
64
+ `to_markdown` would have written as bare YAML. Every earlier tag still
65
+ reads. A row's bytes move on its next save, so content hashes
66
+ recompute and a 0.115 build refuses the row from then on.
67
+ - feat(core,wasm,python)!: **a card-wire payload item refuses an unknown
68
+ key.** A 0.115 integration still sending `{value, fill: true}` committed the
69
+ placeholder as an answer, and an item typo vanished; `insertCard` throws and
70
+ `insert_card` raises `ValueError` instead. (#1933)
71
+ - feat(core): **a `?` on a cell's type renders it `none` when unanswered**,
72
+ so a plate can tell "0 votes against" from "nobody entered a tally". `?` and
73
+ `default:` are exclusive (`quill::optional_default`), a namespace takes no
74
+ `?` (`quill::optional_namespace`), and the transform schema projects
75
+ `type: [t, "null"]`. (#1880)
76
+
77
+ ### The blueprint and the seed
78
+
79
+ - feat(core)!: **seeding commits no `example:`.** An example documents a
80
+ cell's shape rather than answering it, and `body.example` is guide text
81
+ quills write as instructions. `seed_document`, `seed_main` and `seed_card`
82
+ commit the `$seed` overlay alone; every other body is empty and every other
83
+ field absent. (#1923)
84
+ - feat(core,wasm,python)!: **the blueprint answers nothing on the author's
85
+ behalf.** A defaultless cell is empty (`title: # string`) with its
86
+ `example:` on a `# e.g.` line above it; every body is empty, a
87
+ `body.example` riding a `# body e.g.` line that closes the payload and a
88
+ kind under `body.enabled: false` closing with `# no body`; a card's
89
+ description rides its `$kind` line; a `default: []` table is led by the
90
+ whole field commented out holding its row; and a matrix names its columns
91
+ in an `# e.g.` line, a column with no example showing its
92
+ `<type>[<format>]`. (#1874, #1942)
93
+ - feat(core,wasm,python)!: **`FORMAT_RULES` is `MARKDOWN_RULES`**, headed
94
+ `Quillmark Markdown rules:` (`markdownRules` / `markdown_rules`), and keeps
95
+ only what departs from plain Markdown and YAML. `blueprint_instruction` is
96
+ deleted in every binding: a prompt is the consumer's to write. (#1942)
97
+ - feat(core)!: **`to_markdown` writes a multi-line string as a `|` literal
98
+ block** (`|-` without a trailing newline), at every depth, where it wrote
99
+ one double-quoted line. A string a block would not hold verbatim stays
100
+ quoted. The value round-trips unchanged; the markdown bytes move. (#1883)
101
+
102
+ ### The quill authoring contract
103
+
104
+ - feat(core)!: **a field's and a card kind's label is a top-level `title`**,
105
+ beside `description`, which `schema()` emits and the blueprint prints as
106
+ `# <title> — <description>`. `ui.title` fails the load, a `{field}` token
107
+ in a `title` is `quill::title_template`, and a `title` on an array's `items`
108
+ is `quill::title_on_items`. (#1922)
109
+ - refactor(core,wasm)!: **`body.unsupported` is removed**, with
110
+ `plate::unsupported_construct`, `Quill::unsupported_constructs` and the WASM
111
+ `QuillBlockConstruct`. Nothing verified a declaration against the plate and
112
+ no editor read it; a `Quill.yaml` still declaring it fails
113
+ `quill::invalid_body`.
114
+ - fix(core)!: **an unquoted `quill.version` reads as written**: `version:
115
+ 1.10` is `1.10`, where it loaded as `1.1`, and `1.1e1` fails
116
+ `quill::invalid_version`.
117
+ - feat(core)!: **a `plaintext(inline)` value ending in a newline names `|-`
118
+ as the fix.** `validation::not_inline`, `edit::field_not_inline` and
119
+ `conform::field_not_inline` carry `trailingNewline: true`, and
120
+ `ValidationError::NotInline` / `EditError::FieldNotInline` gain
121
+ `trailing_newline`. (#1918)
122
+ - fix(core): `quill::invalid_name` suggests a name the loader accepts.
123
+
124
+ ### The render date
125
+
126
+ - feat(core)!: **the host supplies the render date; a `date` field may hold
127
+ `today`.** The engine reads no clock. `Quillmark::open` / `render`,
128
+ `Quill::compile_data` / `compile_checked` / `resolve`,
129
+ `QuillConfig::compile_data` / `compile_checked`, `TypedReader::resolve` and
130
+ `Backend::open` take a required `today: CalendarDate`; a plate's
131
+ `datetime.today()` returns it whatever its `offset:`. The CLI (`--today`)
132
+ and the WASM and Python bindings default to the local date. A `date` value
133
+ is `YYYY-MM-DD` or `today`, and the blueprint annotates `date<YYYY-MM-DD |
134
+ today>`. `CalendarDate`'s parse fails with `ParseDateError`, which tells a
135
+ day the calendar lacks from a string that is no date. (#1900, #1930)
136
+ - feat(fixtures)!: `usaf_memo`'s blank `date` renders a fill-in field where it
137
+ rendered today's date.
138
+
139
+ ### Plates and the Typst backend
140
+
141
+ - feat(typst)!: **a dictionary carries the `$ink` twin its fields print
142
+ through.** A scalar printed off `data` kept its click target only as a
143
+ direct read in the plate. `data`, each card, and each typed dictionary or
144
+ table row carry `$ink` and `$path` wherever a field has ink: compute with the
145
+ dictionary, print its ink (`ink(row).org`). A date's ink prints its default
146
+ display, and `display(row, "due", "[year]")` formats one by dictionary and
147
+ key. The keys are visible to `keys()`, `pairs()`, `len()`, `==` and a
148
+ spread. (#1868)
149
+ - feat(typst)!: **a plate imports the quill's `.typ` files by Typst's path
150
+ rules.** The plate loads at its `plate_file` path and every `.typ` file
151
+ outside `packages/` beside it, so a bare path resolves from the importing
152
+ file and a `/`-rooted one from the quill root. Plate diagnostics name the
153
+ declared `plate_file`, where they named `main.typ`, and a vendored package's
154
+ file under its spec (`@local/p:0.1.0/lib.typ`), apart from a quill module
155
+ of the same path. (#1869)
156
+ - feat(typst): **a `typst:` key the backend does not read warns
157
+ `typst::unknown_key`**, `typst.packages` included; a package is vendored
158
+ under `packages/`, never fetched. A package manifest without `version` or
159
+ `entrypoint` is skipped at load.
160
+ - refactor(typst): the `when` / `value-or` plate helpers are dropped; a plate
161
+ reads an optional cell with `!= none`.
162
+ - fix(typst): a negative scalar's ink lexes, and region queries stay linear.
163
+
164
+ ### Rendering, the CLI and the bindings
165
+
166
+ - feat(core)!: **one raster ceiling, 16384 px a side**, `MAX_RASTER_SIDE`,
167
+ owned by core, where core refused past 16384² px of area and the WASM
168
+ painter clamped by side on its own. A PNG export long on one axis that fit
169
+ by area now fails `backend::invalid_raster_scale`.
170
+ - feat(wasm)!: **`paint(ctx, page, scale)` takes one scale and returns
171
+ nothing**; `PaintOptions` and `PaintResult` are deleted, and a missing or
172
+ non-finite scale throws `backend::invalid_raster_scale`.
173
+ - feat(cli): **`quillmark workspace` exports a plate for Typst's own
174
+ tooling**: the generated helper for one document, every vendored package
175
+ and the backend's fonts, with the `typst watch` command over them. A rerun
176
+ replaces the `packages/` and `fonts/` an earlier export wrote. The facade
177
+ re-exports it as `quillmark::typst_workspace`. (#1869)
178
+ - feat(cli): **`quillmark check` prints a document's every diagnostic**, and
179
+ `--strict` exits `1` on a warning, for a CI gate over a repository of
180
+ documents; `render` prints the warnings for what the page leaves out.
181
+ (#1872)
182
+ - fix(cli)!: `render` takes its format from an `-o` extension naming one, and
183
+ a `-f` the extension contradicts exits `1` before writing.
184
+ - feat(core,wasm,python): `Quill.emptyDocument()` / `Quill.empty_document()`
185
+ reach the bindings, and WASM `isInline` / `isPlain` export the content
186
+ predicates. (#1919)
187
+
188
+ ### Fixes
189
+
190
+ - fix(core): the YAML prescan keeps comments, tags and anchors with the value
191
+ they sit on across continuation lines, dash lines, block scalars, multi-line
192
+ flow collections, a value below its key, nested keys with an inner `:` or a
193
+ leading `-`/`?`, and a removed item's trailing comment; a `$seed` overlay edit keeps the comment on
194
+ its `$seed` line.
195
+ - fix(core): `to_markdown` projects a content object at every depth of a
196
+ field's value, and a container under a sequence item's first key
197
+ round-trips.
198
+ - fix(core): a folded island past `isl-{u64::MAX}` mints a fresh id, and a
199
+ folded card's `$ext` stays out of the body.
200
+ - fix(core): the unknown-key walk stays linear in a mapping's keys.
201
+ - fix(core): a `$seed.<kind>.$body` for a kind under `body.enabled: false`
202
+ warns `validation::seed_unknown_field`, where the seed dropped it silently.
203
+ - fix(cli): `validate -v` reports a backend load warning once, where each of
204
+ the three canonical renders repeated it.
205
+ - fix(quillmark): the facade re-exports `ParseDateError`.
206
+
207
+
3
208
  ## v0.115.0 - 2026-09-22
4
209
 
5
210
  Upgrade path: [0.114 → 0.115](docs/migrations/0.114-to-0.115.md).
package/README.md CHANGED
@@ -150,8 +150,7 @@ document was built. Parse warnings and the `conform::*` warnings both ride
150
150
  other way (`fromStored`, a stored row), returning the `conform::*` `Diagnostic[]`
151
151
  (`[]` when everything rested). It is idempotent and a byte no-op on an
152
152
  already-canonical document, YAML comments included, so calling it on every load
153
- is safe. A `!must_fill` marker anywhere in a field's value skips that field; a
154
- value the strict write refuses stays as authored under a warning. Both throw
153
+ is safe. A value the strict write refuses stays as authored under a warning. Both throw
155
154
  when the document declares a `$quill` this quill does not answer to, before any
156
155
  mutation.
157
156
 
@@ -175,9 +174,9 @@ const doc = Document.storageVersionOf(content)
175
174
  : Document.fromMarkdown(content);
176
175
  ```
177
176
 
178
- The `schema` value (`quillmark/document@0.115.0`) is the **model version**,
177
+ The `schema` value (`quillmark/document@0.116.0`) is the **model version**,
179
178
  not the running crate version. It is a hand-set constant, bumped only when
180
- the `Document` model itself changes, so every `0.115.x` patch release reads
179
+ the `Document` model itself changes, so every `0.116.x` patch release reads
181
180
  and writes that same value.
182
181
 
183
182
  - **Upgrading is safe.** A newer build reads documents an older build's
@@ -220,8 +219,9 @@ To render a form editor, read field definitions from `quill.schema` (walk
220
219
  values from the `Document` payload: there is no separate form-view projection.
221
220
  `quill.validate(doc)` scores it without invoking the backend.
222
221
 
223
- `quill.seedDocument()` returns a starter document with each field's `example:`
224
- committed; `quill.seedMain()` and `quill.seedCard(kind)` seed one card. All
222
+ `quill.seedDocument()` returns a starter document: one card per kind, each body
223
+ empty, every field absent (no `example:` is committed);
224
+ `quill.seedMain()` and `quill.seedCard(kind)` seed one card. All
225
225
  return the read `Card` shape of `doc.main` / `doc.cards`, which `doc.insertCard`
226
226
  accepts directly:
227
227
 
@@ -248,7 +248,7 @@ card is an object literal: one `{ type: "field", key, value }` per field in
248
248
  `{ card?, field? }`, absent `card` = main, absent `field` = body, and a bare
249
249
  string is shorthand for `{ field }`. So `doc.storeField("qty", 3)` targets the
250
250
  main card's `qty`, `doc.storeField({ card: 2, field: "qty" }, 3)` a composable
251
- card's. Reads are total over the field axis (`getStored` → `undefined`, `isFill` → `false` for
251
+ card's. Reads are total over the field axis (`getStored` → `undefined` for
252
252
  an absent field; only an out-of-range card throws); field writes throw on a body
253
253
  address. `getStored` is the verbatim transport read, distinct from the interpreted
254
254
  `quill.reader(doc).get`; `bodyMarkdown` is the body markdown read (a `CardAddr`; a field's
@@ -289,7 +289,7 @@ on `Document` itself (**store** = verbatim, **set** = typed):
289
289
  `_reviseField` ABI the writer delegates to, hidden from the `.d.ts`.)
290
290
 
291
291
  - **`store*`: the deliberate quill-free primitive.** `doc.storeField(addr, value)`
292
- / `doc.storeFields(cardAddr, {...})` (and `storeFill`) validate only the field
292
+ / `doc.storeFields(cardAddr, {...})` validate only the field
293
293
  name/depth/kind and store the value verbatim, no quill required. Reach for it
294
294
  on purpose when you *want* the opaque store: quill-agnostic storage/migration
295
295
  infra that has no bundle and must write regardless of a drifted schema;
@@ -354,43 +354,47 @@ tells you which pages to repaint (`dirty ∩ visible`). `update` is transactiona
354
354
  on throw, every read keeps serving the last-good compile. Don't open a session
355
355
  per export, and don't re-open per edit: `update` instead.
356
356
 
357
+ Both take the render date as an optional last argument (`YYYY-MM-DD`, default
358
+ the local date): what a `today` date field and a plate's `datetime.today()`
359
+ render as. A session keeps the date it opened with, so a preview left open past
360
+ midnight renders yesterday's until it is reopened.
361
+
357
362
  A document that compiles to zero pages still produces a valid session
358
- (`pageCount === 0`); `paint(ctx, 0)` and `pageSize(0)` then throw. Branch on
363
+ (`pageCount === 0`); `paint(ctx, 0, scale)` and `pageSize(0)` then throw. Branch on
359
364
  `pageCount === 0` to render a "no pages to preview" UI rather than relying on
360
365
  the throw.
361
366
 
362
367
  Their `warnings` differ in reach. `engine.render` returns one list for the
363
- whole pipeline: `doc.warnings` (parse, `conform::*`,
364
- `plate::unsupported_construct`) ahead of the compile's own. A session outlives
368
+ whole pipeline: `doc.warnings` (parse and `conform::*`), then every
369
+ `quill.validate(doc)` warning, then the compile's own. A session outlives
365
370
  the document it opened from, so `session.render` and `session.warnings` carry
366
- the compile half alone — read `doc.warnings` beside them.
371
+ the compile's alone — read `doc.warnings` and `quill.validate(doc)` beside them.
367
372
 
368
373
  ### Canvas Preview
369
374
 
370
- `session.paint(ctx, page, opts?)` rasterizes a page directly into a
375
+ `session.paint(ctx, page, scale)` rasterizes a page directly into a
371
376
  `CanvasRenderingContext2D` (main thread) or
372
377
  `OffscreenCanvasRenderingContext2D` (Worker), skipping PNG/SVG byte
373
378
  round-trips.
374
379
 
375
- The painter owns `canvas.width` / `canvas.height`: it sizes the backing
376
- store itself. Consumers own `canvas.style.*` (or the layout system that
377
- sets them) and read `layoutWidth` / `layoutHeight` from the returned
378
- `PaintResult`.
380
+ `scale` is backing-store pixels per point. The painter owns `canvas.width` /
381
+ `canvas.height`, reducing `scale` where it must so neither exceeds 16384 px;
382
+ consumers own `canvas.style.*`. A canvas styled to fill its page box
383
+ needs nothing back from the paint:
379
384
 
380
385
  ```ts
381
- const result = session.paint(canvas.getContext("2d"), 0, {
382
- layoutScale: 1, // layout px per pt (page geometry unit)
383
- densityScale: window.devicePixelRatio, // backing-store density
384
- });
385
-
386
- canvas.style.width = `${result.layoutWidth}px`;
387
- canvas.style.height = `${result.layoutHeight}px`;
386
+ canvas.style.width = "100%"; // the box sets the display size
387
+ const cssPxPerPt = canvas.clientWidth / session.pageSize(0).widthPt;
388
+ if (cssPxPerPt > 0) { // 0 while the canvas has no layout box
389
+ session.paint(canvas.getContext("2d")!, 0, cssPxPerPt * window.devicePixelRatio);
390
+ }
388
391
  ```
389
392
 
390
- - `layoutScale` sets the display-box size (`layoutWidth = widthPt * layoutScale`);
391
- fold `devicePixelRatio`, in-app zoom, and `visualViewport.scale` into
392
- `densityScale`. Their product is the rasterization scale, clamped at 16384 px
393
- per side (`result.clamped`, `result.effectiveDensityScale`).
393
+ - Fold `devicePixelRatio`, in-app zoom, and `visualViewport.scale` into `scale`.
394
+ - A canvas with no layout box (`display: none` on it or an ancestor, detached,
395
+ in a zero-width container) has `clientWidth` 0, and a 0 scale throws
396
+ `backend::invalid_raster_scale`: skip the paint, and paint when a
397
+ `ResizeObserver` reports a width.
394
398
  - `paint` writes the whole backing store with `putImageData`, which ignores the
395
399
  2D context transform, `globalAlpha`, and clip. Give each visible page its own
396
400
  `<canvas>`: no compositing, sub-rect, or transform reaches through `paint`.
@@ -402,33 +406,28 @@ canvas.style.height = `${result.layoutHeight}px`;
402
406
  cache them between committed `update`s only. After one, the count is
403
407
  `ChangeSet.pageCount`, and every page in `ChangeSet.dirtyPages` needs its
404
408
  `pageSize` re-read.
405
- - In a Worker, pass an `OffscreenCanvasRenderingContext2D`; the layout
406
- dimensions are informational there. Loading the WASM module inside the Worker
407
- is the host's responsibility.
409
+ - In a Worker, pass an `OffscreenCanvasRenderingContext2D`. An
410
+ `OffscreenCanvas` has no layout box to measure, so the main thread posts the
411
+ scale. Loading the WASM module inside the Worker is the host's
412
+ responsibility.
408
413
  - `paint` / `pageSize` throw on a page the compile does not have, a zero-page
409
414
  compile included, naming the index and the `pageCount` that excludes it. That
410
415
  throw is the whole contract: open the session and handle it.
411
416
 
412
417
  ### Schema model
413
418
 
414
- A field carries two independent axes, and no `required` one.
419
+ A field declares no `required` key: nothing is required.
415
420
 
416
- **Value** — what the cell holds. With a `default:`, `quill.blueprint` renders
417
- that value under a type-only `# <type>` annotation and the render path uses it
418
- when the document omits the field. Without one, an `example` takes the cell as
419
- a suggested value, and an absent field blank-fills.
421
+ **`default:`** — what an unanswered field renders. With one, `quill.blueprint`
422
+ renders that value under a type-only `# <type>` annotation and the render path
423
+ uses it when the document omits the field. Without one, the blueprint leaves the
424
+ cell empty and an absent field blank-fills.
420
425
 
421
- **Obligation** — whether a human must author the field, read off `default:`'s
422
- absence: a defaulted field asks nobody, a defaultless one asks. An obliged
423
- field carries the `!must_fill` marker in `quill.blueprint`, and
424
- `quill.validate(doc)` emits the non-fatal `validation::must_fill` warning while
425
- the document leaves it unauthored — from either of two triggers, named by the
426
- diagnostic's `trigger` arg: `marker` for a marker the document still carries,
427
- `unauthored` for a cell the schema obliges and the document never filled.
428
- Authoring the field's blank discharges the obligation; clearing the key does
429
- not.
426
+ **`example:`** — the schema's illustration of the field's shape.
427
+ `quill.blueprint` shows it on a `# e.g.` line above the field; it never takes a
428
+ cell and never renders.
430
429
 
431
- Neither axis gates render. Partial documents are accepted, and
430
+ An unanswered field draws no diagnostic. Partial documents are accepted, and
432
431
  `engine.render(quill, doc)` throws only for malformed input.
433
432
 
434
433
  ### Errors
@@ -516,7 +515,7 @@ in a browser that hasn't shipped it:
516
515
  const session = await engine.open(quill, doc);
517
516
  try {
518
517
  for (let p = 0; p < session.pageCount; p++) {
519
- session.paint(ctx, p);
518
+ session.paint(canvases[p].getContext("2d")!, p, scale);
520
519
  }
521
520
  } finally {
522
521
  session.free();