@quillmark/quiver 0.2.0 → 0.4.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/PROGRAM.md CHANGED
@@ -18,28 +18,43 @@ This means Quiver must own quill selection and lifecycle entirely. There is no e
18
18
 
19
19
  ## Core Decisions
20
20
 
21
- ### 1) Quiver Shapes and Transports Are Orthogonal
22
-
23
- Two on-disk/data shapes:
24
-
25
- - **Source Quiver** (authoring shape)
26
- - Human-authored, git-friendly
27
- - Root `Quiver.yaml`
28
- - Quills under `quills/<name>/<version>/Quill.yaml`
29
- - Assets in normal source layout
30
- - **Packed Quiver** (distribution/runtime artifact)
31
- - Hashed manifest
32
- - Bundle zips
33
- - Dehydrated shared font store (`store/<md5>`)
34
- - Stable pointer file to current hashed manifest
35
-
36
- Transport is a separate axis for **Packed Quiver** loading:
37
-
38
- - `http` (browser/runtime delivery)
39
- - `fs` (packed artifact loaded from local disk)
40
- - future transports can be added without changing packed format
41
-
42
- **Naming decision:** use `pack()` (format operation), not transport-specific names.
21
+ ### 1) One Authored Shape; A Build Output Derived From It
22
+
23
+ There is one user-facing shape: the **Source Quiver**.
24
+
25
+ - Human-authored, git-friendly
26
+ - Root `Quiver.yaml`
27
+ - Quills under `quills/<name>/<version>/Quill.yaml`
28
+ - Assets in normal source layout
29
+
30
+ A build step (`Quiver.build`) derives a **runtime artifact** from the source.
31
+ This artifact is not a peer "format" — it is the source layout's `dist/`
32
+ output, optimized for browser delivery (hashed manifest, per-quill bundle
33
+ zips, dehydrated shared font store, stable pointer file). Authors do not
34
+ version it, publish it, or check it into git.
35
+
36
+ Loaders are paired 1:1 with what they load:
37
+
38
+ - `Quiver.fromPackage(specifier)` and `Quiver.fromDir(path)` load the
39
+ **source** layout (Node only).
40
+ - `Quiver.fromBuilt(url)` loads the **build output** over HTTP/HTTPS
41
+ (browser-safe; works in Node too).
42
+
43
+ There is no auto-detection: each loader's name commits to what it expects.
44
+
45
+ **Naming decisions:**
46
+ - The verb is `build()`. Pairs with `fromBuilt()` (past participle = "the
47
+ built one"). Avoids collision with `npm pack`, JS bundler vocabulary,
48
+ and the internal "bundle zips" term.
49
+ - Loaders for source layouts are named by **where the source lives**
50
+ (`Package` / `Dir`). The loader for build output is named by **what it
51
+ loads** (`Built`), not by transport — `fromUrl` was rejected because it
52
+ hides the artifact type.
53
+ - Build output is HTTP-served only. Loading a built artifact from local
54
+ disk is intentionally unsupported in V1; consumers either spin up a
55
+ local HTTP server or use `fromPackage`/`fromDir` against the source.
56
+ Adding `Quiver.fromBuiltDir(path)` later is purely additive if a real
57
+ use case appears.
43
58
 
44
59
  ### 2) `Quiver.yaml` Is Required in Source Quivers
45
60
 
@@ -57,32 +72,26 @@ Unknown fields in `Quiver.yaml` are a **validation error** (`quiver_invalid`). S
57
72
  ### 3) `QuillSource` Becomes Quiver-Centric
58
73
 
59
74
  - Re-express `QuillSource` concepts around Quivers
60
- - Split old filesystem concept into:
61
- - **Source filesystem loader** (reads Source Quiver directly)
62
- - **Packed filesystem transport** (loads Packed Quiver artifact from disk)
63
- - Treat HTTP as a Packed transport, not a separate format concept
64
-
65
- ### 4) Multi-Quiver Composition with Deterministic Precedence
66
-
67
- `QuiverRegistry` accepts multiple quivers with explicit order.
68
-
69
- This registry/composition layer lives entirely in `@quillmark/quiver`; it is not a Quillmark engine feature.
70
-
71
- Precedence rule:
72
-
73
- - **Precedence is a hard filter**
74
- - Scan quivers in order
75
- - First quiver with any matching candidate wins
76
- - Choose highest matching version **within that quiver**
77
-
78
- Applies to:
79
-
80
- - unqualified refs (e.g. `usaf_memo`)
81
- - selector refs (e.g. `usaf_memo@1.2`)
82
-
83
- No global highest-across-all-quivers behavior.
84
-
85
- **Identity collision:** duplicate `Quiver.yaml.name` across composed quivers is an error in V1.
75
+ - Three loaders, each with one input shape and one shape-of-thing-loaded:
76
+ - `Quiver.fromPackage(specifier)` Node-only; resolves an npm specifier
77
+ against `node_modules` and loads the source layout at the package root
78
+ - `Quiver.fromDir(path)` Node-only; loads source layout from a local
79
+ directory. Also accepts `import.meta.url`-style `file://` URLs as a
80
+ convenience for tests (the URL's parent directory is used)
81
+ - `Quiver.fromBuilt(url)` — browser-safe; loads build output from an
82
+ `http(s)://` or origin-relative URL
83
+ - "Transport" is not a first-class concept; HTTP fetching is an internal
84
+ detail of `fromBuilt`
85
+
86
+ ### 4) Single-Quiver Scope (V1)
87
+
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`.
91
+
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.
86
95
 
87
96
  ### 5) Semver Selector Rules Are Strict and Small
88
97
 
@@ -108,17 +117,35 @@ Canonical version format:
108
117
 
109
118
  Canonicalization:
110
119
 
111
- - resolve selector to canonical ref once per manifest snapshot
112
- - 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)
113
122
 
114
123
  ### 6) Warm/Prefetch Is Purely a Quiver Concern
115
124
 
116
- `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:
117
132
 
118
- - `warm()` warms all by default in V1
119
- - `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).
120
143
 
121
- Warm now means "load/prepare quills and artifacts", not "register in engine". There is no engine registration step anymore.
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.
122
149
 
123
150
  ### 7) Engine Boundary: New Canonical Contract (Node / JS–WASM only)
124
151
 
@@ -140,7 +167,7 @@ For advanced dynamic-asset behavior, defer to Quillmark’s JS/WASM docs; the de
140
167
  ### 8) Markdown and Ref Parsing Boundary
141
168
 
142
169
  - Markdown parsing does not require a quill registry: `Document.fromMarkdown(markdown)`
143
- - 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)
144
171
  - QUILL field is informational at render time; Quiver routes to the intended quill explicitly without mutating the parsed document in V1
145
172
 
146
173
  Upstream behavior note:
@@ -150,16 +177,41 @@ Upstream behavior note:
150
177
 
151
178
  ### 9) Distribution Strategy
152
179
 
153
- V1 supports:
154
-
155
- - npm distribution for Source Quiver projects
156
- - git/folder-copy consumption of Source Quivers
157
- - `pack()` output for Packed runtime distribution
158
-
159
- Clarification:
160
-
161
- - npm/git are developer distribution channels
162
- - packed artifacts are runtime delivery artifacts
180
+ **Source-first distribution.** The published artifact is the **Source
181
+ Quiver** — an npm package whose root contains `Quiver.yaml`. Consumers
182
+ choose how to consume it:
183
+
184
+ - **Node consumers** load the source layout directly:
185
+ ```ts
186
+ const quiver = await Quiver.fromPackage("@org/my-quiver");
187
+ ```
188
+ - **Browser consumers** run a build step against the resolved source dir
189
+ and serve the output as static assets:
190
+ ```ts
191
+ // build step (Node)
192
+ await Quiver.build("./node_modules/@org/my-quiver", "./public/quivers/my-quiver");
193
+ // browser runtime
194
+ const quiver = await Quiver.fromBuilt("/quivers/my-quiver/");
195
+ ```
196
+
197
+ Rationale:
198
+
199
+ - Author release pipeline is `npm publish` (or `git tag`). No second
200
+ artifact, no CDN, no hash bookkeeping outside the npm tarball.
201
+ - Deployment topology is the consumer's concern, not the author's.
202
+ - The runtime artifact is a build output of the source, not a peer
203
+ distribution shape (see §1).
204
+
205
+ **Pre-built distribution as a published artifact is supported but not
206
+ the default.** Authors who need to ship runtime-ready output directly
207
+ (e.g. their consumers cannot run a Node build step) may publish
208
+ `Quiver.build(...)` output to a CDN and instruct consumers to use
209
+ `Quiver.fromBuilt(<cdn-url>)`. Treated as the exception.
210
+
211
+ Validation responsibility shifts left: authors should run
212
+ `Quiver.fromDir` and `Quiver.build` in CI so `quiver_invalid` errors
213
+ surface on publish, not on the consumer's build. The bundled
214
+ `@quillmark/quiver/testing` harness covers this.
163
215
 
164
216
  ---
165
217
 
@@ -167,19 +219,20 @@ Clarification:
167
219
 
168
220
  V1 intentionally retains:
169
221
 
170
- 1. Font dehydration as a Packed Quiver property
171
- 2. Consumer validation tooling for Source Quivers (+ optional Packed parity checks)
172
- 3. Manifest pointer resolution for Packed format
173
- 4. HTTP loading as a Packed transport
174
- 5. Source filesystem loading as first-class dev loop
175
- 6. Packed filesystem loading as first-class runtime option
176
- 7. Typed errors (`QuiverError`) with quiver/transport context
177
- 8. Concurrency coalescing for in-flight loads
178
- 9. Preload/fail-fast helpers where they still add value
222
+ 1. Font dehydration as a build-output property
223
+ 2. Consumer validation tooling for source layouts (+ optional build-parity checks)
224
+ 3. Manifest pointer resolution for build output
225
+ 4. HTTP/HTTPS loading via `Quiver.fromBuilt`
226
+ 5. Source layout loading as first-class dev loop (`fromPackage` / `fromDir`)
227
+ 6. Typed errors (`QuiverError`) with quiver/source context
228
+ 7. Concurrency coalescing for in-flight loads
229
+ 8. Preload/fail-fast helpers where they still add value
179
230
 
180
231
  Removed from carryover assumptions:
181
232
 
182
233
  - Any engine-registration cache fast path (`register`/`has`) because upstream removed the capability
234
+ - "Transport" as a user-facing concept (folded into `fromBuilt` internals)
235
+ - Loading build output from local disk (no `fromBuiltDir` in V1; YAGNI)
183
236
 
184
237
  ---
185
238
 
@@ -188,7 +241,7 @@ Removed from carryover assumptions:
188
241
  V1 should trim public API where behavior can stay internal:
189
242
 
190
243
  - Drop internal-only utility exports
191
- - Keep engine payload and transport internals opaque
244
+ - Keep engine payload and loader internals opaque
192
245
  - Consolidate validation exports
193
246
  - Avoid duplicate entry points for equivalent validation workflows
194
247
  - Do not expose internal quill-object cache mechanisms as public contract
@@ -204,13 +257,12 @@ V1 code catalog (closed set):
204
257
  | Code | Fires when |
205
258
  |---|---|
206
259
  | `invalid_ref` | Malformed ref string at `resolve()`/`warm()` boundary (fails `parseQuillRef`) |
207
- | `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 |
208
261
  | `quiver_invalid` | `Quiver.yaml` or hashed manifest malformed, unknown field, non-canonical version on disk, or font/bundle hash mismatch |
209
262
  | `transport_error` | I/O failure: missing path, HTTP non-2xx, network error, permission error. Wraps underlying cause. |
210
- | `quiver_collision` | Two composed quivers share `Quiver.yaml.name` at registry construction |
211
263
 
212
264
  Notes:
213
- - `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.
214
266
  - `transport_error` is artifact access failure (filesystem/HTTP/network/permissions), including missing packed files and HTTP 404.
215
267
  - Legacy categories such as `manifest_invalid`, `quill_load_failed`, and `backend_not_found` are folded into `quiver_invalid` or `transport_error` in V1.
216
268
 
@@ -218,18 +270,22 @@ Errors must include offending ref/version/quiver identifiers when available.
218
270
 
219
271
  ---
220
272
 
221
- ## Runtime + Packaging Model
273
+ ## Runtime + Build Model
222
274
 
223
275
  V1 runtime loading paths:
224
276
 
225
- 1. Source Quiver from filesystem (authoring/dev)
226
- 2. Packed Quiver over HTTP (browser/runtime)
227
- 3. Packed Quiver from filesystem (air-gapped/container/runtime)
277
+ 1. `Quiver.fromPackage(specifier)` npm package resolution; loads source
278
+ (authoring/dev/Node runtime)
279
+ 2. `Quiver.fromDir(path)` local directory; loads source (Node)
280
+ 3. `Quiver.fromBuilt(url)` — HTTP(S); loads build output (browser; also
281
+ works in Node)
228
282
 
229
- V1 packaging behavior:
283
+ V1 build behavior:
230
284
 
231
- - `Quiver.pack()` produces Packed Quiver artifact independent of transport
232
- - packed output includes pointer + hashed manifest + bundles + dehydrated font store
285
+ - `Quiver.build(srcDir, outDir)` produces the runtime artifact from a
286
+ source layout
287
+ - output includes pointer + hashed manifest + bundles + dehydrated font store
288
+ (see "Runtime Artifact Format" below)
233
289
 
234
290
  Execution behavior:
235
291
 
@@ -261,7 +317,11 @@ Caching scope:
261
317
  - Non-canonical version directories are a validation error (`quiver_invalid`).
262
318
  - Dedup of identical fonts across quills happens at pack time (into `store/<md5>`), not at the source layer.
263
319
 
264
- ## Packed Quiver Format (normative)
320
+ ## Runtime Artifact Format (normative)
321
+
322
+ Produced by `Quiver.build()`. Authors do not author this layout; consumers
323
+ do not need to inspect it. It is an implementation detail of build output,
324
+ specified here only because loaders must agree on its shape.
265
325
 
266
326
  ```
267
327
  <root>/
@@ -272,7 +332,7 @@ Caching scope:
272
332
  <md5> # raw font bytes, no extension
273
333
  ```
274
334
 
275
- **Hash:** MD5 prefix-6, computed with `node:crypto` at `pack()` time only (dev/tooling; not browser runtime).
335
+ **Hash:** MD5 prefix-6, computed with `node:crypto` at `build()` time only (dev/tooling; not browser runtime).
276
336
 
277
337
  **Pointer** `Quiver.json`:
278
338
  ```json
@@ -295,63 +355,71 @@ Caching scope:
295
355
  }
296
356
  ```
297
357
 
298
- **Bundle zips** contain pure quill content (`Quill.yaml` + templates + partials + non-font assets). Fonts are dehydrated at pack time: their bytes live only in `store/<md5>`; their path→hash mapping lives only in the hashed manifest. Bundles do **not** embed a `fonts.json`.
358
+ **Bundle zips** contain pure quill content (`Quill.yaml` + templates + partials + non-font assets). Fonts are dehydrated at build time: their bytes live only in `store/<md5>`; their path→hash mapping lives only in the hashed manifest. Bundles do **not** embed a `fonts.json`.
299
359
 
300
- Rehydration on load: transport fetches the pointer → hashed manifest → required bundle(s) → required `store/<md5>` blobs; library reconstructs the full in-memory tree and builds a render-ready quill via `engine.quill(tree)`.
360
+ Rehydration on load: the loader fetches the pointer → hashed manifest → required bundle(s) → required `store/<md5>` blobs; library reconstructs the full in-memory tree and builds a render-ready quill via `engine.quill(tree)`.
301
361
 
302
362
  ## API Surface (V1)
303
363
 
304
- Single class, three factories:
364
+ Single class, three loaders + one builder. Each loader has one input
365
+ shape and one shape-of-thing-loaded; no auto-detection.
305
366
 
306
367
  ```ts
307
368
  class Quiver {
308
- // Node-only factories (from `@quillmark/quiver/node`). Fail fast in browser.
309
- static fromSourceDir(path: string): Promise<Quiver>;
310
- static fromPackedDir(path: string): Promise<Quiver>;
311
- // Browser-safe factory (from `@quillmark/quiver` main).
312
- static fromHttp(url: string): Promise<Quiver>;
313
- // Node-only tooling. Writes a Packed Quiver to outDir.
314
- static pack(sourceDir: string, outDir: string, opts?: PackOptions): Promise<void>;
369
+ // Node-only loader: resolve an npm specifier against node_modules and
370
+ // load the source layout at the package root.
371
+ // (From `@quillmark/quiver/node`.)
372
+ static fromPackage(specifier: string): Promise<Quiver>;
373
+
374
+ // Node-only loader: load source layout from a local directory.
375
+ // Also accepts `import.meta.url`-style `file://` URLs (the URL's parent
376
+ // directory is used) as a convenience for tests.
377
+ // (From `@quillmark/quiver/node`.)
378
+ static fromDir(pathOrFileUrl: string): Promise<Quiver>;
379
+
380
+ // Browser-safe loader: load build output from an http(s):// or
381
+ // origin-relative URL. Throws `transport_error` on file:// inputs.
382
+ // (From `@quillmark/quiver` main.)
383
+ static fromBuilt(url: string): Promise<Quiver>;
384
+
385
+ // Node-only tooling: produce the runtime artifact from a source layout.
386
+ // (From `@quillmark/quiver/node`.)
387
+ static build(sourceDir: string, outDir: string, opts?: BuildOptions): Promise<void>;
315
388
 
316
389
  readonly name: string; // from Quiver.yaml
317
390
 
318
- // Read-only introspection and lazy tree access used by QuiverRegistry
319
- // internally; also available for external debugging and tooling.
391
+ // Read-only introspection and lazy tree access; also used internally by
392
+ // resolve/getQuill/warm.
320
393
  quillNames(): string[]; // sorted lex
321
394
  versionsOf(name: string): string[]; // sorted desc
322
395
  loadTree(name: string, version: string): Promise<Map<string, Uint8Array>>;
323
- }
324
- ```
325
-
326
- ```ts
327
- class QuiverRegistry {
328
- constructor(args: { engine: Quillmark; quivers: Quiver[] });
329
396
 
330
397
  // Selector ref -> canonical ref. Throws invalid_ref / quill_not_found.
331
398
  resolve(ref: string): Promise<string>;
332
399
 
333
- // Canonical ref -> render-ready quill handle (materialized via engine.quill(tree), cached in-process).
334
- 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>;
335
403
 
336
- // 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.
337
406
  warm(): Promise<void>;
338
407
  }
339
408
 
340
409
  class QuiverError extends Error {
341
- code: "invalid_ref" | "quill_not_found" | "quiver_invalid" | "transport_error" | "quiver_collision";
410
+ code: "invalid_ref" | "quill_not_found" | "quiver_invalid" | "transport_error";
342
411
  // plus contextual payload fields
343
412
  }
344
413
  ```
345
414
 
346
- **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.
347
416
 
348
- **Internal (not exported):** `QuiverTransport`, `QuiverManifest` (runtime shape), `parseQuillRef`, in-flight coalescing state.
417
+ **Internal (not exported):** `QuiverManifest` (runtime shape), `parseQuillRef`, in-flight coalescing state, source-vs-built layout detection.
349
418
 
350
419
  Hot-path flow:
351
420
  ```ts
352
421
  const doc = Document.fromMarkdown(md);
353
- const canonicalRef = await registry.resolve(doc.quillRef);
354
- const quill = await registry.getQuill(canonicalRef);
422
+ const quill = await quiver.getQuill(doc.quillRef, { engine });
355
423
  const result = quill.render(doc, { format: "pdf" });
356
424
  ```
357
425
 
@@ -360,13 +428,23 @@ const result = quill.render(doc, { format: "pdf" });
360
428
  **Name:** `@quillmark/quiver`
361
429
 
362
430
  **Entrypoints:**
363
- - `@quillmark/quiver` (main, browser-safe): `Quiver` class with only `fromHttp` functional (Node-only factories/pack throw `transport_error` if reached in browser), `QuiverRegistry`, `QuiverError`, shared types.
364
- - `@quillmark/quiver/node`: adds `Quiver.fromSourceDir`, `Quiver.fromPackedDir`, `Quiver.pack` behaviors. Single `Quiver` class — Node-only factories fail fast outside Node.
431
+ - `@quillmark/quiver` (main, browser-safe): `Quiver` class with only
432
+ `fromBuilt` functional (Node-only loaders/builder throw
433
+ `transport_error` if reached in browser), `QuiverError`,
434
+ `QuillmarkLike`, `QuillLike`, shared types.
435
+ - `@quillmark/quiver/node`: adds `Quiver.fromPackage`, `Quiver.fromDir`,
436
+ `Quiver.build` behaviors. Single `Quiver` class — Node-only factories
437
+ fail fast outside Node.
438
+ - `@quillmark/quiver/testing` (Node-only): single export
439
+ `runQuiverTests(metaUrlOrDir, engine)` built on `node:test` (zero
440
+ external test-runner dependency). Optional convenience; users on other
441
+ test runners wire their own loops against the main API.
365
442
 
366
443
  **Dependencies:**
367
444
  - Peer: `@quillmark/wasm@>=0.59.0-rc.2` with `Quillmark`, `Document.fromMarkdown`, `engine.quill(tree)`, and `quill.render(doc, opts?)` APIs.
368
445
  - Runtime: `fflate ^0.8.2` for zip read/write (Node + browser)
369
- - Dev-only: `node:crypto` (MD5 hashing in `pack()` — never reached at runtime)
446
+ - Dev-only: `node:crypto` (MD5 hashing in `build()` — never reached at runtime)
447
+ - No test-runner peer dependency; `/testing` uses `node:test` (built-in)
370
448
 
371
449
  ---
372
450
 
@@ -380,7 +458,7 @@ const result = quill.render(doc, { format: "pdf" });
380
458
  - inter-quiver dependency graph in `Quiver.yaml`
381
459
  - marketplace/discovery service
382
460
  - advanced warm strategies beyond API-compatible hooks
383
- - multi-quiver name-collision soft handling (V1 errors on duplicate `Quiver.yaml.name`)
461
+ - multi-quiver composition (single quiver per consumer in V1)
384
462
 
385
463
  ---
386
464
 
@@ -388,14 +466,14 @@ const result = quill.render(doc, { format: "pdf" });
388
466
 
389
467
  All V1 planner questions resolved; implementation plan can proceed against the spec above.
390
468
 
391
- 1. ~~Final `Quiver` interface shape and transport factoring style~~ → Single `Quiver` class, three static factories (`fromHttp`, `fromSourceDir`, `fromPackedDir`). Transport kept internal (no `fromTransport` in V1; YAGNI).
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.
392
470
  2. ~~Final `Quiver.yaml` schema and unknown-field policy~~ → See §2: alphanumeric `name` and optional tooling-only `description`. Unknown fields are `quiver_invalid`.
393
471
  3. ~~Canonical ref grammar and parser API contract~~ → Internal `parseQuillRef`, not exported. Selector syntax per §5. Throws `invalid_ref`.
394
- 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`.
395
- 5. ~~Validation API shape consolidation~~ → No separate validation API. Validation errors surface as `QuiverError('quiver_invalid')` during load or `pack()`.
396
- 6. ~~Pack artifact directory structure~~ → See "Packed Quiver Format (normative)".
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).
473
+ 5. ~~Validation API shape consolidation~~ → No separate validation API. Validation errors surface as `QuiverError('quiver_invalid')` during load or `build()`.
474
+ 6. ~~Build output directory structure~~ → See "Runtime Artifact Format (normative)".
397
475
  7. ~~Node/browser entrypoint split~~ → See "Package Structure": main + `/node` subpath, single `Quiver` class.
398
- 8. ~~Final exported type names~~ → `Quiver`, `QuiverRegistry`, `QuiverError`. Hot-path entry is `QuiverRegistry.resolve(ref)` + `QuiverRegistry.getQuill(canonicalRef)`.
476
+ 8. ~~Final exported type names~~ → `Quiver`, `QuiverError`. Hot-path entry is `Quiver.getQuill(ref, { engine })`.
399
477
 
400
478
  ---
401
479
 
@@ -413,7 +491,8 @@ Local copies in this repo for `@quillmark/quiver` implementation:
413
491
  ## Success Criteria
414
492
 
415
493
  - A team can author and validate a Source Quiver locally with fast filesystem loops
416
- - A packed artifact can be loaded via HTTP or local filesystem with equivalent semantics
494
+ - Build output can be loaded over HTTP/HTTPS with parity behavior in browser and Node
495
+ - Each loader names exactly what it loads — no auto-detection, no ambiguous return shape
417
496
  - Multi-quiver resolution is deterministic and matches precedence hard-filter rules
418
497
  - Selector behavior is predictable and explicitly documented
419
498
  - Quiver (Node) integrates via `engine.quill(tree)` + `quill.render(...)` only (no engine quill registration path)