@quillmark/quiver 0.20.0 → 0.22.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/dist/build.js CHANGED
@@ -8,7 +8,7 @@
8
8
  import { QuiverError } from './errors.js';
9
9
  import { packFiles } from './bundle.js';
10
10
  import { NAME_DIGEST_LENGTH } from './digest.js';
11
- import { POINTER_FORMAT } from './format.js';
11
+ import { MANIFEST_VERSION, POINTER_FORMAT } from './format.js';
12
12
  /** Font file extensions recognised by the builder (case-insensitive). */
13
13
  const FONT_EXT = /\.(ttf|otf|woff|woff2)$/i;
14
14
  /** The two trees a build owns beside its output: where a generation is assembled, and
@@ -77,12 +77,9 @@ export async function buildQuiver(sourceDir, outDir) {
77
77
  const { existsSync } = await import('node:fs');
78
78
  const { createHash } = await import('node:crypto');
79
79
  const { scanSourceQuiver, readQuillTree } = await import('./source-loader.js');
80
- // 0. Three trees get cleared here; refuse the paths where that deletes the
81
- // caller instead of a previous build.
82
80
  assertSafeOutDir(path, sourceDir, outDir);
83
81
  const out = resolve(outDir);
84
82
  const { stage, prev } = stagesOf(out);
85
- // 1. Scan + validate source quiver (throws quiver_invalid on bad input).
86
83
  const { meta, catalog } = await scanSourceQuiver(sourceDir);
87
84
  try {
88
85
  await packGeneration();
@@ -103,7 +100,6 @@ export async function buildQuiver(sourceDir, outDir) {
103
100
  * wide against the seconds a build takes.
104
101
  */
105
102
  async function packGeneration() {
106
- // 2. Clear and recreate the staged tree + its store/.
107
103
  try {
108
104
  await rm(stage, { recursive: true, force: true });
109
105
  await mkdir(join(stage, 'store'), { recursive: true });
@@ -111,14 +107,11 @@ export async function buildQuiver(sourceDir, outDir) {
111
107
  catch (err) {
112
108
  throw new QuiverError('transport_error', `Failed to prepare output directory "${outDir}": ${err.message}`, { cause: err });
113
109
  }
114
- // 3. Process each quill version.
115
110
  const manifestQuills = [];
116
111
  for (const [quillName, versions] of catalog) {
117
112
  for (const version of versions) {
118
113
  const quillDir = join(sourceDir, 'quills', quillName, version);
119
- // a. Read quill file tree.
120
114
  const tree = await readQuillTree(quillDir);
121
- // b. Partition fonts vs content.
122
115
  const fontEntries = [];
123
116
  const contentEntries = [];
124
117
  for (const [path, bytes] of tree) {
@@ -129,7 +122,6 @@ export async function buildQuiver(sourceDir, outDir) {
129
122
  contentEntries.push([path, bytes]);
130
123
  }
131
124
  }
132
- // c. Dehydrate fonts into store/.
133
125
  const fonts = {};
134
126
  for (const [path, bytes] of fontEntries) {
135
127
  // Full width: the store is keyed by hash, so two distinct fonts
@@ -143,33 +135,29 @@ export async function buildQuiver(sourceDir, outDir) {
143
135
  }
144
136
  fonts[path] = hash;
145
137
  }
146
- // d. Zip content files (deterministic: sorted paths, fixed mtime).
147
138
  const contentRecord = {};
148
139
  for (const [path, bytes] of contentEntries) {
149
140
  contentRecord[path] = bytes;
150
141
  }
151
142
  const zipBytes = packFiles(contentRecord);
152
- // e–f. Compute bundle hash and name.
153
143
  const bundleHash = createHash('sha256')
154
144
  .update(zipBytes)
155
145
  .digest('hex')
156
146
  .slice(0, NAME_DIGEST_LENGTH);
157
147
  const bundleName = `${quillName}@${version}.${bundleHash}.zip`;
158
- // g. Write bundle zip.
159
148
  try {
160
149
  await writeFile(join(stage, bundleName), zipBytes);
161
150
  }
162
151
  catch (err) {
163
152
  throw new QuiverError('transport_error', `Failed to write bundle "${join(outDir, bundleName)}": ${err.message}`, { cause: err });
164
153
  }
165
- // h. Record manifest entry.
166
154
  manifestQuills.push({ name: quillName, version, bundle: bundleName, fonts });
167
155
  }
168
156
  }
169
- // 4–8. Build and write hashed manifest.
170
157
  const manifest = {
171
- version: 1,
158
+ version: MANIFEST_VERSION,
172
159
  name: meta.name,
160
+ ...(meta.description === undefined ? {} : { description: meta.description }),
173
161
  quills: manifestQuills
174
162
  };
175
163
  const manifestJson = JSON.stringify(manifest, null, 2);
@@ -184,8 +172,8 @@ export async function buildQuiver(sourceDir, outDir) {
184
172
  catch (err) {
185
173
  throw new QuiverError('transport_error', `Failed to write manifest "${join(outDir, manifestFileName)}": ${err.message}`, { cause: err });
186
174
  }
187
- // 9–10. Write stable pointer latest.json. The format is stamped here and read first,
188
- // so a client older than the tree says so rather than misreading it.
175
+ // The format is stamped here and read first, so a client older than the tree
176
+ // says so rather than misreading it.
189
177
  const pointer = { format: POINTER_FORMAT, manifest: manifestFileName };
190
178
  try {
191
179
  await writeFile(join(stage, 'latest.json'), JSON.stringify(pointer), 'utf-8');
@@ -193,9 +181,8 @@ export async function buildQuiver(sourceDir, outDir) {
193
181
  catch (err) {
194
182
  throw new QuiverError('transport_error', `Failed to write pointer "${join(outDir, 'latest.json')}": ${err.message}`, { cause: err });
195
183
  }
196
- // 11. Move the generation in. `prev` is cleared first: a rename onto a
197
- // non-empty directory fails, and the tree left by an interrupted run is
198
- // one.
184
+ // `prev` is cleared first: a rename onto a non-empty directory fails, and the
185
+ // tree left by an interrupted run is one.
199
186
  try {
200
187
  await mkdir(dirname(out), { recursive: true });
201
188
  await rm(prev, { recursive: true, force: true });
@@ -22,12 +22,5 @@ export interface BuiltTransport {
22
22
  export interface FetchOptions {
23
23
  revalidate?: boolean;
24
24
  }
25
- /**
26
- * Load a build-output quiver via the given transport.
27
- *
28
- * 1. Fetches latest.json (pointer) and parses it.
29
- * 2. Fetches the manifest file it points to and validates it.
30
- * 3. Builds a catalog from manifest entries (versions sorted descending).
31
- * 4. Returns a Quiver instance backed by a BuiltLoader.
32
- */
25
+ /** Load a build-output quiver via the given transport. */
33
26
  export declare function loadBuiltQuiver(transport: BuiltTransport): Promise<Quiver>;
@@ -9,7 +9,7 @@ import { QuiverError } from './errors.js';
9
9
  import { unpackFiles } from './bundle.js';
10
10
  import { isCanonicalSemver, compareSemver } from './semver.js';
11
11
  import { NAME_DIGEST_LENGTH, sha256Hex } from './digest.js';
12
- import { POINTER_FORMAT } from './format.js';
12
+ import { MANIFEST_VERSION, POINTER_FORMAT } from './format.js';
13
13
  import { createQuiver } from './quiver.js';
14
14
  /**
15
15
  * Fetch, then check the bytes against the digest their name carries. That
@@ -68,15 +68,12 @@ class BuiltLoader {
68
68
  // The catalog the outer Quiver resolves against derives from this index,
69
69
  // so every ref reaching here has an entry.
70
70
  const entry = this.index.get(`${name}@${version}`);
71
- // 1. Fetch + unpack bundle zip.
72
71
  const zipBytes = await fetchVerified(this.transport, entry.bundle, digestOfName(BUNDLE_FILENAME_RE, entry.bundle, 'bundle filename'));
73
72
  const files = unpackFiles(zipBytes);
74
- // 2. Rehydrate fonts from store (coalesced).
75
73
  const fontEntries = Object.entries(entry.fonts);
76
74
  await Promise.all(fontEntries.map(async ([path, hash]) => {
77
75
  files[path] = await this.fetchFont(hash);
78
76
  }));
79
- // 3. Convert to Map.
80
77
  return new Map(Object.entries(files));
81
78
  }
82
79
  /**
@@ -116,7 +113,7 @@ function parsePointer(raw) {
116
113
  throw new QuiverError('quiver_invalid', 'latest.json must be a JSON object');
117
114
  }
118
115
  const obj = parsed;
119
- // The format, read before anything is believed about the rest. An unknown key is NOT
116
+ // The format, read before anything is believed about the rest. An unknown key is not
120
117
  // a failure here: this is the document a newer format announces itself on, and a
121
118
  // reader rejecting what it does not recognise refuses the announcement too. Absent
122
119
  // means a build from before the marker (`format.ts`).
@@ -145,13 +142,21 @@ function parseManifest(raw) {
145
142
  throw new QuiverError('quiver_invalid', 'Manifest must be a JSON object');
146
143
  }
147
144
  const obj = parsed;
148
- assertNoUnknownKeys(obj, ['version', 'name', 'quills'], 'manifest');
149
- if (obj['version'] !== 1) {
150
- throw new QuiverError('quiver_invalid', `Manifest version must be 1, got ${String(obj['version'])}`);
145
+ assertNoUnknownKeys(obj, ['version', 'name', 'description', 'quills'], 'manifest');
146
+ const version = obj['version'];
147
+ if (typeof version !== 'number' || !Number.isInteger(version) || version < 1) {
148
+ throw new QuiverError('quiver_invalid', `Manifest version must be a positive integer, got ${JSON.stringify(version)}`);
149
+ }
150
+ if (version > MANIFEST_VERSION) {
151
+ throw new QuiverError('quiver_invalid', `This quiver's manifest is version ${version} and this loader reads ${MANIFEST_VERSION}. ` +
152
+ `Upgrade @quillmark/quiver, or for a served client the client itself, which carries the copy that reads this.`);
151
153
  }
152
154
  if (typeof obj['name'] !== 'string' || obj['name'].length === 0) {
153
155
  throw new QuiverError('quiver_invalid', 'Manifest must have a non-empty string "name" field');
154
156
  }
157
+ if (obj['description'] !== undefined && typeof obj['description'] !== 'string') {
158
+ throw new QuiverError('quiver_invalid', `Manifest "description" must be a string if present, got ${typeof obj['description']}`);
159
+ }
155
160
  if (!Array.isArray(obj['quills'])) {
156
161
  throw new QuiverError('quiver_invalid', 'Manifest must have a "quills" array');
157
162
  }
@@ -191,8 +196,8 @@ function parseManifest(raw) {
191
196
  });
192
197
  }
193
198
  return {
194
- version: 1,
195
199
  name: obj['name'],
200
+ description: obj['description'],
196
201
  quills
197
202
  };
198
203
  }
@@ -215,17 +220,10 @@ function catalogOf(index) {
215
220
  return catalog;
216
221
  }
217
222
  // ─── Main entry point ─────────────────────────────────────────────────────────
218
- /**
219
- * Load a build-output quiver via the given transport.
220
- *
221
- * 1. Fetches latest.json (pointer) and parses it.
222
- * 2. Fetches the manifest file it points to and validates it.
223
- * 3. Builds a catalog from manifest entries (versions sorted descending).
224
- * 4. Returns a Quiver instance backed by a BuiltLoader.
225
- */
223
+ /** Load a build-output quiver via the given transport. */
226
224
  export async function loadBuiltQuiver(transport) {
227
- // 1. Fetch and parse pointer. It is the one name that is not
228
- // content-addressed, so it is also the one fetch that must revalidate.
225
+ // The pointer is the one name that is not content-addressed, so it is also the
226
+ // one fetch that must revalidate.
229
227
  let pointerBytes;
230
228
  try {
231
229
  pointerBytes = await transport.fetchBytes('latest.json', { revalidate: true });
@@ -237,7 +235,6 @@ export async function loadBuiltQuiver(transport) {
237
235
  }
238
236
  const manifestFileName = parsePointer(new TextDecoder().decode(pointerBytes));
239
237
  const manifestDigest = digestOfName(MANIFEST_FILENAME_RE, manifestFileName, 'Pointer manifest filename');
240
- // 2. Fetch and parse manifest.
241
238
  let manifestBytes;
242
239
  try {
243
240
  manifestBytes = await fetchVerified(transport, manifestFileName, manifestDigest);
@@ -247,7 +244,6 @@ export async function loadBuiltQuiver(transport) {
247
244
  throw err;
248
245
  throw new QuiverError('transport_error', `Failed to fetch manifest "${manifestFileName}": ${err.message}`, { cause: err });
249
246
  }
250
- // 3–4. Validate manifest + assemble catalog/loader.
251
247
  const manifest = parseManifest(new TextDecoder().decode(manifestBytes));
252
248
  const index = new Map();
253
249
  for (const entry of manifest.quills) {
@@ -257,5 +253,5 @@ export async function loadBuiltQuiver(transport) {
257
253
  }
258
254
  index.set(key, entry);
259
255
  }
260
- return createQuiver(manifest.name, catalogOf(index), new BuiltLoader(transport, index));
256
+ return createQuiver(manifest.name, manifest.description, catalogOf(index), new BuiltLoader(transport, index));
261
257
  }
package/dist/format.d.ts CHANGED
@@ -3,9 +3,19 @@
3
3
  * (QUIVER §"The pointer" for why the pointer carries it and reads past keys it does not
4
4
  * know).
5
5
  *
6
- * It moves when a reader assuming it would MISREAD a tree rather than fail on it: a
6
+ * It moves when a reader assuming it would misread a tree rather than fail on it: a
7
7
  * renamed pointer field, a second pointer, a store keyed by something other than the
8
8
  * full hash. A change confined to the manifest's own shape moves the manifest's
9
9
  * `version` instead.
10
10
  */
11
11
  export declare const POINTER_FORMAT = 1;
12
+ /**
13
+ * The manifest's shape, stated in the document and refused to be read past. Version 2
14
+ * carries the quiver's `description`; version 1 is the same document without it.
15
+ *
16
+ * The manifest is closed, so a field added to it is a field an older reader rejects as an
17
+ * unknown key, naming neither what happened nor what to do. The version is what that
18
+ * reader reads first instead, and a manifest above its own is refused with the upgrade
19
+ * named.
20
+ */
21
+ export declare const MANIFEST_VERSION = 2;
package/dist/format.js CHANGED
@@ -3,9 +3,19 @@
3
3
  * (QUIVER §"The pointer" for why the pointer carries it and reads past keys it does not
4
4
  * know).
5
5
  *
6
- * It moves when a reader assuming it would MISREAD a tree rather than fail on it: a
6
+ * It moves when a reader assuming it would misread a tree rather than fail on it: a
7
7
  * renamed pointer field, a second pointer, a store keyed by something other than the
8
8
  * full hash. A change confined to the manifest's own shape moves the manifest's
9
9
  * `version` instead.
10
10
  */
11
11
  export const POINTER_FORMAT = 1;
12
+ /**
13
+ * The manifest's shape, stated in the document and refused to be read past. Version 2
14
+ * carries the quiver's `description`; version 1 is the same document without it.
15
+ *
16
+ * The manifest is closed, so a field added to it is a field an older reader rejects as an
17
+ * unknown key, naming neither what happened nor what to do. The version is what that
18
+ * reader reads first instead, and a manifest above its own is refused with the upgrade
19
+ * named.
20
+ */
21
+ export const MANIFEST_VERSION = 2;
package/dist/node.d.ts CHANGED
@@ -4,9 +4,8 @@
4
4
  * from the main entry, unmodified. Importing this module installs nothing, so
5
5
  * the import path is the whole contract and a bundler drops what is not called.
6
6
  *
7
- * Bundler note: this entry pulls in `./source-loader.js`, `./build.js`, and
8
- * `./transports/fs-built-transport.js`, all of which statically import `node:*`
9
- * builtins. Browser bundles must never reach it. The main entry (`./index.js`)
7
+ * The modules behind these factories statically import `node:*` builtins, so a
8
+ * browser bundle must never reach this entry. The main entry (`./index.js`)
10
9
  * makes no static or dynamic reference to it.
11
10
  */
12
11
  import { Quiver } from './quiver.js';
package/dist/node.js CHANGED
@@ -4,9 +4,8 @@
4
4
  * from the main entry, unmodified. Importing this module installs nothing, so
5
5
  * the import path is the whole contract and a bundler drops what is not called.
6
6
  *
7
- * Bundler note: this entry pulls in `./source-loader.js`, `./build.js`, and
8
- * `./transports/fs-built-transport.js`, all of which statically import `node:*`
9
- * builtins. Browser bundles must never reach it. The main entry (`./index.js`)
7
+ * The modules behind these factories statically import `node:*` builtins, so a
8
+ * browser bundle must never reach this entry. The main entry (`./index.js`)
10
9
  * makes no static or dynamic reference to it.
11
10
  */
12
11
  import { createQuiver } from './quiver.js';
@@ -35,7 +34,7 @@ export async function fromDir(pathOrFileUrl) {
35
34
  ? fileURLToPath(new URL('.', pathOrFileUrl))
36
35
  : pathOrFileUrl;
37
36
  const { meta, catalog } = await scanSourceQuiver(dir);
38
- return createQuiver(meta.name, catalog, new SourceLoader(dir));
37
+ return createQuiver(meta.name, meta.description, catalog, new SourceLoader(dir));
39
38
  }
40
39
  /**
41
40
  * Loads a packed (build-output) quiver from a local directory containing
@@ -1,9 +1,7 @@
1
1
  /**
2
- * Internal parser/validator for Quiver.yaml files.
3
- *
4
- * Uses the `yaml` npm package for robust YAML parsing. Quiver.yaml has a
5
- * simple two-field schema (name, description), but using a proper YAML parser
6
- * ensures correct handling of quoting, escaping, and multi-line strings.
2
+ * Internal parser/validator for Quiver.yaml: two fields, `name` and
3
+ * `description`, strict about everything else. Parsed through the `yaml`
4
+ * package, so quoting, escaping and multi-line strings behave as YAML.
7
5
  */
8
6
  export interface QuiverMeta {
9
7
  name: string;
@@ -1,9 +1,7 @@
1
1
  /**
2
- * Internal parser/validator for Quiver.yaml files.
3
- *
4
- * Uses the `yaml` npm package for robust YAML parsing. Quiver.yaml has a
5
- * simple two-field schema (name, description), but using a proper YAML parser
6
- * ensures correct handling of quoting, escaping, and multi-line strings.
2
+ * Internal parser/validator for Quiver.yaml: two fields, `name` and
3
+ * `description`, strict about everything else. Parsed through the `yaml`
4
+ * package, so quoting, escaping and multi-line strings behave as YAML.
7
5
  */
8
6
  import { parse as parseYaml } from 'yaml';
9
7
  import { QuiverError } from './errors.js';
@@ -32,13 +30,11 @@ export function parseQuiverYaml(raw) {
32
30
  throw new QuiverError('quiver_invalid', `Quiver.yaml: expected a mapping at the top level, got ${Array.isArray(parsed) ? 'array' : String(parsed)}`);
33
31
  }
34
32
  const doc = parsed;
35
- // Check for unknown fields (strict mode)
36
33
  for (const key of Object.keys(doc)) {
37
34
  if (!KNOWN_FIELDS.has(key)) {
38
35
  throw new QuiverError('quiver_invalid', `Quiver.yaml: unknown field "${key}" — only "name" and "description" are valid in V1`);
39
36
  }
40
37
  }
41
- // Validate `name`
42
38
  if (!('name' in doc)) {
43
39
  throw new QuiverError('quiver_invalid', `Quiver.yaml: required field "name" is missing`);
44
40
  }
@@ -49,7 +45,6 @@ export function parseQuiverYaml(raw) {
49
45
  if (!NAME_RE.test(name)) {
50
46
  throw new QuiverError('quiver_invalid', `Quiver.yaml: "name" value "${name}" contains invalid characters — only [A-Za-z0-9_-] are allowed`);
51
47
  }
52
- // Validate optional `description`
53
48
  if ('description' in doc && doc['description'] !== undefined) {
54
49
  if (typeof doc['description'] !== 'string') {
55
50
  throw new QuiverError('quiver_invalid', `Quiver.yaml: "description" must be a string if present, got ${typeof doc['description']}`);
package/dist/quiver.d.ts CHANGED
@@ -1,12 +1,11 @@
1
1
  /**
2
- * Quiver — primary runtime abstraction for a collection of quills.
2
+ * A collection of quills, addressed by ref. One class over a composed
3
+ * `QuiverLoader`, source-backed or built, so what a quiver reads is the loader's
4
+ * business and never the class's.
3
5
  *
4
- * Polymorphism via composition: internally stores a pluggable loader
5
- * (either source-backed or build-output-backed).
6
- *
7
- * This module is browser-safe: only `fromBuiltUrl`, `fromBuiltFiles` and the
8
- * instance API live here. The filesystem factories are free functions in
9
- * `./node.js`; the class is the same either way.
6
+ * Browser-safe: only `fromBuiltUrl`, `fromBuiltFiles` and the instance API live
7
+ * here. The filesystem factories are free functions in `./node.js`; the class is
8
+ * the same either way.
10
9
  */
11
10
  import type { Quill } from '@quillmark/wasm';
12
11
  /** Loader strategy: source or build output. Package-internal. */
@@ -18,10 +17,12 @@ export interface QuiverLoader {
18
17
  * Exported as `createQuiver` for this package's loaders and reachable from no
19
18
  * entry in `exports`, so a consumer holds the factories and nothing else.
20
19
  */
21
- declare let create: (name: string, catalog: Map<string, string[]>, loader: QuiverLoader) => Quiver;
20
+ declare let create: (name: string, description: string | undefined, catalog: Map<string, string[]>, loader: QuiverLoader) => Quiver;
22
21
  export declare class Quiver {
23
22
  #private;
24
23
  readonly name: string;
24
+ /** What `Quiver.yaml` says the collection is; `undefined` where the field is absent. */
25
+ readonly description: string | undefined;
25
26
  /**
26
27
  * Private constructor. A Quiver comes from a factory (`Quiver.fromBuiltUrl`,
27
28
  * `Quiver.fromBuiltFiles`, or `fromDir` / `fromBuiltDir` from
@@ -101,7 +102,7 @@ export declare class Quiver {
101
102
  * first. Materializes once and caches per canonical ref — concurrent calls
102
103
  * coalesce into a single load.
103
104
  *
104
- * BORROWED, not handed over: the quill is the quiver's, shared with every
105
+ * Borrowed, not handed over: the quill is the quiver's, shared with every
105
106
  * other caller for the same ref, and lives as long as the quiver. Do not
106
107
  * `free()` it: the next `getQuill` for that ref would hand out a freed
107
108
  * handle. A caller wanting one of its own mints it from `.toTree()`.
package/dist/quiver.js CHANGED
@@ -1,12 +1,11 @@
1
1
  /**
2
- * Quiver — primary runtime abstraction for a collection of quills.
2
+ * A collection of quills, addressed by ref. One class over a composed
3
+ * `QuiverLoader`, source-backed or built, so what a quiver reads is the loader's
4
+ * business and never the class's.
3
5
  *
4
- * Polymorphism via composition: internally stores a pluggable loader
5
- * (either source-backed or build-output-backed).
6
- *
7
- * This module is browser-safe: only `fromBuiltUrl`, `fromBuiltFiles` and the
8
- * instance API live here. The filesystem factories are free functions in
9
- * `./node.js`; the class is the same either way.
6
+ * Browser-safe: only `fromBuiltUrl`, `fromBuiltFiles` and the instance API live
7
+ * here. The filesystem factories are free functions in `./node.js`; the class is
8
+ * the same either way.
10
9
  */
11
10
  import { QuiverError } from './errors.js';
12
11
  import { init } from '@quillmark/wasm';
@@ -20,10 +19,12 @@ import { matchesSemverSelector, chooseHighestVersion } from './semver.js';
20
19
  let create;
21
20
  export class Quiver {
22
21
  name;
22
+ /** What `Quiver.yaml` says the collection is; `undefined` where the field is absent. */
23
+ description;
23
24
  #catalog;
24
25
  #loader;
25
26
  /**
26
- * Cache of materialized quills, keyed by canonical ref. A `Quill` is now
27
+ * Cache of materialized quills, keyed by canonical ref. A `Quill` is
27
28
  * engine-free, portable data (`Quill.fromTree`), so one instance per ref is
28
29
  * shared across every engine. Promise values so concurrent getQuill calls
29
30
  * coalesce into a single materialization.
@@ -34,13 +35,14 @@ export class Quiver {
34
35
  * `Quiver.fromBuiltFiles`, or `fromDir` / `fromBuiltDir` from
35
36
  * `@quillmark/quiver/node`), which is what names the thing being read.
36
37
  */
37
- constructor(name, catalog, loader) {
38
+ constructor(name, description, catalog, loader) {
38
39
  this.name = name;
40
+ this.description = description;
39
41
  this.#catalog = new Map(catalog);
40
42
  this.#loader = loader;
41
43
  }
42
44
  static {
43
- create = (name, catalog, loader) => new Quiver(name, catalog, loader);
45
+ create = (name, description, catalog, loader) => new Quiver(name, description, catalog, loader);
44
46
  }
45
47
  /**
46
48
  * Browser-safe factory. Loads build output from an HTTP/HTTPS URL.
@@ -146,7 +148,7 @@ export class Quiver {
146
148
  * first. Materializes once and caches per canonical ref — concurrent calls
147
149
  * coalesce into a single load.
148
150
  *
149
- * BORROWED, not handed over: the quill is the quiver's, shared with every
151
+ * Borrowed, not handed over: the quill is the quiver's, shared with every
150
152
  * other caller for the same ref, and lives as long as the quiver. Do not
151
153
  * `free()` it: the next `getQuill` for that ref would hand out a freed
152
154
  * handle. A caller wanting one of its own mints it from `.toTree()`.
package/dist/ref.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /** Internal parsed representation of a quill reference. */
2
2
  export interface ParsedQuillRef {
3
3
  name: string;
4
- /** Undefined = "highest in first-winning quiver". */
4
+ /** Undefined selects the highest version present. */
5
5
  selector?: string;
6
6
  /** Selector part count: 1 = `x`, 2 = `x.y`, 3 = `x.y.z` (exact). */
7
7
  selectorDepth?: 1 | 2 | 3;
package/dist/ref.js CHANGED
@@ -12,18 +12,15 @@ export function parseQuillRef(ref) {
12
12
  }
13
13
  const atIndex = ref.indexOf('@');
14
14
  if (atIndex === 0) {
15
- // Starts with @, no name
16
15
  throw new QuiverError('invalid_ref', `Invalid ref: missing name in "${ref}"`, { ref });
17
16
  }
18
17
  if (atIndex === -1) {
19
- // No selector — just a name
20
18
  const name = ref;
21
19
  if (!NAME_RE.test(name)) {
22
20
  throw new QuiverError('invalid_ref', `Invalid ref: name "${name}" contains invalid characters`, { ref });
23
21
  }
24
22
  return { name };
25
23
  }
26
- // Has @
27
24
  const name = ref.slice(0, atIndex);
28
25
  const selector = ref.slice(atIndex + 1);
29
26
  if (!name) {
@@ -1,8 +1,6 @@
1
1
  /**
2
- * Internal filesystem scanner for Source Quiver layout.
3
- *
4
- * Uses Node.js `fs/promises` — this module must only be imported from
5
- * Node-only contexts (`fromDir`, `build`).
2
+ * Internal filesystem scanner for the Source Quiver layout. Statically imports
3
+ * `node:fs`, so it is reachable from the Node entry alone.
6
4
  */
7
5
  import type { QuiverMeta } from './quiver-yaml.js';
8
6
  import type { QuiverLoader } from './quiver.js';
@@ -17,7 +15,7 @@ import type { QuiverLoader } from './quiver.js';
17
15
  * non-canonical, or a version dir is missing its Quill.yaml sentinel.
18
16
  * - `transport_error` for I/O failures (permissions, etc.).
19
17
  *
20
- * Missing `quills/` directory is NOT an error — the quiver is valid but empty.
18
+ * Missing `quills/` directory is not an error — the quiver is valid but empty.
21
19
  */
22
20
  export declare function scanSourceQuiver(rootDir: string): Promise<{
23
21
  meta: QuiverMeta;
@@ -32,8 +30,8 @@ export declare function scanSourceQuiver(rootDir: string): Promise<{
32
30
  export declare function readQuillTree(quillDir: string): Promise<Map<string, Uint8Array>>;
33
31
  /**
34
32
  * Source-backed QuiverLoader: loads file trees from a Source Quiver on disk.
35
- * The outer Quiver.loadTree already validates name/version against the catalog,
36
- * so this loader trusts the gate and goes straight to reading files.
33
+ * `Quiver` resolves every ref against its catalog before reaching a loader, so
34
+ * the name and version arrive catalog-backed and go straight to the disk.
37
35
  */
38
36
  export declare class SourceLoader implements QuiverLoader {
39
37
  private readonly rootDir;
@@ -1,8 +1,6 @@
1
1
  /**
2
- * Internal filesystem scanner for Source Quiver layout.
3
- *
4
- * Uses Node.js `fs/promises` — this module must only be imported from
5
- * Node-only contexts (`fromDir`, `build`).
2
+ * Internal filesystem scanner for the Source Quiver layout. Statically imports
3
+ * `node:fs`, so it is reachable from the Node entry alone.
6
4
  */
7
5
  import { readdir, readFile, stat } from 'node:fs/promises';
8
6
  import { join, relative, sep } from 'node:path';
@@ -20,10 +18,9 @@ import { isCanonicalSemver, compareSemver } from './semver.js';
20
18
  * non-canonical, or a version dir is missing its Quill.yaml sentinel.
21
19
  * - `transport_error` for I/O failures (permissions, etc.).
22
20
  *
23
- * Missing `quills/` directory is NOT an error — the quiver is valid but empty.
21
+ * Missing `quills/` directory is not an error — the quiver is valid but empty.
24
22
  */
25
23
  export async function scanSourceQuiver(rootDir) {
26
- // --- Read Quiver.yaml ---
27
24
  const quiverYamlPath = join(rootDir, 'Quiver.yaml');
28
25
  let raw;
29
26
  try {
@@ -42,7 +39,6 @@ export async function scanSourceQuiver(rootDir) {
42
39
  throw new QuiverError('transport_error', `Failed to read "Quiver.yaml" at "${quiverYamlPath}": ${err.message}`, { cause: err });
43
40
  }
44
41
  const meta = parseQuiverYaml(raw);
45
- // --- Walk quills/ directory ---
46
42
  const quillsDir = join(rootDir, 'quills');
47
43
  let quillNames;
48
44
  try {
@@ -51,7 +47,6 @@ export async function scanSourceQuiver(rootDir) {
51
47
  catch (err) {
52
48
  const code = err.code;
53
49
  if (code === 'ENOENT') {
54
- // Missing quills/ is fine — empty catalog
55
50
  return { meta, catalog: new Map() };
56
51
  }
57
52
  throw new QuiverError('transport_error', `Failed to read "quills/" directory at "${quillsDir}": ${err.message}`, { cause: err });
@@ -59,7 +54,6 @@ export async function scanSourceQuiver(rootDir) {
59
54
  const catalog = new Map();
60
55
  for (const quillName of quillNames) {
61
56
  const quillNameDir = join(quillsDir, quillName);
62
- // Ensure it's a directory
63
57
  let st;
64
58
  try {
65
59
  st = await stat(quillNameDir);
@@ -69,7 +63,6 @@ export async function scanSourceQuiver(rootDir) {
69
63
  }
70
64
  if (!st.isDirectory())
71
65
  continue;
72
- // Read version directories
73
66
  let versionDirs;
74
67
  try {
75
68
  versionDirs = await readdir(quillNameDir);
@@ -80,7 +73,6 @@ export async function scanSourceQuiver(rootDir) {
80
73
  const versions = [];
81
74
  for (const versionDir of versionDirs) {
82
75
  const versionPath = join(quillNameDir, versionDir);
83
- // Ensure it's a directory
84
76
  let vst;
85
77
  try {
86
78
  vst = await stat(versionPath);
@@ -90,11 +82,9 @@ export async function scanSourceQuiver(rootDir) {
90
82
  }
91
83
  if (!vst.isDirectory())
92
84
  continue;
93
- // Non-canonical version → quiver_invalid
94
85
  if (!isCanonicalSemver(versionDir)) {
95
86
  throw new QuiverError('quiver_invalid', `Quill "${quillName}" has non-canonical version directory "${versionDir}" — only x.y.z format is allowed`, { quiverName: meta.name, version: versionDir });
96
87
  }
97
- // Require Quill.yaml sentinel inside the version dir
98
88
  const quillYamlPath = join(versionPath, 'Quill.yaml');
99
89
  try {
100
90
  await stat(quillYamlPath);
@@ -109,7 +99,6 @@ export async function scanSourceQuiver(rootDir) {
109
99
  versions.push(versionDir);
110
100
  }
111
101
  if (versions.length > 0) {
112
- // Sort descending
113
102
  versions.sort((a, b) => compareSemver(b, a));
114
103
  catalog.set(quillName, versions);
115
104
  }
@@ -148,7 +137,6 @@ async function walkDir(baseDir, currentDir, tree) {
148
137
  await walkDir(baseDir, fullPath, tree);
149
138
  }
150
139
  else {
151
- // Compute relative POSIX path
152
140
  const rel = relative(baseDir, fullPath);
153
141
  const posixRel = sep === '/' ? rel : rel.split(sep).join('/');
154
142
  let bytes;
@@ -164,8 +152,8 @@ async function walkDir(baseDir, currentDir, tree) {
164
152
  }
165
153
  /**
166
154
  * Source-backed QuiverLoader: loads file trees from a Source Quiver on disk.
167
- * The outer Quiver.loadTree already validates name/version against the catalog,
168
- * so this loader trusts the gate and goes straight to reading files.
155
+ * `Quiver` resolves every ref against its catalog before reaching a loader, so
156
+ * the name and version arrive catalog-backed and go straight to the disk.
169
157
  */
170
158
  export class SourceLoader {
171
159
  rootDir;
@@ -8,11 +8,9 @@ import { QuiverError } from '../errors.js';
8
8
  export class HttpTransport {
9
9
  baseUrl;
10
10
  constructor(baseUrl) {
11
- // Normalize: ensure exactly one trailing slash.
12
11
  this.baseUrl = baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`;
13
12
  }
14
13
  async fetchBytes(relativePath, opts) {
15
- // Strip any leading slash from relativePath to avoid double slashes.
16
14
  const cleanPath = relativePath.startsWith('/') ? relativePath.slice(1) : relativePath;
17
15
  const url = `${this.baseUrl}${cleanPath}`;
18
16
  let response;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quillmark/quiver",
3
- "version": "0.20.0",
3
+ "version": "0.22.0",
4
4
  "description": "A quiver of Quillmark quills: load one, pack one, hand out quills.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -21,7 +21,7 @@
21
21
  ],
22
22
  "author": "Quillmark Contributors",
23
23
  "engines": {
24
- "node": ">=22"
24
+ "node": ">=24"
25
25
  },
26
26
  "exports": {
27
27
  ".": {
@@ -42,7 +42,7 @@
42
42
  "NOTICE"
43
43
  ],
44
44
  "peerDependencies": {
45
- "@quillmark/wasm": ">=0.103.0-0"
45
+ "@quillmark/wasm": ">=0.104.0-0"
46
46
  },
47
47
  "dependencies": {
48
48
  "fflate": "^0.8.2",