@quillmark/quiver 0.11.0 → 0.13.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/README.md CHANGED
@@ -45,8 +45,8 @@ my-quiver/
45
45
 
46
46
  Recommended CI: use the bundled `@quillmark/quiver/testing` harness — it
47
47
  loads with `Quiver.fromDir`, compiles every quill, and renders each quill's
48
- blueprint so validation errors surface on publish, not on the consumer's
49
- build. The harness uses `node:test` (built into Node 18+); no extra
48
+ example document so validation errors surface on publish, not on the
49
+ consumer's build. The harness uses `node:test` (built into Node 18+); no extra
50
50
  test-runner dependency required. If you prefer vitest/jest/mocha, write a
51
51
  12-line loop against the main API instead.
52
52
 
@@ -58,18 +58,20 @@ human can look at. To eyeball real renders, run the
58
58
 
59
59
  ```ts
60
60
  // scripts/preview.ts — run with: node --experimental-strip-types scripts/preview.ts
61
- import { Quillmark, Document } from "@quillmark/wasm";
61
+ import { Quillmark } from "@quillmark/wasm";
62
62
  import { renderQuiverSamples } from "@quillmark/quiver/preview";
63
63
 
64
64
  await renderQuiverSamples(import.meta.url, {
65
65
  engine: new Quillmark(),
66
- Document,
67
66
  });
68
67
  // → writes ./preview/<name>@<version>.<fmt> + index.html
69
68
  ```
70
69
 
71
- It renders every quill's auto-generated blueprint (`quill.blueprint`), writes
72
- the artifacts to `outDir` (default `preview/`), and emits an `index.html`
70
+ It renders every quill's illustrative example document (seeded via
71
+ `quill.seedDocument()` — a fully filled-out, always-renderable sample; the
72
+ blueprint itself carries `<must-fill>` sentinels and is not directly
73
+ renderable), writes the artifacts
74
+ to `outDir` (default `preview/`), and emits an `index.html`
73
75
  gallery. A `.gitignore` is written into `outDir` so the generated artifacts
74
76
  are never accidentally committed. A quill that throws is recorded as failed —
75
77
  with every diagnostic, not just the first — without aborting the run, so one
@@ -81,7 +83,6 @@ quill name or canonical ref):
81
83
  ```ts
82
84
  await renderQuiverSamples(import.meta.url, {
83
85
  engine: new Quillmark(),
84
- Document,
85
86
  exclude: ["broken-quill"], // or: include: ["memo@1.0.0"]
86
87
  });
87
88
  ```
@@ -205,7 +206,3 @@ try {
205
206
  ```
206
207
 
207
208
  Error codes: `invalid_ref`, `quill_not_found`, `quiver_invalid`, `transport_error`.
208
-
209
- ## Full specification
210
-
211
- See [PROGRAM.md](./PROGRAM.md) for the complete API surface, runtime artifact format specification, and design decisions.
@@ -9,8 +9,8 @@
9
9
  * [--include <ref>...] [--exclude <ref>...]
10
10
  *
11
11
  * Engine discovery (for `test` and `preview`):
12
- * 1. Named exports `engine` and `Document` from `quiver.config.js` at the
13
- * collection root, if the file exists.
12
+ * 1. Named export `engine` from `quiver.config.js` at the collection root,
13
+ * if the file exists.
14
14
  * 2. Auto-imported `@quillmark/wasm` from the collection's node_modules.
15
15
  */
16
16
  export {};
@@ -9,8 +9,8 @@
9
9
  * [--include <ref>...] [--exclude <ref>...]
10
10
  *
11
11
  * Engine discovery (for `test` and `preview`):
12
- * 1. Named exports `engine` and `Document` from `quiver.config.js` at the
13
- * collection root, if the file exists.
12
+ * 1. Named export `engine` from `quiver.config.js` at the collection root,
13
+ * if the file exists.
14
14
  * 2. Auto-imported `@quillmark/wasm` from the collection's node_modules.
15
15
  */
16
16
  import { createRequire } from "node:module";
@@ -42,14 +42,11 @@ function hasFlag(name) {
42
42
  // Engine discovery
43
43
  // ---------------------------------------------------------------------------
44
44
  async function loadEngine(cwd) {
45
- // 1. Named exports from quiver.config.js, if present.
45
+ // 1. Named export from quiver.config.js, if present.
46
46
  try {
47
47
  const cfg = await import(pathToFileURL(join(cwd, "quiver.config.js")).href);
48
- if (cfg.engine != null && cfg.Document != null) {
49
- return {
50
- engine: cfg.engine,
51
- Document: cfg.Document,
52
- };
48
+ if (cfg.engine != null) {
49
+ return { engine: cfg.engine };
53
50
  }
54
51
  }
55
52
  catch {
@@ -64,10 +61,10 @@ async function loadEngine(cwd) {
64
61
  catch {
65
62
  throw new Error("Cannot find @quillmark/wasm in this collection's node_modules.\n" +
66
63
  " Install it: npm install @quillmark/wasm\n" +
67
- " Or export { engine, Document } from quiver.config.js for a custom engine.");
64
+ " Or export { engine } from quiver.config.js for a custom engine.");
68
65
  }
69
66
  const wasm = (await import(pathToFileURL(wasmPath).href));
70
- return { engine: new wasm.Quillmark(), Document: wasm.Document };
67
+ return { engine: new wasm.Quillmark() };
71
68
  }
72
69
  // ---------------------------------------------------------------------------
73
70
  // Commands
@@ -81,7 +78,7 @@ async function build() {
81
78
  }
82
79
  async function test() {
83
80
  const cwd = process.cwd();
84
- const { engine, Document } = await loadEngine(cwd);
81
+ const { engine } = await loadEngine(cwd);
85
82
  const quiver = await Quiver.fromDir(cwd);
86
83
  const names = quiver.quillNames();
87
84
  if (names.length === 0) {
@@ -95,10 +92,10 @@ async function test() {
95
92
  const ref = `${name}@${version}`;
96
93
  try {
97
94
  const quill = await quiver.getQuill(ref, { engine });
98
- const doc = Document.fromMarkdown(quill.blueprint);
95
+ const doc = quill.seedDocument();
99
96
  const result = quill.render(doc);
100
97
  if (!Array.isArray(result.artifacts) || result.artifacts.length === 0) {
101
- throw new Error("blueprint render produced no artifacts");
98
+ throw new Error("example render produced no artifacts");
102
99
  }
103
100
  console.log(`pass ${ref}`);
104
101
  pass++;
@@ -116,7 +113,7 @@ async function test() {
116
113
  }
117
114
  async function preview() {
118
115
  const cwd = process.cwd();
119
- const { engine, Document } = await loadEngine(cwd);
116
+ const { engine } = await loadEngine(cwd);
120
117
  const outDir = flag("--out");
121
118
  const format = flag("--format");
122
119
  const quiet = hasFlag("--quiet");
@@ -124,7 +121,6 @@ async function preview() {
124
121
  const exclude = multiFlag("--exclude");
125
122
  await renderQuiverSamples(cwd, {
126
123
  engine,
127
- Document,
128
124
  ...(outDir !== undefined && { outDir }),
129
125
  ...(format !== undefined && { format }),
130
126
  quiet,
package/dist/index.d.ts CHANGED
@@ -1,4 +1,3 @@
1
1
  export { QuiverError } from "./errors.js";
2
2
  export type { QuiverErrorCode } from "./errors.js";
3
3
  export { Quiver } from "./quiver.js";
4
- export type { QuillmarkLike, QuillLike } from "./engine-types.js";
package/dist/index.js CHANGED
@@ -5,3 +5,6 @@
5
5
  // live at `@quillmark/quiver/node`.
6
6
  export { QuiverError } from "./errors.js";
7
7
  export { Quiver } from "./quiver.js";
8
+ // Engine types (`Quillmark`, `Quill`, `Document`, `RenderResult`, …) are not
9
+ // re-exported: import them straight from the `@quillmark/wasm` peer dependency,
10
+ // which is the single source of truth for the engine contract.
package/dist/node.d.ts CHANGED
@@ -79,4 +79,3 @@ export declare const Quiver: typeof Base & NodeQuiverStatics;
79
79
  export { QuiverError } from "./errors.js";
80
80
  export type { QuiverErrorCode } from "./errors.js";
81
81
  export type { BuildOptions } from "./build.js";
82
- export type { QuillmarkLike, QuillLike } from "./engine-types.js";
package/dist/node.js CHANGED
@@ -70,3 +70,6 @@ Quiver.buildPackage = async function buildPackage(specifier, outDir, opts) {
70
70
  // 3. Re-export the rest of the public surface so consumers get one import.
71
71
  // ---------------------------------------------------------------------------
72
72
  export { QuiverError } from "./errors.js";
73
+ // Engine types (`Quillmark`, `Quill`, `Document`, `RenderResult`, …) are not
74
+ // re-exported: import them straight from the `@quillmark/wasm` peer dependency,
75
+ // which is the single source of truth for the engine contract.
package/dist/preview.d.ts CHANGED
@@ -3,43 +3,35 @@
3
3
  *
4
4
  * `runQuiverTests` (`@quillmark/quiver/testing`) proves every quill *compiles*
5
5
  * — it never produces a rendered artifact a human can look at. This module
6
- * closes that gap: it renders each quill's blueprint, writes the artifacts to
7
- * a directory, and emits an `index.html` gallery so an author can eyeball real
8
- * output before publishing.
6
+ * closes that gap: it renders each quill's example document, writes the
7
+ * artifacts to a directory, and emits an `index.html` gallery so an author can
8
+ * eyeball real output before publishing.
9
9
  *
10
10
  * Node-only: writes files and loads a source quiver from disk.
11
11
  *
12
12
  * Usage (place a script next to your Quiver.yaml):
13
13
  *
14
- * import { Quillmark, Document } from "@quillmark/wasm";
14
+ * import { Quillmark } from "@quillmark/wasm";
15
15
  * import { renderQuiverSamples } from "@quillmark/quiver/preview";
16
16
  *
17
17
  * await renderQuiverSamples(import.meta.url, {
18
18
  * engine: new Quillmark(),
19
- * Document,
20
19
  * });
21
20
  * // → open ./preview/index.html
22
21
  *
23
- * The sample document is the auto-generated blueprint (`quill.blueprint`) for
24
- * each quill version. Every quill always has a blueprint, so no quills are
25
- * skipped for lack of a sample document.
22
+ * The sample document is the illustrative example seeded by
23
+ * `quill.seedDocument()` for each quill version — a fully filled-out,
24
+ * always-renderable document (the blueprint itself carries `<must-fill>`
25
+ * sentinels and is not renderable). Every quill always seeds an example, so no
26
+ * quills are skipped for lack of a sample document.
26
27
  *
27
28
  * A `.gitignore` is written into `outDir` so the generated artifacts are not
28
29
  * accidentally committed.
29
30
  */
30
- import type { QuillmarkLike } from "./engine-types.js";
31
- /**
32
- * Structural shape of the `Document` class from `@quillmark/wasm`. Only the
33
- * `fromMarkdown` factory is used; passing the real class satisfies this.
34
- */
35
- export interface DocumentFactoryLike {
36
- fromMarkdown(markdown: string): unknown;
37
- }
31
+ import type { Quillmark } from "@quillmark/wasm";
38
32
  export interface RenderQuiverSamplesOptions {
39
33
  /** Quillmark engine instance (`new Quillmark()` from `@quillmark/wasm`). */
40
- engine: QuillmarkLike;
41
- /** The `Document` class from `@quillmark/wasm`. */
42
- Document: DocumentFactoryLike;
34
+ engine: Quillmark;
43
35
  /** Directory to write rendered artifacts into. Default: `preview`. */
44
36
  outDir?: string;
45
37
  /** Force an output format (`pdf`/`svg`/`png`/`txt`). Default: engine's choice. */
@@ -74,7 +66,7 @@ export interface RenderedSample {
74
66
  reasons: string[];
75
67
  }
76
68
  /**
77
- * Renders every quill's blueprint and writes the artifacts plus an
69
+ * Renders every quill's example document and writes the artifacts plus an
78
70
  * `index.html` gallery to `outDir`.
79
71
  *
80
72
  * Does NOT fail fast: a quill that throws is recorded as `failed` and the
package/dist/preview.js CHANGED
@@ -3,26 +3,27 @@
3
3
  *
4
4
  * `runQuiverTests` (`@quillmark/quiver/testing`) proves every quill *compiles*
5
5
  * — it never produces a rendered artifact a human can look at. This module
6
- * closes that gap: it renders each quill's blueprint, writes the artifacts to
7
- * a directory, and emits an `index.html` gallery so an author can eyeball real
8
- * output before publishing.
6
+ * closes that gap: it renders each quill's example document, writes the
7
+ * artifacts to a directory, and emits an `index.html` gallery so an author can
8
+ * eyeball real output before publishing.
9
9
  *
10
10
  * Node-only: writes files and loads a source quiver from disk.
11
11
  *
12
12
  * Usage (place a script next to your Quiver.yaml):
13
13
  *
14
- * import { Quillmark, Document } from "@quillmark/wasm";
14
+ * import { Quillmark } from "@quillmark/wasm";
15
15
  * import { renderQuiverSamples } from "@quillmark/quiver/preview";
16
16
  *
17
17
  * await renderQuiverSamples(import.meta.url, {
18
18
  * engine: new Quillmark(),
19
- * Document,
20
19
  * });
21
20
  * // → open ./preview/index.html
22
21
  *
23
- * The sample document is the auto-generated blueprint (`quill.blueprint`) for
24
- * each quill version. Every quill always has a blueprint, so no quills are
25
- * skipped for lack of a sample document.
22
+ * The sample document is the illustrative example seeded by
23
+ * `quill.seedDocument()` for each quill version — a fully filled-out,
24
+ * always-renderable document (the blueprint itself carries `<must-fill>`
25
+ * sentinels and is not renderable). Every quill always seeds an example, so no
26
+ * quills are skipped for lack of a sample document.
26
27
  *
27
28
  * A `.gitignore` is written into `outDir` so the generated artifacts are not
28
29
  * accidentally committed.
@@ -33,7 +34,7 @@ import { Quiver } from "./node.js";
33
34
  /** Default directory rendered artifacts are written to. */
34
35
  const DEFAULT_OUT_DIR = "preview";
35
36
  /**
36
- * Renders every quill's blueprint and writes the artifacts plus an
37
+ * Renders every quill's example document and writes the artifacts plus an
37
38
  * `index.html` gallery to `outDir`.
38
39
  *
39
40
  * Does NOT fail fast: a quill that throws is recorded as `failed` and the
@@ -87,8 +88,7 @@ async function renderOne(quiver, name, version, outDir, opts) {
87
88
  let result;
88
89
  try {
89
90
  const quill = await quiver.getQuill(ref, { engine: opts.engine });
90
- const markdown = quill.blueprint;
91
- const doc = opts.Document.fromMarkdown(markdown);
91
+ const doc = quill.seedDocument();
92
92
  result = quill.render(doc, opts.format ? { format: opts.format } : undefined);
93
93
  }
94
94
  catch (err) {
package/dist/quiver.d.ts CHANGED
@@ -9,7 +9,7 @@
9
9
  * `build`) are installed on this class by `./node.js`, which is the
10
10
  * consumer's explicit opt-in to the Node API surface.
11
11
  */
12
- import type { QuillmarkLike, QuillLike } from "./engine-types.js";
12
+ import type { Quillmark, Quill } from "@quillmark/wasm";
13
13
  /** @internal Internal loader strategy: source or build output. */
14
14
  export interface QuiverLoader {
15
15
  loadTree(name: string, version: string): Promise<Map<string, Uint8Array>>;
@@ -86,8 +86,8 @@ export declare class Quiver {
86
86
  * - propagates engine errors from engine.quill() unchanged
87
87
  */
88
88
  getQuill(ref: string, opts: {
89
- engine: QuillmarkLike;
90
- }): Promise<QuillLike>;
89
+ engine: Quillmark;
90
+ }): Promise<Quill>;
91
91
  /**
92
92
  * Prefetches the tree for every quill version in this quiver. Fail-fast.
93
93
  *
package/dist/testing.d.ts CHANGED
@@ -7,15 +7,14 @@
7
7
  *
8
8
  * Usage (place this file next to your Quiver.yaml):
9
9
  *
10
- * import { Quillmark, Document } from "@quillmark/wasm";
10
+ * import { Quillmark } from "@quillmark/wasm";
11
11
  * import { runQuiverTests } from "@quillmark/quiver/testing";
12
12
  * const engine = new Quillmark();
13
- * runQuiverTests(import.meta.url, engine, Document);
13
+ * runQuiverTests(import.meta.url, engine);
14
14
  *
15
15
  * Run with `node --test`.
16
16
  */
17
- import type { QuillmarkLike } from "./engine-types.js";
18
- import type { DocumentFactoryLike } from "./preview.js";
17
+ import type { Quillmark } from "@quillmark/wasm";
19
18
  /**
20
19
  * Registers a `node:test` describe block that validates every quill
21
20
  * version in the quiver at `metaUrlOrDir` against the provided engine.
@@ -25,6 +24,6 @@ import type { DocumentFactoryLike } from "./preview.js";
25
24
  *
26
25
  * Validation covers the full loading pipeline: Quiver.yaml, Quill.yaml,
27
26
  * all template files, engine compilation via engine.quill(tree), and a
28
- * full render of each quill's blueprint document.
27
+ * full render of each quill's example document.
29
28
  */
30
- export declare function runQuiverTests(metaUrlOrDir: string, engine: QuillmarkLike, Document: DocumentFactoryLike): void;
29
+ export declare function runQuiverTests(metaUrlOrDir: string, engine: Quillmark): void;
package/dist/testing.js CHANGED
@@ -7,10 +7,10 @@
7
7
  *
8
8
  * Usage (place this file next to your Quiver.yaml):
9
9
  *
10
- * import { Quillmark, Document } from "@quillmark/wasm";
10
+ * import { Quillmark } from "@quillmark/wasm";
11
11
  * import { runQuiverTests } from "@quillmark/quiver/testing";
12
12
  * const engine = new Quillmark();
13
- * runQuiverTests(import.meta.url, engine, Document);
13
+ * runQuiverTests(import.meta.url, engine);
14
14
  *
15
15
  * Run with `node --test`.
16
16
  */
@@ -28,9 +28,9 @@ import { Quiver } from "./node.js";
28
28
  *
29
29
  * Validation covers the full loading pipeline: Quiver.yaml, Quill.yaml,
30
30
  * all template files, engine compilation via engine.quill(tree), and a
31
- * full render of each quill's blueprint document.
31
+ * full render of each quill's example document.
32
32
  */
33
- export function runQuiverTests(metaUrlOrDir, engine, Document) {
33
+ export function runQuiverTests(metaUrlOrDir, engine) {
34
34
  describe("Quiver", () => {
35
35
  let quiver;
36
36
  before(async () => {
@@ -41,15 +41,15 @@ export function runQuiverTests(metaUrlOrDir, engine, Document) {
41
41
  throw new Error("Quiver has no quills");
42
42
  }
43
43
  });
44
- it("compiles and renders every quill's blueprint without error", async () => {
44
+ it("compiles and renders every quill's example without error", async () => {
45
45
  for (const name of quiver.quillNames()) {
46
46
  for (const version of quiver.versionsOf(name)) {
47
47
  const ref = `${name}@${version}`;
48
48
  const quill = await quiver.getQuill(ref, { engine });
49
- const doc = Document.fromMarkdown(quill.blueprint);
49
+ const doc = quill.seedDocument();
50
50
  const result = quill.render(doc);
51
51
  if (!Array.isArray(result.artifacts) || result.artifacts.length === 0) {
52
- throw new Error(`${ref}: blueprint render produced no artifacts`);
52
+ throw new Error(`${ref}: example render produced no artifacts`);
53
53
  }
54
54
  }
55
55
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quillmark/quiver",
3
- "version": "0.11.0",
3
+ "version": "0.13.0",
4
4
  "description": "Quiver registry and build tooling for Quillmark",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -47,18 +47,17 @@
47
47
  ],
48
48
  "files": [
49
49
  "dist",
50
- "PROGRAM.md",
51
50
  "README.md"
52
51
  ],
53
52
  "peerDependencies": {
54
- "@quillmark/wasm": ">=0.80.0"
53
+ "@quillmark/wasm": ">=0.88.0"
55
54
  },
56
55
  "dependencies": {
57
56
  "fflate": "^0.8.2",
58
57
  "yaml": "^2.8.3"
59
58
  },
60
59
  "devDependencies": {
61
- "@quillmark/wasm": "^0.80.0",
60
+ "@quillmark/wasm": "^0.88.0",
62
61
  "@types/node": "^25.3.3",
63
62
  "typescript": "^5.9.3",
64
63
  "vitest": "^4.0.18"
package/PROGRAM.md DELETED
@@ -1,512 +0,0 @@
1
- # Program: Quivers Rewrite (`@quillmark/quiver`)
2
-
3
- **Status:** Updated for upstream Quillmark Render API overhaul
4
- **Audience:** Senior engineer planning and executing V1
5
- **Scope:** This document applies only to the **Node/npm** package **`@quillmark/quiver`**: a smaller surface than `@quillmark/registry`, aligned with the **JavaScript/WASM** Quillmark bindings (not Python or other language bindings).
6
-
7
- ## Summary
8
-
9
- V1 still introduces Quivers as the primary runtime abstraction, but one boundary changed materially upstream:
10
-
11
- - Quillmark no longer owns a quill registry
12
- - Rendering lives on `Quill` (`quill.render(...)`), not the engine
13
- - Engine is now a backend registry + quill factory (`engine.quill(tree)` on the Quillmark WASM binding, typically `@quillmark/wasm`)
14
-
15
- This means Quiver must own quill selection and lifecycle entirely. There is no engine `registerQuill`, `hasQuill`, or engine-level render hot path to optimize around.
16
-
17
- ---
18
-
19
- ## Core Decisions
20
-
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.fromBuiltUrl(url)` loads the **build output** over HTTP/HTTPS
41
- (browser-safe; works in Node too).
42
- - `Quiver.fromBuiltDir(path)` loads the **build output** from a local
43
- directory (Node only).
44
-
45
- There is no auto-detection: each loader's name commits to what it expects.
46
-
47
- **Naming decisions:**
48
- - The verb is `build()`. Pairs with the `fromBuilt*` family (past participle
49
- = "the built one"). Avoids collision with `npm pack`, JS bundler
50
- vocabulary, and the internal "bundle zips" term.
51
- - Loaders for source layouts are named by **where the source lives**
52
- (`Package` / `Dir`). Loaders for build output are named by **what** they
53
- load (`Built`) plus **where** they load it from (`Url` / `Dir`). The
54
- artifact type stays in the name; the transport disambiguates.
55
- - Server-side runtime (Node) reads packed artifacts from disk via
56
- `fromBuiltDir`. This is the recommended shape when the runtime artifact
57
- ships in the deployment image: source quivers stay as devDependencies,
58
- there is no self-fetch over a load balancer / CDN, and serverless bundle
59
- sizes are minimised.
60
-
61
- ### 2) `Quiver.yaml` Is Required in Source Quivers
62
-
63
- Source Quivers require root `Quiver.yaml` metadata.
64
-
65
- Fields for V1:
66
-
67
- - `name` (required) — runtime namespace identity; may differ from npm package name. Charset: alphanumeric only (`[A-Za-z0-9_-]+`).
68
- - `description` (optional) — tooling-only metadata in V1; not consumed by runtime paths.
69
-
70
- Unknown fields in `Quiver.yaml` are a **validation error** (`quiver_invalid`). Strict-by-default keeps forward compatibility explicit: any future field is additive and requires a schema bump.
71
-
72
- `package.json.version` remains npm-channel identity; packed artifact identity remains hashed-manifest based.
73
-
74
- ### 3) `QuillSource` Becomes Quiver-Centric
75
-
76
- - Re-express `QuillSource` concepts around Quivers
77
- - Four loaders, each with one input shape and one shape-of-thing-loaded:
78
- - `Quiver.fromPackage(specifier)` — Node-only; resolves an npm specifier
79
- against `node_modules` and loads the source layout at the package root
80
- - `Quiver.fromDir(path)` — Node-only; loads source layout from a local
81
- directory. Also accepts `import.meta.url`-style `file://` URLs as a
82
- convenience for tests (the URL's parent directory is used)
83
- - `Quiver.fromBuiltUrl(url)` — browser-safe; loads build output from an
84
- `http(s)://` or origin-relative URL
85
- - `Quiver.fromBuiltDir(path)` — Node-only; loads build output from a
86
- local directory (the output of `Quiver.build`)
87
- - "Transport" is not a first-class concept; HTTP fetching is an internal
88
- detail of `fromBuiltUrl`, filesystem access is an internal detail of
89
- `fromBuiltDir`
90
-
91
- ### 4) Single-Quiver Scope (V1)
92
-
93
- V1 has no multi-quiver composition layer. Each `Quiver` instance is
94
- independent: consumers load one quiver and call `resolve` / `getQuill` /
95
- `warm` directly on it. There is no `QuiverRegistry`.
96
-
97
- If composition becomes a real use case, the additive path is a thin
98
- `Quiver.compose([a, b, ...])` factory that returns a quiver-shaped
99
- composite — it is intentionally out of scope for V1.
100
-
101
- ### 5) Semver Selector Rules Are Strict and Small
102
-
103
- Supported forms:
104
-
105
- - `name` (highest version in first-winning quiver)
106
- - `name@x.y.z` exact
107
- - `name@x.y` highest `x.y.*`
108
- - `name@x` highest `x.*.*`
109
-
110
- Not supported in V1:
111
-
112
- - ranges (`>=`, `<`, etc.)
113
- - npm operators (`^`, `~`)
114
- - wildcards (`*`)
115
- - prerelease/build metadata
116
-
117
- Canonical version format:
118
-
119
- - `x.y.z` only
120
- - applies to quill version directories (`quills/<name>/<version>/`)
121
- - non-canonical versions on disk are validation errors
122
-
123
- Canonicalization:
124
-
125
- - resolve selector to canonical ref once per call to `getQuill`
126
- - key the tree cache by canonical ref; key the quill cache by (engine, canonical-ref)
127
-
128
- ### 6) Warm/Prefetch Is Purely a Quiver Concern
129
-
130
- `warm()` is the network prefetch step. It fetches every quill's tree and
131
- populates the per-quiver tree cache. It does **not** materialize Quill
132
- instances and does **not** require an engine — `engine.quill(tree)` is
133
- microseconds and runs lazily inside `getQuill`. A subsequent `getQuill`
134
- reuses the cached tree, so the network fetch isn't paid twice.
135
-
136
- Tree cache lifecycle:
137
-
138
- - `warm()` populates the tree cache.
139
- - First `getQuill(ref, { engine })` reads the tree, materializes the
140
- Quill via `engine.quill(tree)`, then evicts the tree so its bytes can
141
- be GC'd. The materialized Quill is what's kept (per engine).
142
- - If `engine.quill` throws, the tree is retained so a retry skips the
143
- network.
144
- - Repeated `getQuill` on the same engine hits the per-engine quill cache
145
- — no tree access at all.
146
- - A subsequent `getQuill` for a different engine refetches the tree
147
- (single-engine apps never pay this cost).
148
-
149
- Other invariants:
150
-
151
- - `resolve()` works whether or not anything is warmed.
152
- - Warm semantics are identical for source-loaded and built-output-loaded
153
- quivers; the loader hides the difference.
154
-
155
- ### 7) Engine Boundary: New Canonical Contract (Node / JS–WASM only)
156
-
157
- Quillmark integration for `@quillmark/quiver` is:
158
-
159
- 1. Initialize engine once (`new Quillmark()` from `@quillmark/wasm`)
160
- 2. When quill bytes are ready, build a render-ready quill with `engine.quill(tree)` (`Map<string, Uint8Array>`)
161
- 3. Render through the returned quill: `quill.render(doc, opts?)` (`Document` — built via `Document.fromMarkdown(...)`)
162
-
163
- Important implications:
164
-
165
- - No engine quill registry in the JS binding; no `registerQuill`, `hasQuill`, or engine-level `render(doc)` in Quiver’s flow
166
- - Quiver owns mapping from canonical ref → in-memory tree → `Quill` instance
167
- - Cache optimization is in-process reuse of `Quill` instances, not registration checks
168
- - 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)
169
-
170
- For advanced dynamic-asset behavior, defer to Quillmark’s JS/WASM docs; the default integration path here is `engine.quill` + `quill.render`.
171
-
172
- ### 8) Markdown and Ref Parsing Boundary
173
-
174
- - Markdown parsing does not require a quill registry: `Document.fromMarkdown(markdown)`
175
- - Quiver owns ref parsing and selector resolution for its own API (`resolve`, `getQuill`, `warm`, validation)
176
- - QUILL field is informational at render time; Quiver routes to the intended quill explicitly without mutating the parsed document in V1
177
-
178
- Upstream behavior note:
179
-
180
- - If rendering a parsed document whose `quill_ref` differs from selected quill name, render proceeds with warning `quill::ref_mismatch`
181
- - Quiver should surface that warning, not suppress it. In V1, this is an intentional loud footgun detector for ref/selection drift.
182
-
183
- ### 9) Distribution Strategy
184
-
185
- **Source-first distribution.** The published artifact is the **Source
186
- Quiver** — an npm package whose root contains `Quiver.yaml`. Consumers
187
- choose how to consume it:
188
-
189
- - **Node consumers** load the source layout directly:
190
- ```ts
191
- const quiver = await Quiver.fromPackage("@org/my-quiver");
192
- ```
193
- - **Browser consumers** run a build step against the resolved source dir
194
- and serve the output as static assets:
195
- ```ts
196
- // build step (Node)
197
- await Quiver.build("./node_modules/@org/my-quiver", "./public/quivers/my-quiver");
198
- // browser runtime
199
- const quiver = await Quiver.fromBuiltUrl("/quivers/my-quiver/");
200
- ```
201
- - **Node server-side runtime consumers** also run the build step, ship the
202
- packed artifact in their deployment image, and load it from disk:
203
- ```ts
204
- await Quiver.build("./node_modules/@org/my-quiver", "./static/quills/my-quiver");
205
- // server runtime
206
- const quiver = await Quiver.fromBuiltDir("./static/quills/my-quiver");
207
- ```
208
- This keeps source quivers as devDependencies and avoids self-fetching
209
- through the deployment's load balancer / CDN.
210
-
211
- Rationale:
212
-
213
- - Author release pipeline is `npm publish` (or `git tag`). No second
214
- artifact, no CDN, no hash bookkeeping outside the npm tarball.
215
- - Deployment topology is the consumer's concern, not the author's.
216
- - The runtime artifact is a build output of the source, not a peer
217
- distribution shape (see §1).
218
-
219
- **Pre-built distribution as a published artifact is supported but not
220
- the default.** Authors who need to ship runtime-ready output directly
221
- (e.g. their consumers cannot run a Node build step) may publish
222
- `Quiver.build(...)` output to a CDN and instruct consumers to use
223
- `Quiver.fromBuiltUrl(<cdn-url>)`. Treated as the exception.
224
-
225
- Validation responsibility shifts left: authors should run
226
- `Quiver.fromDir` and `Quiver.build` in CI so `quiver_invalid` errors
227
- surface on publish, not on the consumer's build. The bundled
228
- `@quillmark/quiver/testing` harness covers this.
229
-
230
- ---
231
-
232
- ## Carryover Matrix (What We Keep)
233
-
234
- V1 intentionally retains:
235
-
236
- 1. Font dehydration as a build-output property
237
- 2. Consumer validation tooling for source layouts (+ optional build-parity checks)
238
- 3. Manifest pointer resolution for build output
239
- 4. HTTP/HTTPS loading via `Quiver.fromBuiltUrl`
240
- 5. Filesystem loading of build output via `Quiver.fromBuiltDir` for Node
241
- server-side runtimes
242
- 6. Source layout loading as first-class dev loop (`fromPackage` / `fromDir`)
243
- 7. Typed errors (`QuiverError`) with quiver/source context
244
- 8. Concurrency coalescing for in-flight loads
245
- 9. Preload/fail-fast helpers where they still add value
246
-
247
- Removed from carryover assumptions:
248
-
249
- - Any engine-registration cache fast path (`register`/`has`) because upstream removed the capability
250
- - "Transport" as a user-facing concept (folded into `fromBuiltUrl` /
251
- `fromBuiltDir` internals)
252
-
253
- ---
254
-
255
- ## Explicit Trims (Surface Reduction)
256
-
257
- V1 should trim public API where behavior can stay internal:
258
-
259
- - Drop internal-only utility exports
260
- - Keep engine payload and loader internals opaque
261
- - Consolidate validation exports
262
- - Avoid duplicate entry points for equivalent validation workflows
263
- - Do not expose internal quill-object cache mechanisms as public contract
264
-
265
- ---
266
-
267
- ## Error Model
268
-
269
- Single `QuiverError` class with `code: string` + contextual payload (ref, version, quiver name, underlying cause where applicable). No subclasses. Fail-fast: operations throw on first failure; no aggregate/partial-success results in V1.
270
-
271
- V1 code catalog (closed set):
272
-
273
- | Code | Fires when |
274
- |---|---|
275
- | `invalid_ref` | Malformed ref string at `resolve()`/`warm()` boundary (fails `parseQuillRef`) |
276
- | `quill_not_found` | Selector did not match any quill in the quiver |
277
- | `quiver_invalid` | `Quiver.yaml` or hashed manifest malformed, unknown field, non-canonical version on disk, or font/bundle hash mismatch |
278
- | `transport_error` | I/O failure: missing path, HTTP non-2xx, network error, permission error. Wraps underlying cause. |
279
-
280
- Notes:
281
- - `quill_not_found` is selector-resolution failure within a quiver's catalog.
282
- - `transport_error` is artifact access failure (filesystem/HTTP/network/permissions), including missing packed files and HTTP 404.
283
- - Legacy categories such as `manifest_invalid`, `quill_load_failed`, and `backend_not_found` are folded into `quiver_invalid` or `transport_error` in V1.
284
-
285
- Errors must include offending ref/version/quiver identifiers when available.
286
-
287
- ---
288
-
289
- ## Runtime + Build Model
290
-
291
- V1 runtime loading paths:
292
-
293
- 1. `Quiver.fromPackage(specifier)` — npm package resolution; loads source
294
- (authoring/dev/Node runtime)
295
- 2. `Quiver.fromDir(path)` — local directory; loads source (Node)
296
- 3. `Quiver.fromBuiltUrl(url)` — HTTP(S); loads build output (browser; also
297
- works in Node)
298
- 4. `Quiver.fromBuiltDir(path)` — local directory; loads build output
299
- (Node server-side runtime)
300
-
301
- V1 build behavior:
302
-
303
- - `Quiver.build(srcDir, outDir)` produces the runtime artifact from a
304
- source layout
305
- - output includes pointer + hashed manifest + bundles + dehydrated font store
306
- (see "Runtime Artifact Format" below)
307
-
308
- Execution behavior:
309
-
310
- - Quiver resolves selector -> canonical ref
311
- - Quiver loads/creates a render-ready `Quill` via engine factory
312
- - Quiver reuses loaded quill objects by canonical ref
313
- - Quiver renders through `quill.render(...)`
314
-
315
- Caching scope:
316
-
317
- - In V1, loaded-quill object reuse is in scope
318
- - Cache eviction policy is out of scope for V1 (can be added as an additive lifecycle control later)
319
-
320
- ---
321
-
322
- ## Source Quiver Layout (normative)
323
-
324
- ```
325
- <root>/
326
- Quiver.yaml
327
- quills/
328
- <name>/
329
- <version>/ # canonical x.y.z
330
- Quill.yaml
331
- ... # quill-local templates, partials, assets, fonts
332
- ```
333
-
334
- - All assets (including fonts) are **quill-local**. No quiver-level shared asset directory in V1.
335
- - Non-canonical version directories are a validation error (`quiver_invalid`).
336
- - Dedup of identical fonts across quills happens at pack time (into `store/<md5>`), not at the source layer.
337
-
338
- ## Runtime Artifact Format (normative)
339
-
340
- Produced by `Quiver.build()`. Authors do not author this layout; consumers
341
- do not need to inspect it. It is an implementation detail of build output,
342
- specified here only because loaders must agree on its shape.
343
-
344
- ```
345
- <root>/
346
- Quiver.json # stable pointer, always this filename
347
- manifest.<md5>.json # hashed manifest, content-addressed
348
- <name>@<version>.<md5>.zip # one bundle per quill, content-addressed
349
- store/
350
- <md5> # raw font bytes, no extension
351
- ```
352
-
353
- **Hash:** MD5 prefix-6, computed with `node:crypto` at `build()` time only (dev/tooling; not browser runtime).
354
-
355
- **Pointer** `Quiver.json`:
356
- ```json
357
- { "manifest": "manifest.abc123.json" }
358
- ```
359
-
360
- **Hashed manifest** `manifest.<md5>.json`:
361
- ```json
362
- {
363
- "version": 1,
364
- "name": "<quiver-name>",
365
- "quills": [
366
- {
367
- "name": "usaf_memo",
368
- "version": "1.2.3",
369
- "bundle": "usaf_memo@1.2.3.def456.zip",
370
- "fonts": { "fonts/roboto.ttf": "md5abc", "fonts/arial.ttf": "md5def" }
371
- }
372
- ]
373
- }
374
- ```
375
-
376
- **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`.
377
-
378
- 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)`.
379
-
380
- ## API Surface (V1)
381
-
382
- Single class, four loaders + one builder. Each loader has one input
383
- shape and one shape-of-thing-loaded; no auto-detection.
384
-
385
- ```ts
386
- class Quiver {
387
- // Node-only loader: resolve an npm specifier against node_modules and
388
- // load the source layout at the package root.
389
- // (From `@quillmark/quiver/node`.)
390
- static fromPackage(specifier: string): Promise<Quiver>;
391
-
392
- // Node-only loader: load source layout from a local directory.
393
- // Also accepts `import.meta.url`-style `file://` URLs (the URL's parent
394
- // directory is used) as a convenience for tests.
395
- // (From `@quillmark/quiver/node`.)
396
- static fromDir(pathOrFileUrl: string): Promise<Quiver>;
397
-
398
- // Browser-safe loader: load build output from an http(s):// or
399
- // origin-relative URL. Throws `transport_error` on file:// inputs.
400
- // (From `@quillmark/quiver` main.)
401
- static fromBuiltUrl(url: string): Promise<Quiver>;
402
-
403
- // Node-only loader: load build output from a local directory (the
404
- // output of Quiver.build). No network. Use for server-side runtime
405
- // when the packed artifact ships in the deployment image.
406
- // (From `@quillmark/quiver/node`.)
407
- static fromBuiltDir(dirPath: string): Promise<Quiver>;
408
-
409
- // Node-only tooling: produce the runtime artifact from a source layout.
410
- // (From `@quillmark/quiver/node`.)
411
- static build(sourceDir: string, outDir: string, opts?: BuildOptions): Promise<void>;
412
-
413
- readonly name: string; // from Quiver.yaml
414
-
415
- // Read-only introspection and lazy tree access; also used internally by
416
- // resolve/getQuill/warm.
417
- quillNames(): string[]; // sorted lex
418
- versionsOf(name: string): string[]; // sorted desc
419
- loadTree(name: string, version: string): Promise<Map<string, Uint8Array>>;
420
-
421
- // Selector ref -> canonical ref. Throws invalid_ref / quill_not_found.
422
- resolve(ref: string): Promise<string>;
423
-
424
- // Selector or canonical ref -> render-ready quill handle (materialized via
425
- // engine.quill(tree), cached per (engine, canonical-ref)).
426
- getQuill(ref: string, opts: { engine: Quillmark }): Promise<Quill>;
427
-
428
- // Prefetches every quill tree (network-only; engine not required).
429
- // Subsequent getQuill calls reuse the cached tree. Fail-fast.
430
- warm(): Promise<void>;
431
- }
432
-
433
- class QuiverError extends Error {
434
- code: "invalid_ref" | "quill_not_found" | "quiver_invalid" | "transport_error";
435
- // plus contextual payload fields
436
- }
437
- ```
438
-
439
- **No render wrapper.** Callers invoke `quill.render(doc, opts?)` (and `quill.open(doc)` when needed) after `getQuill()`. Quiver never mirrors Quillmark render APIs.
440
-
441
- **Internal (not exported):** `BuiltManifest` (runtime shape), `parseQuillRef`, in-flight coalescing state, source-vs-built layout detection.
442
-
443
- Hot-path flow:
444
- ```ts
445
- const doc = Document.fromMarkdown(md);
446
- const quill = await quiver.getQuill(doc.quillRef, { engine });
447
- const result = quill.render(doc, { format: "pdf" });
448
- ```
449
-
450
- ## Package Structure
451
-
452
- **Name:** `@quillmark/quiver`
453
-
454
- **Entrypoints:**
455
- - `@quillmark/quiver` (main, browser-safe): `Quiver` class with only
456
- `fromBuiltUrl` functional (Node-only loaders/builder throw
457
- `transport_error` if reached in browser), `QuiverError`,
458
- `QuillmarkLike`, `QuillLike`, shared types.
459
- - `@quillmark/quiver/node`: adds `Quiver.fromPackage`, `Quiver.fromDir`,
460
- `Quiver.fromBuiltDir`, `Quiver.build` behaviors. Single `Quiver` class —
461
- Node-only factories fail fast outside Node.
462
- - `@quillmark/quiver/testing` (Node-only): single export
463
- `runQuiverTests(metaUrlOrDir, engine)` built on `node:test` (zero
464
- external test-runner dependency). Optional convenience; users on other
465
- test runners wire their own loops against the main API.
466
-
467
- **Dependencies:**
468
- - Peer: `@quillmark/wasm@>=0.71.0` with `Quillmark`, `Document.fromMarkdown`, `engine.quill(tree)`, and `quill.render(doc, opts?)` APIs.
469
- - Runtime: `fflate ^0.8.2` for zip read/write (Node + browser)
470
- - Dev-only: `node:crypto` (MD5 hashing in `build()` — never reached at runtime)
471
- - No test-runner peer dependency; `/testing` uses `node:test` (built-in)
472
-
473
- ---
474
-
475
- ## Out of Scope for V1
476
-
477
- - Non-Node consumers of Quillmark (e.g. Python bindings, Rust CLI) as deliverables of this program — `@quillmark/quiver` is the Node/npm package only
478
- - Quiver CLI (`quiver init`, etc.)
479
- - prerelease semver support
480
- - semver range expression support
481
- - quiver-declared precedence/priority
482
- - inter-quiver dependency graph in `Quiver.yaml`
483
- - marketplace/discovery service
484
- - advanced warm strategies beyond API-compatible hooks
485
- - multi-quiver composition (single quiver per consumer in V1)
486
-
487
- ---
488
-
489
- ## Planner Questions — Resolved
490
-
491
- All V1 planner questions resolved; implementation plan can proceed against the spec above.
492
-
493
- 1. ~~Final `Quiver` interface shape and transport factoring style~~ → Single `Quiver` class, four loaders (`fromPackage`, `fromDir`, `fromBuiltUrl`, `fromBuiltDir`) + one builder (`build`). Each loader names what it loads (source/built) and where it loads from (package/dir/url); no auto-detection.
494
- 2. ~~Final `Quiver.yaml` schema and unknown-field policy~~ → See §2: alphanumeric `name` and optional tooling-only `description`. Unknown fields are `quiver_invalid`.
495
- 3. ~~Canonical ref grammar and parser API contract~~ → Internal `parseQuillRef`, not exported. Selector syntax per §5. Throws `invalid_ref`.
496
- 4. ~~Exact warning policy for shadowed refs across quivers~~ → N/A in V1: no multi-quiver composition layer; each `Quiver` instance is independent (§4).
497
- 5. ~~Validation API shape consolidation~~ → No separate validation API. Validation errors surface as `QuiverError('quiver_invalid')` during load or `build()`.
498
- 6. ~~Build output directory structure~~ → See "Runtime Artifact Format (normative)".
499
- 7. ~~Node/browser entrypoint split~~ → See "Package Structure": main + `/node` subpath, single `Quiver` class.
500
- 8. ~~Final exported type names~~ → `Quiver`, `QuiverError`. Hot-path entry is `Quiver.getQuill(ref, { engine })`.
501
-
502
- ---
503
-
504
- ## Success Criteria
505
-
506
- - A team can author and validate a Source Quiver locally with fast filesystem loops
507
- - Build output can be loaded over HTTP/HTTPS with parity behavior in browser and Node
508
- - Each loader names exactly what it loads — no auto-detection, no ambiguous return shape
509
- - Multi-quiver resolution is deterministic and matches precedence hard-filter rules
510
- - Selector behavior is predictable and explicitly documented
511
- - Quiver (Node) integrates via `engine.quill(tree)` + `quill.render(...)` only (no engine quill registration path)
512
- - Public API surface is smaller and clearer than `@quillmark/registry`
@@ -1,28 +0,0 @@
1
- /**
2
- * Minimal structural types matching @quillmark/wasm >=0.79.0.
3
- *
4
- * Shape:
5
- * class Quillmark { quill(tree: Map<string, Uint8Array>): Quill }
6
- * class Quill { render(doc, opts?): RenderResult; open(doc): RenderSession }
7
- *
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
- *
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
- *
17
- * Call-site note: Quiver never invokes `render` or `open` itself; consumers do
18
- * after `getQuill()`. The loose `unknown` parameter typing is intentional.
19
- */
20
- export interface QuillmarkLike {
21
- quill(tree: Map<string, Uint8Array>): QuillLike;
22
- }
23
- export interface QuillLike {
24
- render(doc: unknown, opts?: unknown): unknown;
25
- open?: (doc: unknown) => unknown;
26
- /** Auto-generated annotated Markdown blueprint — always present in @quillmark/wasm >=0.79.0. */
27
- blueprint: string;
28
- }
@@ -1,20 +0,0 @@
1
- /**
2
- * Minimal structural types matching @quillmark/wasm >=0.79.0.
3
- *
4
- * Shape:
5
- * class Quillmark { quill(tree: Map<string, Uint8Array>): Quill }
6
- * class Quill { render(doc, opts?): RenderResult; open(doc): RenderSession }
7
- *
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
- *
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
- *
17
- * Call-site note: Quiver never invokes `render` or `open` itself; consumers do
18
- * after `getQuill()`. The loose `unknown` parameter typing is intentional.
19
- */
20
- export {};