@quillmark/quiver 0.3.0 → 0.4.1

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/PROGRAM.md CHANGED
@@ -83,27 +83,15 @@ Unknown fields in `Quiver.yaml` are a **validation error** (`quiver_invalid`). S
83
83
  - "Transport" is not a first-class concept; HTTP fetching is an internal
84
84
  detail of `fromBuilt`
85
85
 
86
- ### 4) Multi-Quiver Composition with Deterministic Precedence
86
+ ### 4) Single-Quiver Scope (V1)
87
87
 
88
- `QuiverRegistry` accepts multiple quivers with explicit order.
88
+ V1 has no multi-quiver composition layer. Each `Quiver` instance is
89
+ independent: consumers load one quiver and call `resolve` / `getQuill` /
90
+ `warm` directly on it. There is no `QuiverRegistry`.
89
91
 
90
- This registry/composition layer lives entirely in `@quillmark/quiver`; it is not a Quillmark engine feature.
91
-
92
- Precedence rule:
93
-
94
- - **Precedence is a hard filter**
95
- - Scan quivers in order
96
- - First quiver with any matching candidate wins
97
- - Choose highest matching version **within that quiver**
98
-
99
- Applies to:
100
-
101
- - unqualified refs (e.g. `usaf_memo`)
102
- - selector refs (e.g. `usaf_memo@1.2`)
103
-
104
- No global highest-across-all-quivers behavior.
105
-
106
- **Identity collision:** duplicate `Quiver.yaml.name` across composed quivers is an error in V1.
92
+ If composition becomes a real use case, the additive path is a thin
93
+ `Quiver.compose([a, b, ...])` factory that returns a quiver-shaped
94
+ composite — it is intentionally out of scope for V1.
107
95
 
108
96
  ### 5) Semver Selector Rules Are Strict and Small
109
97
 
@@ -129,20 +117,35 @@ Canonical version format:
129
117
 
130
118
  Canonicalization:
131
119
 
132
- - resolve selector to canonical ref once per manifest snapshot
133
- - key internal caches by canonical ref
120
+ - resolve selector to canonical ref once per call to `getQuill`
121
+ - key the tree cache by canonical ref; key the quill cache by (engine, canonical-ref)
134
122
 
135
123
  ### 6) Warm/Prefetch Is Purely a Quiver Concern
136
124
 
137
- `warm()` remains a quiver-layer optimization:
125
+ `warm()` is the network prefetch step. It fetches every quill's tree and
126
+ populates the per-quiver tree cache. It does **not** materialize Quill
127
+ instances and does **not** require an engine — `engine.quill(tree)` is
128
+ microseconds and runs lazily inside `getQuill`. A subsequent `getQuill`
129
+ reuses the cached tree, so the network fetch isn't paid twice.
130
+
131
+ Tree cache lifecycle:
138
132
 
139
- - `warm()` warms all by default in V1
140
- - `resolve()` must work even if nothing is warmed
133
+ - `warm()` populates the tree cache.
134
+ - First `getQuill(ref, { engine })` reads the tree, materializes the
135
+ Quill via `engine.quill(tree)`, then evicts the tree so its bytes can
136
+ be GC'd. The materialized Quill is what's kept (per engine).
137
+ - If `engine.quill` throws, the tree is retained so a retry skips the
138
+ network.
139
+ - Repeated `getQuill` on the same engine hits the per-engine quill cache
140
+ — no tree access at all.
141
+ - A subsequent `getQuill` for a different engine refetches the tree
142
+ (single-engine apps never pay this cost).
141
143
 
142
- Warm means "load/prepare quills and materialize render-ready instances",
143
- not "register in engine". There is no engine registration step anymore.
144
- Warm semantics are identical for source-loaded and built-output-loaded
145
- quivers; the loader hides the difference.
144
+ Other invariants:
145
+
146
+ - `resolve()` works whether or not anything is warmed.
147
+ - Warm semantics are identical for source-loaded and built-output-loaded
148
+ quivers; the loader hides the difference.
146
149
 
147
150
  ### 7) Engine Boundary: New Canonical Contract (Node / JS–WASM only)
148
151
 
@@ -157,14 +160,14 @@ Important implications:
157
160
  - No engine quill registry in the JS binding; no `registerQuill`, `hasQuill`, or engine-level `render(doc)` in Quiver’s flow
158
161
  - Quiver owns mapping from canonical ref → in-memory tree → `Quill` instance
159
162
  - Cache optimization is in-process reuse of `Quill` instances, not registration checks
160
- - Path-based loading (`quill_from_path`) exists in **other** bindings only; in Node, Quiver reads files and assembles `tree` for `engine.quill(tree)` (see upstream `references/quillmark/docs/integration/javascript/api.md`)
163
+ - Path-based loading (`quill_from_path`) exists in **other** bindings only; in Node, Quiver reads files and assembles `tree` for `engine.quill(tree)` (see the upstream `@quillmark/wasm` JS/WASM API docs)
161
164
 
162
165
  For advanced dynamic-asset behavior, defer to Quillmark’s JS/WASM docs; the default integration path here is `engine.quill` + `quill.render`.
163
166
 
164
167
  ### 8) Markdown and Ref Parsing Boundary
165
168
 
166
169
  - Markdown parsing does not require a quill registry: `Document.fromMarkdown(markdown)`
167
- - Quiver owns ref parsing and selector resolution for its own API (`resolve`, `warm`, validation)
170
+ - Quiver owns ref parsing and selector resolution for its own API (`resolve`, `getQuill`, `warm`, validation)
168
171
  - QUILL field is informational at render time; Quiver routes to the intended quill explicitly without mutating the parsed document in V1
169
172
 
170
173
  Upstream behavior note:
@@ -254,13 +257,12 @@ V1 code catalog (closed set):
254
257
  | Code | Fires when |
255
258
  |---|---|
256
259
  | `invalid_ref` | Malformed ref string at `resolve()`/`warm()` boundary (fails `parseQuillRef`) |
257
- | `quill_not_found` | Selector did not match any quill in any composed quiver |
260
+ | `quill_not_found` | Selector did not match any quill in the quiver |
258
261
  | `quiver_invalid` | `Quiver.yaml` or hashed manifest malformed, unknown field, non-canonical version on disk, or font/bundle hash mismatch |
259
262
  | `transport_error` | I/O failure: missing path, HTTP non-2xx, network error, permission error. Wraps underlying cause. |
260
- | `quiver_collision` | Two composed quivers share `Quiver.yaml.name` at registry construction |
261
263
 
262
264
  Notes:
263
- - `quill_not_found` is selector-resolution failure after quiver composition and precedence.
265
+ - `quill_not_found` is selector-resolution failure within a quiver's catalog.
264
266
  - `transport_error` is artifact access failure (filesystem/HTTP/network/permissions), including missing packed files and HTTP 404.
265
267
  - Legacy categories such as `manifest_invalid`, `quill_load_failed`, and `backend_not_found` are folded into `quiver_invalid` or `transport_error` in V1.
266
268
 
@@ -386,43 +388,38 @@ class Quiver {
386
388
 
387
389
  readonly name: string; // from Quiver.yaml
388
390
 
389
- // Read-only introspection and lazy tree access used by QuiverRegistry
390
- // internally; also available for external debugging and tooling.
391
+ // Read-only introspection and lazy tree access; also used internally by
392
+ // resolve/getQuill/warm.
391
393
  quillNames(): string[]; // sorted lex
392
394
  versionsOf(name: string): string[]; // sorted desc
393
395
  loadTree(name: string, version: string): Promise<Map<string, Uint8Array>>;
394
- }
395
- ```
396
-
397
- ```ts
398
- class QuiverRegistry {
399
- constructor(args: { engine: Quillmark; quivers: Quiver[] });
400
396
 
401
397
  // Selector ref -> canonical ref. Throws invalid_ref / quill_not_found.
402
398
  resolve(ref: string): Promise<string>;
403
399
 
404
- // Canonical ref -> render-ready quill handle (materialized via engine.quill(tree), cached in-process).
405
- getQuill(canonicalRef: string): Promise<Quill>;
400
+ // Selector or canonical ref -> render-ready quill handle (materialized via
401
+ // engine.quill(tree), cached per (engine, canonical-ref)).
402
+ getQuill(ref: string, opts: { engine: Quillmark }): Promise<Quill>;
406
403
 
407
- // Warms every ref in every composed quiver. Fail-fast. Zero params in V1.
404
+ // Prefetches every quill tree (network-only; engine not required).
405
+ // Subsequent getQuill calls reuse the cached tree. Fail-fast.
408
406
  warm(): Promise<void>;
409
407
  }
410
408
 
411
409
  class QuiverError extends Error {
412
- code: "invalid_ref" | "quill_not_found" | "quiver_invalid" | "transport_error" | "quiver_collision";
410
+ code: "invalid_ref" | "quill_not_found" | "quiver_invalid" | "transport_error";
413
411
  // plus contextual payload fields
414
412
  }
415
413
  ```
416
414
 
417
- **No render wrapper.** Callers invoke `quill.render(doc, opts?)` (and `quill.open(doc)` when needed) after `resolve()` + `getQuill()`. Quiver never mirrors Quillmark render APIs.
415
+ **No render wrapper.** Callers invoke `quill.render(doc, opts?)` (and `quill.open(doc)` when needed) after `getQuill()`. Quiver never mirrors Quillmark render APIs.
418
416
 
419
- **Internal (not exported):** `QuiverManifest` (runtime shape), `parseQuillRef`, in-flight coalescing state, source-vs-built layout detection.
417
+ **Internal (not exported):** `BuiltManifest` (runtime shape), `parseQuillRef`, in-flight coalescing state, source-vs-built layout detection.
420
418
 
421
419
  Hot-path flow:
422
420
  ```ts
423
421
  const doc = Document.fromMarkdown(md);
424
- const canonicalRef = await registry.resolve(doc.quillRef);
425
- const quill = await registry.getQuill(canonicalRef);
422
+ const quill = await quiver.getQuill(doc.quillRef, { engine });
426
423
  const result = quill.render(doc, { format: "pdf" });
427
424
  ```
428
425
 
@@ -433,8 +430,8 @@ const result = quill.render(doc, { format: "pdf" });
433
430
  **Entrypoints:**
434
431
  - `@quillmark/quiver` (main, browser-safe): `Quiver` class with only
435
432
  `fromBuilt` functional (Node-only loaders/builder throw
436
- `transport_error` if reached in browser), `QuiverRegistry`,
437
- `QuiverError`, `QuillmarkLike`, `QuillLike`, shared types.
433
+ `transport_error` if reached in browser), `QuiverError`,
434
+ `QuillmarkLike`, `QuillLike`, shared types.
438
435
  - `@quillmark/quiver/node`: adds `Quiver.fromPackage`, `Quiver.fromDir`,
439
436
  `Quiver.build` behaviors. Single `Quiver` class — Node-only factories
440
437
  fail fast outside Node.
@@ -444,7 +441,7 @@ const result = quill.render(doc, { format: "pdf" });
444
441
  test runners wire their own loops against the main API.
445
442
 
446
443
  **Dependencies:**
447
- - Peer: `@quillmark/wasm@>=0.59.0-rc.2` with `Quillmark`, `Document.fromMarkdown`, `engine.quill(tree)`, and `quill.render(doc, opts?)` APIs.
444
+ - Peer: `@quillmark/wasm@>=0.59.0` with `Quillmark`, `Document.fromMarkdown`, `engine.quill(tree)`, and `quill.render(doc, opts?)` APIs.
448
445
  - Runtime: `fflate ^0.8.2` for zip read/write (Node + browser)
449
446
  - Dev-only: `node:crypto` (MD5 hashing in `build()` — never reached at runtime)
450
447
  - No test-runner peer dependency; `/testing` uses `node:test` (built-in)
@@ -461,7 +458,7 @@ const result = quill.render(doc, { format: "pdf" });
461
458
  - inter-quiver dependency graph in `Quiver.yaml`
462
459
  - marketplace/discovery service
463
460
  - advanced warm strategies beyond API-compatible hooks
464
- - multi-quiver name-collision soft handling (V1 errors on duplicate `Quiver.yaml.name`)
461
+ - multi-quiver composition (single quiver per consumer in V1)
465
462
 
466
463
  ---
467
464
 
@@ -472,22 +469,11 @@ All V1 planner questions resolved; implementation plan can proceed against the s
472
469
  1. ~~Final `Quiver` interface shape and transport factoring style~~ → Single `Quiver` class, three loaders (`fromPackage`, `fromDir`, `fromBuilt`) + one builder (`build`). Each loader names what it loads; no auto-detection.
473
470
  2. ~~Final `Quiver.yaml` schema and unknown-field policy~~ → See §2: alphanumeric `name` and optional tooling-only `description`. Unknown fields are `quiver_invalid`.
474
471
  3. ~~Canonical ref grammar and parser API contract~~ → Internal `parseQuillRef`, not exported. Selector syntax per §5. Throws `invalid_ref`.
475
- 4. ~~Exact warning policy for shadowed refs across quivers~~ → No warnings in V1. Precedence is a hard filter (§4); duplicate quiver names error as `quiver_collision`.
472
+ 4. ~~Exact warning policy for shadowed refs across quivers~~ → N/A in V1: no multi-quiver composition layer; each `Quiver` instance is independent (§4).
476
473
  5. ~~Validation API shape consolidation~~ → No separate validation API. Validation errors surface as `QuiverError('quiver_invalid')` during load or `build()`.
477
474
  6. ~~Build output directory structure~~ → See "Runtime Artifact Format (normative)".
478
475
  7. ~~Node/browser entrypoint split~~ → See "Package Structure": main + `/node` subpath, single `Quiver` class.
479
- 8. ~~Final exported type names~~ → `Quiver`, `QuiverRegistry`, `QuiverError`. Hot-path entry is `QuiverRegistry.resolve(ref)` + `QuiverRegistry.getQuill(canonicalRef)`.
480
-
481
- ---
482
-
483
- ## References
484
-
485
- Local copies in this repo for `@quillmark/quiver` implementation:
486
-
487
- - `references/quillmark-registry/` — prior `@quillmark/registry` source and patterns to mine or replace
488
- - `references/quillmark/docs/integration/javascript/api.md` — JS/WASM API this package integrates with
489
- - `references/quillmark/prose/designs/WASM.md` — WASM binding shape
490
- - `references/quillmark/prose/taskings/quill_render_api.md` — upstream render API overhaul (cross-binding; use the JS/WASM sections for Node)
476
+ 8. ~~Final exported type names~~ → `Quiver`, `QuiverError`. Hot-path entry is `Quiver.getQuill(ref, { engine })`.
491
477
 
492
478
  ---
493
479
 
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @quillmark/quiver
2
2
 
3
- Quiver registry and build tooling for Quillmark — load, compose, and build collections of quills for rendering with `@quillmark/wasm`.
3
+ Load and build collections of quills for rendering with `@quillmark/wasm`.
4
4
 
5
5
  ## Install
6
6
 
@@ -51,19 +51,27 @@ the main API instead.
51
51
 
52
52
  ```ts
53
53
  import { Quillmark, Document } from "@quillmark/wasm";
54
- import { Quiver, QuiverRegistry } from "@quillmark/quiver/node";
55
-
56
- const quiver = await Quiver.fromPackage("@org/my-quiver");
54
+ import { Quiver } from "@quillmark/quiver/node";
57
55
 
58
56
  const engine = new Quillmark();
59
- const registry = new QuiverRegistry({ engine, quivers: [quiver] });
57
+ const quiver = await Quiver.fromPackage("@org/my-quiver");
60
58
 
61
59
  const doc = Document.fromMarkdown(markdownString);
62
- const canonicalRef = await registry.resolve(doc.quillRef);
63
- const quill = await registry.getQuill(canonicalRef);
60
+ const quill = await quiver.getQuill(doc.quillRef, { engine });
64
61
  const result = quill.render(doc, { format: "pdf" });
65
62
  ```
66
63
 
64
+ `getQuill` accepts both selector refs (`"memo"`, `"memo@1"`) and canonical
65
+ refs (`"memo@1.0.0"`). It resolves the selector, materializes the quill via
66
+ `engine.quill(tree)`, and caches per (engine, canonical-ref). Concurrent
67
+ calls for the same ref share a single load.
68
+
69
+ If you only need the canonical ref (without materializing), use `resolve`:
70
+
71
+ ```ts
72
+ const canonicalRef = await quiver.resolve("memo"); // "memo@1.1.0"
73
+ ```
74
+
67
75
  ## Consuming a quiver (browser)
68
76
 
69
77
  Browsers cannot read the source layout directly, so build at deploy time and
@@ -81,10 +89,10 @@ await Quiver.build(
81
89
 
82
90
  ```ts
83
91
  // browser runtime
84
- import { Quiver, QuiverRegistry } from "@quillmark/quiver";
92
+ import { Quiver } from "@quillmark/quiver";
85
93
 
86
94
  const quiver = await Quiver.fromBuilt("/quivers/my-quiver/");
87
- const registry = new QuiverRegistry({ engine, quivers: [quiver] });
95
+ const quill = await quiver.getQuill(doc.quillRef, { engine });
88
96
  ```
89
97
 
90
98
  ## Advanced: pre-built distribution to a CDN
@@ -101,23 +109,20 @@ await Quiver.build("./my-quiver", "./dist/my-quiver");
101
109
  const quiver = await Quiver.fromBuilt("https://cdn.example.com/quivers/my-quiver/");
102
110
  ```
103
111
 
104
- ## Warm (prefetch all quills)
112
+ ## Warm (prefetch all quill trees)
105
113
 
106
114
  ```ts
107
- await registry.warm();
115
+ await quiver.warm();
108
116
  ```
109
117
 
110
- ## Multi-quiver composition
118
+ `warm()` is network-only: it fetches every quill's tree and caches them.
119
+ It does not require an engine and does not materialize Quill instances —
120
+ that happens lazily on the first `getQuill` call, which is microseconds.
121
+ A subsequent `getQuill` reuses the cached tree, skipping the fetch.
111
122
 
112
- Quivers are scanned in order. The first quiver with any matching candidate wins;
113
- the highest matching version within that quiver is returned.
114
-
115
- ```ts
116
- const registry = new QuiverRegistry({
117
- engine,
118
- quivers: [primaryQuiver, fallbackQuiver],
119
- });
120
- ```
123
+ Once a tree has been turned into a Quill, the cached tree is dropped so
124
+ its bytes can be GC'd the materialized Quill is the runtime artifact.
125
+ Calling `warm()` again refills the tree cache.
121
126
 
122
127
  ## Error handling
123
128
 
@@ -127,7 +132,7 @@ All errors are instances of `QuiverError` with a `code` field.
127
132
  import { QuiverError } from "@quillmark/quiver";
128
133
 
129
134
  try {
130
- const canonicalRef = await registry.resolve("unknown_quill");
135
+ await quiver.resolve("unknown_quill");
131
136
  } catch (err) {
132
137
  if (err instanceof QuiverError) {
133
138
  console.error(err.code); // e.g. "quill_not_found"
@@ -137,7 +142,7 @@ try {
137
142
  }
138
143
  ```
139
144
 
140
- Error codes: `invalid_ref`, `quill_not_found`, `quiver_invalid`, `transport_error`, `quiver_collision`.
145
+ Error codes: `invalid_ref`, `quill_not_found`, `quiver_invalid`, `transport_error`.
141
146
 
142
147
  ## Full specification
143
148
 
@@ -1,18 +1,18 @@
1
1
  /**
2
- * Minimal structural types matching @quillmark/wasm >=0.57.0 (verified against 0.59.0-rc.2).
2
+ * Minimal structural types matching @quillmark/wasm >=0.59.0.
3
3
  *
4
4
  * Shape:
5
5
  * class Quillmark { quill(tree: Map<string, Uint8Array>): Quill }
6
6
  * class Quill { render(doc, opts?): RenderResult; open(doc): RenderSession }
7
7
  *
8
- * Note: as of 0.59.0-rc.2 the first arg to render/open is a `Document` instance
9
- * (from `Document.fromMarkdown(...)`), not the old `ParsedDocument` interface.
10
- * Quiver keeps the arg typed as `unknown` so consumers of either shape (and
11
- * test doubles) satisfy the contract structurally.
8
+ * The first arg to `render`/`open` is a `Document` instance (from
9
+ * `Document.fromMarkdown(...)`). Quiver keeps the arg typed as `unknown` so
10
+ * consumers (and test doubles) satisfy the contract structurally without
11
+ * importing from @quillmark/wasm.
12
12
  *
13
- * These types are INTERNAL — never re-exported from index.ts. They exist so
14
- * registry.ts never imports from @quillmark/wasm directly and so test doubles
15
- * can satisfy the contract without pulling the real WASM module.
13
+ * These types are re-exported from `index.ts` so consumers can type their
14
+ * own engine wrappers / test doubles against them. Quiver itself never
15
+ * imports from @quillmark/wasm directly.
16
16
  *
17
17
  * Call-site note: Quiver never invokes `render` or `open` itself; consumers do
18
18
  * after `getQuill()`. The loose `unknown` parameter typing is intentional.
@@ -1,18 +1,18 @@
1
1
  /**
2
- * Minimal structural types matching @quillmark/wasm >=0.57.0 (verified against 0.59.0-rc.2).
2
+ * Minimal structural types matching @quillmark/wasm >=0.59.0.
3
3
  *
4
4
  * Shape:
5
5
  * class Quillmark { quill(tree: Map<string, Uint8Array>): Quill }
6
6
  * class Quill { render(doc, opts?): RenderResult; open(doc): RenderSession }
7
7
  *
8
- * Note: as of 0.59.0-rc.2 the first arg to render/open is a `Document` instance
9
- * (from `Document.fromMarkdown(...)`), not the old `ParsedDocument` interface.
10
- * Quiver keeps the arg typed as `unknown` so consumers of either shape (and
11
- * test doubles) satisfy the contract structurally.
8
+ * The first arg to `render`/`open` is a `Document` instance (from
9
+ * `Document.fromMarkdown(...)`). Quiver keeps the arg typed as `unknown` so
10
+ * consumers (and test doubles) satisfy the contract structurally without
11
+ * importing from @quillmark/wasm.
12
12
  *
13
- * These types are INTERNAL — never re-exported from index.ts. They exist so
14
- * registry.ts never imports from @quillmark/wasm directly and so test doubles
15
- * can satisfy the contract without pulling the real WASM module.
13
+ * These types are re-exported from `index.ts` so consumers can type their
14
+ * own engine wrappers / test doubles against them. Quiver itself never
15
+ * imports from @quillmark/wasm directly.
16
16
  *
17
17
  * Call-site note: Quiver never invokes `render` or `open` itself; consumers do
18
18
  * after `getQuill()`. The loose `unknown` parameter typing is intentional.
package/dist/errors.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export type QuiverErrorCode = "invalid_ref" | "quill_not_found" | "quiver_invalid" | "transport_error" | "quiver_collision";
1
+ export type QuiverErrorCode = "invalid_ref" | "quill_not_found" | "quiver_invalid" | "transport_error";
2
2
  export declare class QuiverError extends Error {
3
3
  readonly code: QuiverErrorCode;
4
4
  /** Offending ref string, when available. */
package/dist/index.d.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  export { QuiverError } from "./errors.js";
2
2
  export type { QuiverErrorCode } from "./errors.js";
3
3
  export { Quiver } from "./quiver.js";
4
- export { QuiverRegistry } from "./registry.js";
5
4
  export type { BuildOptions } from "./build.js";
6
5
  export type { QuillmarkLike, QuillLike } from "./engine-types.js";
package/dist/index.js CHANGED
@@ -1,4 +1,3 @@
1
1
  // Main browser-safe entrypoint.
2
2
  export { QuiverError } from "./errors.js";
3
3
  export { Quiver } from "./quiver.js";
4
- export { QuiverRegistry } from "./registry.js";
package/dist/quiver.d.ts CHANGED
@@ -5,6 +5,7 @@
5
5
  * (either source-backed or build-output-backed).
6
6
  */
7
7
  import type { BuildOptions } from "./build.js";
8
+ import type { QuillmarkLike, QuillLike } from "./engine-types.js";
8
9
  /** @internal Internal loader strategy: source or build output. */
9
10
  export interface QuiverLoader {
10
11
  loadTree(name: string, version: string): Promise<Map<string, Uint8Array>>;
@@ -74,9 +75,47 @@ export declare class Quiver {
74
75
  * Lazily loads the file tree for a specific quill version.
75
76
  *
76
77
  * Returns `Map<string, Uint8Array>` suitable for `engine.quill(tree)`.
77
- * Does NOT cache the result — caching is the registry's concern.
78
+ * Does NOT cache the result — caching of materialized Quill instances
79
+ * happens in `getQuill`.
78
80
  *
79
81
  * Throws `transport_error` if name/version not in catalog or I/O fails.
80
82
  */
81
83
  loadTree(name: string, version: string): Promise<Map<string, Uint8Array>>;
84
+ /**
85
+ * Resolves a selector ref → canonical ref (e.g. "memo" → "memo@1.1.0").
86
+ *
87
+ * Selector forms: `name`, `name@x`, `name@x.y`, `name@x.y.z`. Picks the
88
+ * highest matching version in this quiver.
89
+ *
90
+ * Throws:
91
+ * - `invalid_ref` if ref fails parseQuillRef
92
+ * - `quill_not_found` if no version matches
93
+ */
94
+ resolve(ref: string): Promise<string>;
95
+ /**
96
+ * Returns a render-ready `Quill` for a ref (selector or canonical).
97
+ *
98
+ * Selector refs (e.g. `"memo"`, `"memo@1"`) are resolved to canonical
99
+ * form first. Materializes via `engine.quill(tree)` on first call;
100
+ * caches per (engine, canonical-ref). Reuses a tree cached by `warm()`
101
+ * (or a previous `getQuill`) so the network fetch isn't paid twice.
102
+ * Concurrent calls for the same ref coalesce into a single load.
103
+ *
104
+ * Throws:
105
+ * - `invalid_ref` if ref is malformed
106
+ * - `quill_not_found` if ref does not match any version in this quiver
107
+ * - propagates I/O errors from loadTree unchanged
108
+ * - propagates engine errors from engine.quill() unchanged
109
+ */
110
+ getQuill(ref: string, opts: {
111
+ engine: QuillmarkLike;
112
+ }): Promise<QuillLike>;
113
+ /**
114
+ * Prefetches the tree for every quill version in this quiver. Fail-fast.
115
+ *
116
+ * Network-bound only — does not materialize Quill instances and does not
117
+ * require an engine. Subsequent `getQuill` calls reuse the cached trees,
118
+ * skipping the fetch. Rejects on the first fetch failure.
119
+ */
120
+ warm(): Promise<void>;
82
121
  }
package/dist/quiver.js CHANGED
@@ -6,10 +6,24 @@
6
6
  */
7
7
  import { QuiverError } from "./errors.js";
8
8
  import { assertNode } from "./assert-node.js";
9
+ import { parseQuillRef } from "./ref.js";
10
+ import { matchesSemverSelector, chooseHighestVersion } from "./semver.js";
9
11
  export class Quiver {
10
12
  name;
11
13
  #catalog;
12
14
  #loader;
15
+ /**
16
+ * Per-engine cache of materialized quills, keyed by canonical ref.
17
+ * WeakMap so engines can be GC'd; Promise values so concurrent
18
+ * getQuill calls coalesce into a single materialization.
19
+ */
20
+ #quillCache = new WeakMap();
21
+ /**
22
+ * Engine-independent cache of fetched trees, keyed by canonical ref.
23
+ * Populated by `warm()` and on first `getQuill` for a ref. Promise
24
+ * values so concurrent fetches coalesce.
25
+ */
26
+ #treeCache = new Map();
13
27
  /**
14
28
  * Private constructor — use static factory methods.
15
29
  * TS prevents external `new Quiver(...)` at compile time.
@@ -118,7 +132,8 @@ export class Quiver {
118
132
  * Lazily loads the file tree for a specific quill version.
119
133
  *
120
134
  * Returns `Map<string, Uint8Array>` suitable for `engine.quill(tree)`.
121
- * Does NOT cache the result — caching is the registry's concern.
135
+ * Does NOT cache the result — caching of materialized Quill instances
136
+ * happens in `getQuill`.
122
137
  *
123
138
  * Throws `transport_error` if name/version not in catalog or I/O fails.
124
139
  */
@@ -129,4 +144,110 @@ export class Quiver {
129
144
  }
130
145
  return this.#loader.loadTree(name, version);
131
146
  }
147
+ /**
148
+ * Resolves a selector ref → canonical ref (e.g. "memo" → "memo@1.1.0").
149
+ *
150
+ * Selector forms: `name`, `name@x`, `name@x.y`, `name@x.y.z`. Picks the
151
+ * highest matching version in this quiver.
152
+ *
153
+ * Throws:
154
+ * - `invalid_ref` if ref fails parseQuillRef
155
+ * - `quill_not_found` if no version matches
156
+ */
157
+ async resolve(ref) {
158
+ const parsed = parseQuillRef(ref);
159
+ const versions = this.#catalog.get(parsed.name);
160
+ if (versions && versions.length > 0) {
161
+ const candidates = parsed.selector === undefined
162
+ ? [...versions]
163
+ : versions.filter((v) => matchesSemverSelector(v, parsed.selector));
164
+ if (candidates.length > 0) {
165
+ // chooseHighestVersion returns null only for empty arrays; candidates is non-empty.
166
+ const winner = chooseHighestVersion(candidates);
167
+ return `${parsed.name}@${winner}`;
168
+ }
169
+ }
170
+ throw new QuiverError("quill_not_found", `No quill found for ref "${ref}" in quiver "${this.name}".`, { ref, quiverName: this.name });
171
+ }
172
+ /**
173
+ * Returns a render-ready `Quill` for a ref (selector or canonical).
174
+ *
175
+ * Selector refs (e.g. `"memo"`, `"memo@1"`) are resolved to canonical
176
+ * form first. Materializes via `engine.quill(tree)` on first call;
177
+ * caches per (engine, canonical-ref). Reuses a tree cached by `warm()`
178
+ * (or a previous `getQuill`) so the network fetch isn't paid twice.
179
+ * Concurrent calls for the same ref coalesce into a single load.
180
+ *
181
+ * Throws:
182
+ * - `invalid_ref` if ref is malformed
183
+ * - `quill_not_found` if ref does not match any version in this quiver
184
+ * - propagates I/O errors from loadTree unchanged
185
+ * - propagates engine errors from engine.quill() unchanged
186
+ */
187
+ async getQuill(ref, opts) {
188
+ const canonicalRef = await this.resolve(ref);
189
+ const engine = opts.engine;
190
+ let perEngine = this.#quillCache.get(engine);
191
+ if (perEngine === undefined) {
192
+ perEngine = new Map();
193
+ this.#quillCache.set(engine, perEngine);
194
+ }
195
+ let entry = perEngine.get(canonicalRef);
196
+ if (entry === undefined) {
197
+ entry = this.#materializeQuill(canonicalRef, engine).catch((err) => {
198
+ perEngine.delete(canonicalRef);
199
+ throw err;
200
+ });
201
+ perEngine.set(canonicalRef, entry);
202
+ }
203
+ return entry;
204
+ }
205
+ /**
206
+ * Internal: load tree (cached) + invoke engine.quill. Errors propagate.
207
+ *
208
+ * On success, evicts the tree from the cache so its bytes can be GC'd —
209
+ * the materialized Quill is the runtime artifact; the tree is dead weight
210
+ * once a Quill exists. On failure, the tree is retained so retries skip
211
+ * the network.
212
+ */
213
+ async #materializeQuill(canonicalRef, engine) {
214
+ const tree = await this.#getTreeCached(canonicalRef);
215
+ const quill = engine.quill(tree);
216
+ this.#treeCache.delete(canonicalRef);
217
+ return quill;
218
+ }
219
+ /**
220
+ * Internal: tree cache reader. On miss, fetches via `loadTree` and stores
221
+ * the in-flight Promise. On rejection, evicts so a retry can succeed.
222
+ */
223
+ async #getTreeCached(canonicalRef) {
224
+ let entry = this.#treeCache.get(canonicalRef);
225
+ if (entry === undefined) {
226
+ const at = canonicalRef.indexOf("@");
227
+ const name = canonicalRef.slice(0, at);
228
+ const version = canonicalRef.slice(at + 1);
229
+ entry = this.loadTree(name, version).catch((err) => {
230
+ this.#treeCache.delete(canonicalRef);
231
+ throw err;
232
+ });
233
+ this.#treeCache.set(canonicalRef, entry);
234
+ }
235
+ return entry;
236
+ }
237
+ /**
238
+ * Prefetches the tree for every quill version in this quiver. Fail-fast.
239
+ *
240
+ * Network-bound only — does not materialize Quill instances and does not
241
+ * require an engine. Subsequent `getQuill` calls reuse the cached trees,
242
+ * skipping the fetch. Rejects on the first fetch failure.
243
+ */
244
+ async warm() {
245
+ const promises = [];
246
+ for (const name of this.quillNames()) {
247
+ for (const version of this.versionsOf(name)) {
248
+ promises.push(this.#getTreeCached(`${name}@${version}`));
249
+ }
250
+ }
251
+ await Promise.all(promises);
252
+ }
132
253
  }
package/dist/testing.d.ts CHANGED
@@ -9,7 +9,7 @@
9
9
  *
10
10
  * import { Quillmark } from "@quillmark/wasm";
11
11
  * import { runQuiverTests } from "@quillmark/quiver/testing";
12
- * const engine = await Quillmark.load();
12
+ * const engine = new Quillmark();
13
13
  * runQuiverTests(import.meta.url, engine);
14
14
  *
15
15
  * Run with `node --test`.
package/dist/testing.js CHANGED
@@ -9,14 +9,13 @@
9
9
  *
10
10
  * import { Quillmark } from "@quillmark/wasm";
11
11
  * import { runQuiverTests } from "@quillmark/quiver/testing";
12
- * const engine = await Quillmark.load();
12
+ * const engine = new Quillmark();
13
13
  * runQuiverTests(import.meta.url, engine);
14
14
  *
15
15
  * Run with `node --test`.
16
16
  */
17
17
  import { describe, it, before } from "node:test";
18
18
  import { Quiver } from "./quiver.js";
19
- import { QuiverRegistry } from "./registry.js";
20
19
  /**
21
20
  * Registers a `node:test` describe block that validates every quill
22
21
  * version in the quiver at `metaUrlOrDir` against the provided engine.
@@ -29,11 +28,9 @@ import { QuiverRegistry } from "./registry.js";
29
28
  */
30
29
  export function runQuiverTests(metaUrlOrDir, engine) {
31
30
  describe("Quiver", () => {
32
- let registry;
33
31
  let quiver;
34
32
  before(async () => {
35
33
  quiver = await Quiver.fromDir(metaUrlOrDir);
36
- registry = new QuiverRegistry({ engine, quivers: [quiver] });
37
34
  });
38
35
  it("has at least one quill", () => {
39
36
  if (quiver.quillNames().length === 0) {
@@ -43,7 +40,7 @@ export function runQuiverTests(metaUrlOrDir, engine) {
43
40
  it("compiles every quill version without error", async () => {
44
41
  for (const name of quiver.quillNames()) {
45
42
  for (const version of quiver.versionsOf(name)) {
46
- const quill = await registry.getQuill(`${name}@${version}`);
43
+ const quill = await quiver.getQuill(`${name}@${version}`, { engine });
47
44
  if (typeof quill.render !== "function") {
48
45
  throw new Error(`${name}@${version}: engine returned non-conforming Quill`);
49
46
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quillmark/quiver",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "description": "Quiver registry and build tooling for Quillmark",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,39 +0,0 @@
1
- import type { QuillmarkLike, QuillLike } from "./engine-types.js";
2
- import type { Quiver } from "./quiver.js";
3
- export declare class QuiverRegistry {
4
- #private;
5
- constructor(args: {
6
- engine: QuillmarkLike;
7
- quivers: Quiver[];
8
- });
9
- /**
10
- * Resolves a selector ref → canonical ref (e.g. "memo" → "memo@1.1.0").
11
- *
12
- * Applies multi-quiver precedence (§4): scan quivers in order, first quiver
13
- * with any matching candidate wins, highest match within that quiver returned.
14
- *
15
- * Throws:
16
- * - `invalid_ref` if ref fails parseQuillRef
17
- * - `quill_not_found` if no quiver has a matching candidate
18
- */
19
- resolve(ref: string): Promise<string>;
20
- /**
21
- * Returns a render-ready QuillLike instance for a canonical ref.
22
- * Materializes via engine.quill(tree) on first call; caches by canonical ref.
23
- *
24
- * Throws:
25
- * - `invalid_ref` if canonicalRef is not valid canonical x.y.z form
26
- * - `quill_not_found` if canonical ref doesn't map to a loaded quiver
27
- * - propagates I/O errors from loadTree unchanged
28
- * - propagates engine errors from engine.quill() unchanged (not wrapped)
29
- */
30
- getQuill(canonicalRef: string): Promise<QuillLike>;
31
- /**
32
- * Warms all quill refs across all composed quivers. Fail-fast.
33
- *
34
- * Calls loadTree + engine.quill(tree) for every known quill version in
35
- * parallel. Already-cached refs resolve instantly (idempotent). Rejects on
36
- * the first failure (Promise.all fail-fast semantics).
37
- */
38
- warm(): Promise<void>;
39
- }
package/dist/registry.js DELETED
@@ -1,115 +0,0 @@
1
- import { QuiverError } from "./errors.js";
2
- import { parseQuillRef } from "./ref.js";
3
- import { matchesSemverSelector, chooseHighestVersion } from "./semver.js";
4
- export class QuiverRegistry {
5
- #engine;
6
- #quivers;
7
- /** Cache: canonical ref → pending or resolved Promise<QuillLike>. */
8
- #cache = new Map();
9
- constructor(args) {
10
- this.#engine = args.engine;
11
- // Validate no two quivers share Quiver.yaml.name → quiver_collision.
12
- const seen = new Map();
13
- for (const quiver of args.quivers) {
14
- const existing = seen.get(quiver.name);
15
- if (existing !== undefined) {
16
- throw new QuiverError("quiver_collision", `Two quivers share the name "${quiver.name}": first quiver and a later quiver both declare this name. ` +
17
- `Quiver names must be unique within a registry.`, { quiverName: quiver.name });
18
- }
19
- seen.set(quiver.name, quiver.name);
20
- }
21
- this.#quivers = Object.freeze([...args.quivers]);
22
- }
23
- /**
24
- * Resolves a selector ref → canonical ref (e.g. "memo" → "memo@1.1.0").
25
- *
26
- * Applies multi-quiver precedence (§4): scan quivers in order, first quiver
27
- * with any matching candidate wins, highest match within that quiver returned.
28
- *
29
- * Throws:
30
- * - `invalid_ref` if ref fails parseQuillRef
31
- * - `quill_not_found` if no quiver has a matching candidate
32
- */
33
- async resolve(ref) {
34
- // Throws invalid_ref on malformed input.
35
- const parsed = parseQuillRef(ref);
36
- for (const quiver of this.#quivers) {
37
- const versions = quiver.versionsOf(parsed.name);
38
- if (versions.length === 0)
39
- continue;
40
- // Filter by selector if present; otherwise all versions are candidates.
41
- const candidates = parsed.selector === undefined
42
- ? versions
43
- : versions.filter((v) => matchesSemverSelector(v, parsed.selector));
44
- if (candidates.length === 0)
45
- continue;
46
- // First quiver with any candidate wins — pick highest within that quiver.
47
- const winner = chooseHighestVersion(candidates);
48
- // chooseHighestVersion returns null only for empty arrays; candidates is non-empty.
49
- return `${parsed.name}@${winner}`;
50
- }
51
- throw new QuiverError("quill_not_found", `No quill found for ref "${ref}" in any registered quiver.`, { ref });
52
- }
53
- /**
54
- * Returns a render-ready QuillLike instance for a canonical ref.
55
- * Materializes via engine.quill(tree) on first call; caches by canonical ref.
56
- *
57
- * Throws:
58
- * - `invalid_ref` if canonicalRef is not valid canonical x.y.z form
59
- * - `quill_not_found` if canonical ref doesn't map to a loaded quiver
60
- * - propagates I/O errors from loadTree unchanged
61
- * - propagates engine errors from engine.quill() unchanged (not wrapped)
62
- */
63
- async getQuill(canonicalRef) {
64
- let entry = this.#cache.get(canonicalRef);
65
- if (entry === undefined) {
66
- entry = this.#loadQuill(canonicalRef).catch((err) => {
67
- this.#cache.delete(canonicalRef);
68
- throw err;
69
- });
70
- this.#cache.set(canonicalRef, entry);
71
- }
72
- return entry;
73
- }
74
- /** Internal: does the actual loading work for getQuill. */
75
- async #loadQuill(canonicalRef) {
76
- // Parse and validate canonical form (must be x.y.z).
77
- const parsed = parseQuillRef(canonicalRef);
78
- if (parsed.selectorDepth !== 3) {
79
- throw new QuiverError("invalid_ref", `getQuill requires a canonical ref (x.y.z) but received "${canonicalRef}". ` +
80
- `Use resolve() first to obtain a canonical ref.`, { ref: canonicalRef });
81
- }
82
- const version = parsed.selector;
83
- // Find the first quiver that owns this exact (name, version) pair.
84
- const owningQuiver = this.#quivers.find((q) => q.versionsOf(parsed.name).includes(version));
85
- if (owningQuiver === undefined) {
86
- throw new QuiverError("quill_not_found", `Quill "${canonicalRef}" was not found in any registered quiver.`, { ref: canonicalRef, version });
87
- }
88
- // Load the file tree — I/O errors propagate as-is.
89
- const tree = await owningQuiver.loadTree(parsed.name, version);
90
- // Materialize the Quill via the engine.
91
- // Engine errors propagate unchanged — they are not QuiverErrors and we
92
- // should not mask them. The caller's error-handling stack will see the
93
- // engine's own error type directly.
94
- const quill = this.#engine.quill(tree);
95
- return quill;
96
- }
97
- /**
98
- * Warms all quill refs across all composed quivers. Fail-fast.
99
- *
100
- * Calls loadTree + engine.quill(tree) for every known quill version in
101
- * parallel. Already-cached refs resolve instantly (idempotent). Rejects on
102
- * the first failure (Promise.all fail-fast semantics).
103
- */
104
- async warm() {
105
- const promises = [];
106
- for (const quiver of this.#quivers) {
107
- for (const name of quiver.quillNames()) {
108
- for (const version of quiver.versionsOf(name)) {
109
- promises.push(this.getQuill(`${name}@${version}`));
110
- }
111
- }
112
- }
113
- await Promise.all(promises);
114
- }
115
- }