@quillmark/wasm 0.109.0 → 0.110.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,64 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.110.0 - 2026-08-26
4
+
5
+ - **breaking** wasm: **the seam spells a container's `instance`, so the read
6
+ type can require it and every write lane reports an omission.** 0.109 gave
7
+ `Container` the discriminator that tells one container from an adjacent
8
+ sibling of identical shape, and left it an obligation no checker asked for;
9
+ the op lane got a requirement, the whole-`Content` lane — `overwrite`,
10
+ `CardInput.body`, the one a codec flattening a tree writes through — did not.
11
+ It could not: one encoder served storage and the bindings, writing the key
12
+ only where it was non-zero, and a field a read may omit is a field a write
13
+ cannot require. `serial::to_seam_value` is that encoder with every `instance`
14
+ spelled, and the bindings take it on every lane typed `Content`
15
+ (`reader.getContent{,At}`, `getStored` on a body, `importMarkdown`, `rebase`,
16
+ and the card wire behind `document.main` / `cards` / `card(i)` / `removeCard`
17
+ / `makeCard` / `seedMain` / `seedCard`). `ContentContainer.instance` becomes
18
+ required, `ContentContainerInput` is deleted, and `LineOp.setContainers` takes
19
+ `ContentContainer` — a net-smaller surface than 0.109's. The break is every
20
+ hand-built container literal, which is the one a type checker does report.
21
+ A checker still cannot report a `0` stamped on every run, which is the write
22
+ that welds them: `assignInstances` is the rule, not the type. Storage keeps
23
+ the omission and stored blobs re-encode byte for byte; so do the render lanes,
24
+ and so does what the seam types `unknown` (`getStored` on a field,
25
+ `PayloadItem.value`), which answers with the stored bytes verbatim. Content
26
+ parsed from a stored document is the one shape that needs a cast.
27
+ [Guide](docs/migrations/0.109-to-0.110.md)
28
+ - fix(core): **`Quill::validate` refuses only what the render floor refuses.**
29
+ Validation ran its own read-side type dispatch over the authored value while
30
+ the render door validated the *coerced* one, so five of seven types had values
31
+ that rendered and were fatally `validation::type_mismatch` at once — a bare
32
+ scalar for an `array`, `"3"` for an `integer`, `1` for a `boolean`, a
33
+ length-1 array for a `string` or `date`. `airmark`'s `usaf_memo@0.2` declares
34
+ `letterhead_caption` as an `array`, and the bare scalar a starter template
35
+ spells it with — a valid spelling of a one-element list — audited as fatally
36
+ invalid across every document seeded from it, each rendering correctly.
37
+
38
+ `validate_value` now conforms each document value through `conform_value` at
39
+ `Leniency::Render` before judging it, so a type has one predicate and **a
40
+ fatal `validation::*` diagnostic means the document does not render**.
41
+ Conforming runs per node, so one refused element no longer mistypes its
42
+ siblings: `counts: [true, "abc"]` under `integer` items is one mismatch, at
43
+ `counts[1]`. Two consequences for a consumer routing on codes: a value the
44
+ floor adopts raises nothing where it previously raised
45
+ `validation::type_mismatch`, and a bare scalar the floor stringifies into an
46
+ `enum` field is now domain-checked on that string, so `grade: 5` against
47
+ `values: [alpha, beta]` is `validation::enum_violation` where it was
48
+ previously silent — the diagnostic the render door already raised. Schema
49
+ literals (`example:`, `default:`) stay strict.
50
+
51
+ ## v0.109.1 - 2026-08-24
52
+
53
+ - fix(typst): **a `field-region` claim around inline content no longer widens
54
+ the line.** Each of the helper's two bracketing markers was written
55
+ `#metadata(..) <__qm_region__>`, and the space before the label survived into
56
+ the inline flow. It cost 2.715pt per marker at a 12pt body size, so a claim
57
+ on a date or any mid-paragraph composition shifted the text around it by
58
+ 5.43pt, against the layout-neutral contract the helper documents.
59
+ `form-field`'s marker carries the same shape and drops the space too, rather
60
+ than keep depending on the `box` that follows it.
61
+
3
62
  ## v0.109.0 - 2026-08-24
4
63
 
5
64
  - **breaking** content: **`Normalized` is the precondition the projections
@@ -159,8 +218,18 @@
159
218
  that needs no discriminator — nearly all of them — keeps its exact bytes and
160
219
  its content hash. **Breaking for Rust consumers** that match `Container`
161
220
  exhaustively: `Quote` is now a struct variant, and `ListItem`/`Unknown` carry
162
- the extra field. On the TypeScript surface `instance` is optional; a consumer
163
- that never writes adjacent same-shape siblings needs no change.
221
+ the extra field.
222
+
223
+ On the TypeScript surface `instance` is optional — the wire omits a zero, so a
224
+ read shape cannot require it — and nothing there stops compiling. A consumer
225
+ that only reads needs no change. **A consumer that writes container paths owes
226
+ the field**, which is every codec flattening a tree: two `bullet_list` nodes in
227
+ a row are adjacent same-shape siblings, and omitting the discriminator lands
228
+ them welded. Such a host keeps producing what it produced on 0.108, so the
229
+ four defects above stay open for it until its codec stamps the discriminator.
230
+ Nothing reports the omission. Two adjacent lines with equal paths are one
231
+ container, which is also how a two-paragraph quote is spelled: the model
232
+ cannot tell a boundary a writer meant from one it did not.
164
233
 
165
234
  The block census counts what the projections see, so two adjacent runs of one
166
235
  shape now count two where they counted one: a quill declining `list` or
@@ -170,10 +239,13 @@
170
239
  A blob written here carries `instance` only where a document holds adjacent
171
240
  same-shape siblings, and a reader that predates the field ignores the key —
172
241
  so such a blob loads on 0.108 with the two runs welded, and re-saving there
173
- drops the boundary for good. The `@0.93.0` tag is unchanged because every
174
- blob written before this release re-encodes byte for byte; the forward
175
- direction is the one that costs, and only for the documents that spend the
176
- key.
242
+ drops the boundary for good. A 0.109 host whose codec drops the key on
243
+ write-back loses it the same way, with no version skew involved. The boundary
244
+ is written by whatever produced the row `from_markdown`, the CLI, any Rust
245
+ caller — and survives only as far as the next writer that carries it.
246
+ The `@0.93.0` tag is unchanged because every blob written before this release
247
+ re-encodes byte for byte; the forward direction is the one that costs, and
248
+ only for the documents that spend the key.
177
249
 
178
250
  - fix(blueprint): **a variant's `object` or `array<object>` cell expands per
179
251
  property.** The cell went through the scalar path, so it rendered as
@@ -248,25 +320,6 @@
248
320
  `island_to_value`, now emit their own keys in a different order. An unknown's `attrs` bag is
249
321
  untouched, as in 0.99, and nothing hashes the op wire.
250
322
 
251
- <!-- seed: commits since v0.108.3, confirm the entries above cover them, then delete this comment
252
- - docs: the 0.108 → 0.109 release record, and two misattached doc comments
253
- - docs: dense-prose pass over the branch
254
- - docs: dense-prose pass over the branch
255
- - fix: close the rest of the normalized-is-not-validated gap
256
- - fix(typst): open every block at the enclosing list indent
257
- - fix(content): walk export's block tree on a frame stack
258
- - docs: dense-prose pass over the branch
259
- - fix: hold the token's guarantee on error paths, and walk census iteratively
260
- - feat(content): make "normalized" a type the projections require
261
- - refactor: consume the shared container traversal
262
- - feat(content): one traversal for container runs and items
263
- - fix(content): an item boundary is a parent boundary
264
- - fix(content): clear a `continues` that crosses a container boundary
265
- - fix(content): give a container the instance its identity was missing
266
- - Bulk PR integration: #1347–#1356 (#1358)
267
- -->
268
-
269
-
270
323
  ## v0.108.3 - 2026-08-21
271
324
 
272
325
  - fix(typst): **a paragraph holding one bare `/` renders instead of failing the
@@ -95,16 +95,32 @@ export type ContentLineKind =
95
95
  * `ContentLine.kind`: an unrecognized container round-trips with opaque `attrs`
96
96
  * and renders transparently (its lines sit at the enclosing level).
97
97
  *
98
- * Two adjacent lines sit in the same container iff their whole path matches.
98
+ * Two adjacent lines sit in the same container iff their whole path matches, so
99
99
  * `instance` is what tells one container from an adjacent sibling of identical
100
- * shape — two consecutive quotes, or two consecutive lists — which contiguity
101
- * alone reads as one. Omit it (or write `0`) unless a path immediately above or
102
- * below is otherwise identical; a write is canonicalized to `0`/`1` on the way
103
- * in, so any distinct pair of values works. */
100
+ * shape — two consecutive quotes, two consecutive lists — which contiguity
101
+ * alone reads as one.
102
+ *
103
+ * **A writer owes a distinct value per adjacent sibling run**, not merely a
104
+ * value. Runs of one shape sharing one arrive as one: a second list's items come
105
+ * back as continuation paragraphs of the first, markers gone. The field is
106
+ * required, so a checker reports the omission; it cannot report a `0` stamped on
107
+ * both, which is the same write. A codec flattening a tree takes them from
108
+ * `assignInstances` in `@quillmark/wasm/runtime` rather than by hand. Any
109
+ * distinct pair works; a write is canonicalized to `0`/`1`.
110
+ *
111
+ * Reading is not the mirror of writing. Every read spells the field, the `0` on
112
+ * a container with nothing to be told apart from included. A read also carries a
113
+ * discriminator on pairs no writer had to spell: `1.` beside a list starting at
114
+ * `3` differs by `start`, so those runs arrive apart with nothing written, and
115
+ * the canonical form spends one anyway because Markdown reads only a list's
116
+ * first number.
117
+ *
118
+ * Content parsed from a stored document is the one shape that arrives without
119
+ * it — storage omits a zero — and needs a cast. */
104
120
  export type ContentContainer =
105
- | { container: "list_item"; ordered: boolean; start: number; ordinal: number; instance?: number }
106
- | { container: "quote"; instance?: number }
107
- | { container: string; attrs: unknown; instance?: number };
121
+ | { container: "list_item"; ordered: boolean; start: number; ordinal: number; instance: number }
122
+ | { container: "quote"; instance: number }
123
+ | { container: string; attrs: unknown; instance: number };
108
124
 
109
125
  /** A mark over char range `[start, end)` into `Content.text`. The open `type`
110
126
  * arm blocks discriminant narrowing, so read a payload-carrying arm behind its
@@ -793,6 +809,10 @@ export class Document {
793
809
  * is at rest; one from the transport door may rest as authored until it is
794
810
  * conformed, and this read reports what is there. For the `Content` either
795
811
  * way use `reader.getContent`.
812
+ *
813
+ * The body arm is typed `Content` and answers in the seam form, spelling
814
+ * every `ContentContainer.instance`. A field arm echoes the stored bytes,
815
+ * which omit a zero: verbatim is the contract, and is why it is `unknown`.
796
816
  */
797
817
  getStored(addr: Addr | string): unknown;
798
818
  /**
@@ -608,6 +608,10 @@ export class Document {
608
608
  * is at rest; one from the transport door may rest as authored until it is
609
609
  * conformed, and this read reports what is there. For the `Content` either
610
610
  * way use `reader.getContent`.
611
+ *
612
+ * The body arm is typed `Content` and answers in the seam form, spelling
613
+ * every `ContentContainer.instance`. A field arm echoes the stored bytes,
614
+ * which omit a zero: verbatim is the contract, and is why it is `unknown`.
611
615
  * @param {Addr | string} addr
612
616
  * @returns {unknown}
613
617
  */
Binary file
@@ -95,16 +95,32 @@ export type ContentLineKind =
95
95
  * `ContentLine.kind`: an unrecognized container round-trips with opaque `attrs`
96
96
  * and renders transparently (its lines sit at the enclosing level).
97
97
  *
98
- * Two adjacent lines sit in the same container iff their whole path matches.
98
+ * Two adjacent lines sit in the same container iff their whole path matches, so
99
99
  * `instance` is what tells one container from an adjacent sibling of identical
100
- * shape — two consecutive quotes, or two consecutive lists — which contiguity
101
- * alone reads as one. Omit it (or write `0`) unless a path immediately above or
102
- * below is otherwise identical; a write is canonicalized to `0`/`1` on the way
103
- * in, so any distinct pair of values works. */
100
+ * shape — two consecutive quotes, two consecutive lists — which contiguity
101
+ * alone reads as one.
102
+ *
103
+ * **A writer owes a distinct value per adjacent sibling run**, not merely a
104
+ * value. Runs of one shape sharing one arrive as one: a second list's items come
105
+ * back as continuation paragraphs of the first, markers gone. The field is
106
+ * required, so a checker reports the omission; it cannot report a `0` stamped on
107
+ * both, which is the same write. A codec flattening a tree takes them from
108
+ * `assignInstances` in `@quillmark/wasm/runtime` rather than by hand. Any
109
+ * distinct pair works; a write is canonicalized to `0`/`1`.
110
+ *
111
+ * Reading is not the mirror of writing. Every read spells the field, the `0` on
112
+ * a container with nothing to be told apart from included. A read also carries a
113
+ * discriminator on pairs no writer had to spell: `1.` beside a list starting at
114
+ * `3` differs by `start`, so those runs arrive apart with nothing written, and
115
+ * the canonical form spends one anyway because Markdown reads only a list's
116
+ * first number.
117
+ *
118
+ * Content parsed from a stored document is the one shape that arrives without
119
+ * it — storage omits a zero — and needs a cast. */
104
120
  export type ContentContainer =
105
- | { container: "list_item"; ordered: boolean; start: number; ordinal: number; instance?: number }
106
- | { container: "quote"; instance?: number }
107
- | { container: string; attrs: unknown; instance?: number };
121
+ | { container: "list_item"; ordered: boolean; start: number; ordinal: number; instance: number }
122
+ | { container: "quote"; instance: number }
123
+ | { container: string; attrs: unknown; instance: number };
108
124
 
109
125
  /** A mark over char range `[start, end)` into `Content.text`. The open `type`
110
126
  * arm blocks discriminant narrowing, so read a payload-carrying arm behind its
@@ -793,6 +809,10 @@ export class Document {
793
809
  * is at rest; one from the transport door may rest as authored until it is
794
810
  * conformed, and this read reports what is there. For the `Content` either
795
811
  * way use `reader.getContent`.
812
+ *
813
+ * The body arm is typed `Content` and answers in the seam form, spelling
814
+ * every `ContentContainer.instance`. A field arm echoes the stored bytes,
815
+ * which omit a zero: verbatim is the contract, and is why it is `unknown`.
796
816
  */
797
817
  getStored(addr: Addr | string): unknown;
798
818
  /**
@@ -608,6 +608,10 @@ export class Document {
608
608
  * is at rest; one from the transport door may rest as authored until it is
609
609
  * conformed, and this read reports what is there. For the `Content` either
610
610
  * way use `reader.getContent`.
611
+ *
612
+ * The body arm is typed `Content` and answers in the seam form, spelling
613
+ * every `ContentContainer.instance`. A field arm echoes the stored bytes,
614
+ * which omit a zero: verbatim is the contract, and is why it is `unknown`.
611
615
  * @param {Addr | string} addr
612
616
  * @returns {unknown}
613
617
  */
Binary file
package/core/wasm.d.ts CHANGED
@@ -95,16 +95,32 @@ export type ContentLineKind =
95
95
  * `ContentLine.kind`: an unrecognized container round-trips with opaque `attrs`
96
96
  * and renders transparently (its lines sit at the enclosing level).
97
97
  *
98
- * Two adjacent lines sit in the same container iff their whole path matches.
98
+ * Two adjacent lines sit in the same container iff their whole path matches, so
99
99
  * `instance` is what tells one container from an adjacent sibling of identical
100
- * shape — two consecutive quotes, or two consecutive lists — which contiguity
101
- * alone reads as one. Omit it (or write `0`) unless a path immediately above or
102
- * below is otherwise identical; a write is canonicalized to `0`/`1` on the way
103
- * in, so any distinct pair of values works. */
100
+ * shape — two consecutive quotes, two consecutive lists — which contiguity
101
+ * alone reads as one.
102
+ *
103
+ * **A writer owes a distinct value per adjacent sibling run**, not merely a
104
+ * value. Runs of one shape sharing one arrive as one: a second list's items come
105
+ * back as continuation paragraphs of the first, markers gone. The field is
106
+ * required, so a checker reports the omission; it cannot report a `0` stamped on
107
+ * both, which is the same write. A codec flattening a tree takes them from
108
+ * `assignInstances` in `@quillmark/wasm/runtime` rather than by hand. Any
109
+ * distinct pair works; a write is canonicalized to `0`/`1`.
110
+ *
111
+ * Reading is not the mirror of writing. Every read spells the field, the `0` on
112
+ * a container with nothing to be told apart from included. A read also carries a
113
+ * discriminator on pairs no writer had to spell: `1.` beside a list starting at
114
+ * `3` differs by `start`, so those runs arrive apart with nothing written, and
115
+ * the canonical form spends one anyway because Markdown reads only a list's
116
+ * first number.
117
+ *
118
+ * Content parsed from a stored document is the one shape that arrives without
119
+ * it — storage omits a zero — and needs a cast. */
104
120
  export type ContentContainer =
105
- | { container: "list_item"; ordered: boolean; start: number; ordinal: number; instance?: number }
106
- | { container: "quote"; instance?: number }
107
- | { container: string; attrs: unknown; instance?: number };
121
+ | { container: "list_item"; ordered: boolean; start: number; ordinal: number; instance: number }
122
+ | { container: "quote"; instance: number }
123
+ | { container: string; attrs: unknown; instance: number };
108
124
 
109
125
  /** A mark over char range `[start, end)` into `Content.text`. The open `type`
110
126
  * arm blocks discriminant narrowing, so read a payload-carrying arm behind its
@@ -609,6 +625,10 @@ export class Document {
609
625
  * is at rest; one from the transport door may rest as authored until it is
610
626
  * conformed, and this read reports what is there. For the `Content` either
611
627
  * way use `reader.getContent`.
628
+ *
629
+ * The body arm is typed `Content` and answers in the seam form, spelling
630
+ * every `ContentContainer.instance`. A field arm echoes the stored bytes,
631
+ * which omit a zero: verbatim is the contract, and is why it is `unknown`.
612
632
  */
613
633
  getStored(addr: Addr | string): unknown;
614
634
  /**
package/core/wasm.js CHANGED
@@ -608,6 +608,10 @@ export class Document {
608
608
  * is at rest; one from the transport door may rest as authored until it is
609
609
  * conformed, and this read reports what is there. For the `Content` either
610
610
  * way use `reader.getContent`.
611
+ *
612
+ * The body arm is typed `Content` and answers in the seam form, spelling
613
+ * every `ContentContainer.instance`. A field arm echoes the stored bytes,
614
+ * which omit a zero: verbatim is the contract, and is why it is `unknown`.
611
615
  * @param {Addr | string} addr
612
616
  * @returns {unknown}
613
617
  */
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.109.0",
3
+ "version": "0.110.0",
4
4
  "description": "WebAssembly bindings for Quillmark, a schema-driven document engine",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -245,7 +245,7 @@ export declare function isListItemContainer(
245
245
  ordered: boolean;
246
246
  start: number;
247
247
  ordinal: number;
248
- instance?: number;
248
+ instance: number;
249
249
  };
250
250
 
251
251
  // The guards above answer "is this arm X". These four answer "is this a value
@@ -278,6 +278,41 @@ export declare function isUnknownIsland(
278
278
  island: ContentIsland
279
279
  ): island is ContentIsland & { type: string; props: unknown };
280
280
 
281
+ // `ContentContainer.instance` is required, so a checker reports an omission; it
282
+ // cannot report a `0` stamped on every run, which is the same write. Adjacent
283
+ // runs of one shape sharing a value arrive welded, and nothing reports that
284
+ // either: the flat `containers` form cannot tell it from one container spanning
285
+ // two paragraphs. This carries the rule a codec would otherwise re-derive.
286
+
287
+ /**
288
+ * Stamp `instance` across one parent's blocks at one depth, in document order,
289
+ * returning containers ready to write.
290
+ *
291
+ * One entry per container RUN — a list, not a list item — and `null` for a
292
+ * block carrying no container at this depth. A bare paragraph between two lists
293
+ * is such a block, and separates them on its own. Every line of a run then
294
+ * carries that run's returned container, `ordinal` varying per item and
295
+ * `instance` held.
296
+ *
297
+ * The `instance` it stamps is canonical, so a document reads back the value it
298
+ * was written. `ordinal` stays the caller's, and a write is renumbered to a
299
+ * gapless index within its run.
300
+ *
301
+ * Which fields decide a weld is coarser than equality for a list: CommonMark
302
+ * reads only a list's first number, so `1. a` beside `3. b` welds despite the
303
+ * differing `start`.
304
+ *
305
+ * ```js
306
+ * const [outer, , inner] = assignInstances([listA, null, listB]);
307
+ * // outer.instance === 0, inner.instance === 0 — the paragraph parts them
308
+ * const [a, b] = assignInstances([listA, listB]);
309
+ * // a.instance === 0, b.instance === 1 — adjacent, one shape
310
+ * ```
311
+ */
312
+ export declare function assignInstances(
313
+ runs: (ContentContainer | null)[]
314
+ ): (ContentContainer | null)[];
315
+
281
316
  // The backend-neutral render contract, defined here rather than re-exported from
282
317
  // one private backend because no single backend owns the canonical API's types.
283
318
  // Every backend build must satisfy these shapes; `runtime.types.test-d.ts` keeps
@@ -510,6 +510,64 @@ export function isUnknownIsland(island) {
510
510
  return typeof island?.type === 'string' && !KNOWN_ISLAND_TYPES.has(island.type);
511
511
  }
512
512
 
513
+ // ── Container run boundaries ────────────────────────────────────────────────
514
+ // `ContentContainer.instance` is what keeps two adjacent runs of one shape
515
+ // apart, and only a writer knows where a boundary is: the flat `containers`
516
+ // form cannot tell a list ending beside another from one list of two items, so
517
+ // an omitted discriminator welds them and nothing reports it.
518
+ //
519
+ // WELD_KEYS is the rule `Container::same_weld` owns upstream: which fields two
520
+ // adjacent runs must share for the markdown projection to read them as one, and
521
+ // therefore for the canonical form to have to spend a discriminator. `start` is
522
+ // not among them, since CommonMark reads only a list's first number. A table
523
+ // rather than a switch, so `tests/known_names_drift.rs` can pin it against the
524
+ // Rust predicate.
525
+
526
+ const WELD_KEYS = { list_item: ['ordered'], quote: [] };
527
+
528
+ function sameJson(a, b) {
529
+ if (a === b) return true;
530
+ if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null) return false;
531
+ if (Array.isArray(a) !== Array.isArray(b)) return false;
532
+ const ka = Object.keys(a);
533
+ return (
534
+ ka.length === Object.keys(b).length &&
535
+ ka.every((k) => Object.hasOwn(b, k) && sameJson(a[k], b[k]))
536
+ );
537
+ }
538
+
539
+ /**
540
+ * @param {import('../core/wasm.js').ContentContainer} a
541
+ * @param {import('../core/wasm.js').ContentContainer} b
542
+ * @returns {boolean}
543
+ */
544
+ function weldsWith(a, b) {
545
+ // A malformed value welds with nothing. The membership guards' posture:
546
+ // answer rather than throw.
547
+ if (typeof a?.container !== 'string' || a.container !== b?.container) return false;
548
+ // `hasOwn`, so a tag colliding with an `Object.prototype` member reaches the
549
+ // unknown branch rather than a function.
550
+ if (!Object.hasOwn(WELD_KEYS, a.container)) return sameJson(a.attrs, b.attrs);
551
+ return WELD_KEYS[a.container].every((k) => a[k] === b[k]);
552
+ }
553
+
554
+ /**
555
+ * @param {(import('../core/wasm.js').ContentContainer | null)[]} runs
556
+ * @returns {(import('../core/wasm.js').ContentContainer | null)[]}
557
+ */
558
+ export function assignInstances(runs) {
559
+ let prev = null;
560
+ return runs.map((run) => {
561
+ if (run == null) {
562
+ prev = null;
563
+ return null;
564
+ }
565
+ const instance = prev && weldsWith(prev, run) ? 1 - prev.instance : 0;
566
+ prev = { ...run, instance };
567
+ return prev;
568
+ });
569
+ }
570
+
513
571
  /**
514
572
  * Build a `load` thunk: dynamic-import a backend build, then instantiate it.
515
573
  *