@quillmark/quiver 0.37.0 → 0.38.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
@@ -43,7 +43,7 @@ const quill = await quiver.getQuill(doc.quillRef);
43
43
  const result = await engine.render(quill, doc, { format: 'pdf' });
44
44
  ```
45
45
 
46
- `getQuill(ref)` is the only way to obtain a quill from a quiver, and the only entry point most consumers need. It accepts selector refs (`"memo"`, `"memo@1"`) and canonical ones (`"memo@1.0.0"`), resolves the selector, materializes the quill via `Quill.fromTree`, and caches one instance per canonical ref for the quiver's lifetime; concurrent calls for the same ref coalesce into a single load. It awaits `init()` itself, so instantiating the core is not a precondition of calling it.
46
+ `getQuill(ref)` is the only way to obtain a quill from a quiver, and the only entry point most consumers need. It accepts selector refs (`"memo"`, `"memo@1.0"`) and canonical ones (`"memo@1.0.0"`), resolves the selector, materializes the quill via `Quill.fromTree`, and caches one instance per canonical ref for the quiver's lifetime; concurrent calls for the same ref coalesce into a single load. It awaits `init()` itself, so instantiating the core is not a precondition of calling it.
47
47
 
48
48
  That quill is **borrowed, not owned**: every caller asking for that ref gets the same instance, so `free()` on it hands the next caller a freed handle. Code that owns its quill mints one from `(await quiver.getQuill(ref)).toTree()` and frees that.
49
49
 
@@ -55,6 +55,10 @@ One narrower verb sits beside `getQuill`: `resolve(ref)` returns the canonical r
55
55
  const canonicalRef = quiver.resolve('memo'); // "memo@1.1.0"
56
56
  ```
57
57
 
58
+ A selector is `x.y` or `x.y.z`. `memo@1.0` is the highest `1.0.z` the quiver holds, and `memo@1.0.0` is exact. `memo@1` is `invalid_ref`: a minor step asks a stored document to be rewritten, so a major alone names no compatible series.
59
+
60
+ A ref may name the collection, `<quiver>.memo@1.0`, the form a document's `$quill` takes to say which collection it was written for. A quiver answers its own name as it answers the bare one, and refuses any other as `quill_not_found`; the canonical ref is bare.
61
+
58
62
  ## Consuming a quiver (browser)
59
63
 
60
64
  Build at deploy time, serve the output as static files:
@@ -68,7 +72,7 @@ await build('./node_modules/@org/my-quiver', './public/quivers/my-quiver');
68
72
 
69
73
  `build` owns its output path outright: it assembles a generation in `<outDir>.stage`, moves it in whole, and deletes the one it replaced. A reader fetching mid-build sees the previous generation rather than a torn tree, and a build that throws leaves it serving. An `outDir` that is, or contains, the source quiver or the working directory is refused with a `transport_error` rather than deleted.
70
74
 
71
- Quills below `0.1.0` are drafts and are left out: they stay in the source layout, which `fromDir` reads whole, and reach the artifact only under `build(src, out, { drafts: true })`. A quill with no version above the floor is absent from the built catalog entirely.
75
+ Quills below `0.1.0` are drafts and are left out: they stay in the source layout, which `fromDir` reads whole, and reach the artifact only under `build(src, out, { drafts: true })`. A quill with no version above the floor is absent from the built catalog entirely. `isDraft(version)`, from `@quillmark/quiver/node`, is the same reading of the floor.
72
76
 
73
77
  ```ts
74
78
  // browser runtime
@@ -149,6 +153,8 @@ my-quiver/
149
153
  package.json
150
154
  ```
151
155
 
156
+ A quill's `Quill.yaml` declares the `quill.name` and `quill.version` of the directory it sits in, the version as `x.y.z`.
157
+
152
158
  A consumer reaches an installed quiver by resolving `<specifier>/Quiver.yaml`, which goes through your `exports` map. A package that declares one must expose that subpath, or the quiver is unreachable however correct its layout; a package with no `exports` map needs nothing.
153
159
 
154
160
  ```jsonc
@@ -142,13 +142,6 @@ class BuiltLoader {
142
142
  function sameFiles(a, b) {
143
143
  return a.bundle === b.bundle && JSON.stringify(a.fonts) === JSON.stringify(b.fonts);
144
144
  }
145
- function assertNoUnknownKeys(obj, allowed, context) {
146
- for (const key of Object.keys(obj)) {
147
- if (!allowed.includes(key)) {
148
- throw new QuiverError('quiver_invalid', `${context}: unknown field "${key}"`);
149
- }
150
- }
151
- }
152
145
  async function readIndex(read) {
153
146
  let bytes;
154
147
  try {
@@ -175,8 +168,6 @@ function parseIndex(raw) {
175
168
  throw new QuiverError('quiver_invalid', `${INDEX} must be a JSON object`);
176
169
  }
177
170
  const obj = parsed;
178
- // Read before the key check, so a newer document is refused as newer rather than as
179
- // carrying a field this reader does not know.
180
171
  const format = obj['format'];
181
172
  if (typeof format !== 'number' || !Number.isInteger(format) || format < 1) {
182
173
  throw new QuiverError('quiver_invalid', `${INDEX}: "format" must be a positive integer, got ${JSON.stringify(format)}`);
@@ -185,11 +176,13 @@ function parseIndex(raw) {
185
176
  throw new QuiverError('quiver_invalid', `This quiver is built in format ${format} and this loader reads ${FORMAT}. ` +
186
177
  `Upgrade @quillmark/quiver, or for a served client the client itself, which carries the copy that reads this.`);
187
178
  }
188
- assertNoUnknownKeys(obj, ['format', 'name', 'description', 'quills'], INDEX);
189
179
  const name = obj['name'];
190
180
  if (typeof name !== 'string' || name.length === 0) {
191
181
  throw new QuiverError('quiver_invalid', `${INDEX} must have a non-empty string "name" field`);
192
182
  }
183
+ if (!isQuillName(name)) {
184
+ throw new QuiverError('quiver_invalid', `${INDEX} name "${name}" is not a quill name — a name is [a-z][a-z0-9_]*, at most 64 characters`);
185
+ }
193
186
  const description = obj['description'];
194
187
  if (description !== undefined && typeof description !== 'string') {
195
188
  throw new QuiverError('quiver_invalid', `${INDEX} "description" must be a string if present, got ${typeof description}`);
@@ -206,13 +199,12 @@ function parseIndex(raw) {
206
199
  throw new QuiverError('quiver_invalid', `${at} must be an object`);
207
200
  }
208
201
  const e = entry;
209
- assertNoUnknownKeys(e, ['name', 'version', 'bundle', 'fonts'], at);
210
202
  const quillName = e['name'];
211
203
  if (typeof quillName !== 'string' || quillName.length === 0) {
212
204
  throw new QuiverError('quiver_invalid', `${at}.name must be a non-empty string`);
213
205
  }
214
206
  if (!isQuillName(quillName)) {
215
- throw new QuiverError('quiver_invalid', `${at}.name "${quillName}" is not a name a ref can spell — only [A-Za-z0-9_-] are allowed`);
207
+ throw new QuiverError('quiver_invalid', `${at}.name "${quillName}" is not a quill name — a name is [a-z][a-z0-9_]*, at most 64 characters`);
216
208
  }
217
209
  const version = e['version'];
218
210
  if (typeof version !== 'string' || !isCanonicalSemver(version)) {
package/dist/format.d.ts CHANGED
@@ -3,8 +3,9 @@
3
3
  export declare const INDEX = "quiver.json";
4
4
  /**
5
5
  * The artifact's format, stamped into `quiver.json` and refused to be read past (QUIVER
6
- * §"The index"). Any change a reader assuming the previous shape would misread or reject
7
- * moves it: `quiver.json` is closed, so an added field is one of those.
6
+ * §"The index"). A change a reader assuming the previous shape would misread moves it, and
7
+ * so does the first added field, which format-2 readers before 0.38 refuse; from 3 on, an
8
+ * added field a reader can skip does not.
8
9
  */
9
10
  export declare const FORMAT = 2;
10
11
  /**
package/dist/format.js CHANGED
@@ -3,8 +3,9 @@
3
3
  export const INDEX = 'quiver.json';
4
4
  /**
5
5
  * The artifact's format, stamped into `quiver.json` and refused to be read past (QUIVER
6
- * §"The index"). Any change a reader assuming the previous shape would misread or reject
7
- * moves it: `quiver.json` is closed, so an added field is one of those.
6
+ * §"The index"). A change a reader assuming the previous shape would misread moves it, and
7
+ * so does the first added field, which format-2 readers before 0.38 refuse; from 3 on, an
8
+ * added field a reader can skip does not.
8
9
  */
9
10
  export const FORMAT = 2;
10
11
  /**
package/dist/node.d.ts CHANGED
@@ -54,3 +54,4 @@ export { Quiver } from './quiver.js';
54
54
  export { QuiverError } from './errors.js';
55
55
  export type { QuiverErrorCode } from './errors.js';
56
56
  export type { BuildOptions } from './build.js';
57
+ export { isDraft } from './semver.js';
package/dist/node.js CHANGED
@@ -78,6 +78,7 @@ export async function build(sourceDir, outDir, options) {
78
78
  // The rest of the public surface, so a Node consumer needs one import.
79
79
  export { Quiver } from './quiver.js';
80
80
  export { QuiverError } from './errors.js';
81
+ export { isDraft } from './semver.js';
81
82
  // Engine types (`Quillmark`, `Quill`, `Document`, `RenderResult`, …) are not
82
83
  // re-exported: import them straight from the `@quillmark/wasm` peer dependency,
83
84
  // which is the single source of truth for the engine contract.
@@ -13,7 +13,7 @@ export interface QuiverMeta {
13
13
  * Throws `QuiverError('quiver_invalid')` on:
14
14
  * - YAML parse failure
15
15
  * - Missing or non-string `name`
16
- * - `name` fails charset validation [A-Za-z0-9_-]+
16
+ * - `name` is not a quill name, `[a-z][a-z0-9_]*`
17
17
  * - Unknown fields (strict)
18
18
  * - `description` present but not a string
19
19
  */
@@ -13,7 +13,7 @@ const KNOWN_FIELDS = new Set(['name', 'description']);
13
13
  * Throws `QuiverError('quiver_invalid')` on:
14
14
  * - YAML parse failure
15
15
  * - Missing or non-string `name`
16
- * - `name` fails charset validation [A-Za-z0-9_-]+
16
+ * - `name` is not a quill name, `[a-z][a-z0-9_]*`
17
17
  * - Unknown fields (strict)
18
18
  * - `description` present but not a string
19
19
  */
@@ -43,7 +43,7 @@ export function parseQuiverYaml(raw) {
43
43
  }
44
44
  const name = doc['name'];
45
45
  if (!isQuillName(name)) {
46
- throw new QuiverError('quiver_invalid', `Quiver.yaml: "name" value "${name}" contains invalid characters — only [A-Za-z0-9_-] are allowed`);
46
+ throw new QuiverError('quiver_invalid', `Quiver.yaml: "name" value "${name}" is not a quill name — a name is [a-z][a-z0-9_]*, at most 64 characters`);
47
47
  }
48
48
  const meta = { name };
49
49
  if (doc['description'] !== undefined) {
package/dist/quiver.d.ts CHANGED
@@ -64,8 +64,10 @@ export declare class Quiver {
64
64
  /**
65
65
  * Resolves a selector ref → canonical ref (e.g. "memo" → "memo@1.1.0").
66
66
  *
67
- * Selector forms: `name`, `name@x`, `name@x.y`, `name@x.y.z`. Picks the
68
- * highest matching version in this quiver.
67
+ * Selector forms: `name`, `name@x.y`, `name@x.y.z`. Picks the highest
68
+ * matching version in this quiver. A name prefixed with this quiver's
69
+ * own name (`<quiver>.name`) resolves as the bare name does; one prefixed with
70
+ * any other collection is `quill_not_found`. The canonical ref is bare.
69
71
  *
70
72
  * Sync, and in-memory by construction rather than by implementation: every
71
73
  * loader materializes the catalog as the quiver is built, and `QuiverLoader`
@@ -85,7 +87,7 @@ export declare class Quiver {
85
87
  *
86
88
  * A core `Quill` renders directly: pass it to `engine.render(quill, doc)`.
87
89
  *
88
- * Selector refs (e.g. `"memo"`, `"memo@1"`) are resolved to canonical form
90
+ * Selector refs (e.g. `"memo"`, `"memo@1.0"`) are resolved to canonical form
89
91
  * first. Materializes once and caches per canonical ref — concurrent calls
90
92
  * coalesce into a single load.
91
93
  *
package/dist/quiver.js CHANGED
@@ -92,8 +92,10 @@ export class Quiver {
92
92
  /**
93
93
  * Resolves a selector ref → canonical ref (e.g. "memo" → "memo@1.1.0").
94
94
  *
95
- * Selector forms: `name`, `name@x`, `name@x.y`, `name@x.y.z`. Picks the
96
- * highest matching version in this quiver.
95
+ * Selector forms: `name`, `name@x.y`, `name@x.y.z`. Picks the highest
96
+ * matching version in this quiver. A name prefixed with this quiver's
97
+ * own name (`<quiver>.name`) resolves as the bare name does; one prefixed with
98
+ * any other collection is `quill_not_found`. The canonical ref is bare.
97
99
  *
98
100
  * Sync, and in-memory by construction rather than by implementation: every
99
101
  * loader materializes the catalog as the quiver is built, and `QuiverLoader`
@@ -104,8 +106,8 @@ export class Quiver {
104
106
  * - `quill_not_found` if no version matches
105
107
  */
106
108
  resolve(ref) {
107
- const { name, selector } = parseQuillRef(ref);
108
- const versions = this.#catalog.get(name);
109
+ const { collection, name, selector } = parseQuillRef(ref);
110
+ const versions = collection === undefined || collection === this.name ? this.#catalog.get(name) : undefined;
109
111
  if (versions !== undefined) {
110
112
  // Every loader materializes a catalog sorted descending, so the first match is
111
113
  // the highest.
@@ -126,7 +128,7 @@ export class Quiver {
126
128
  *
127
129
  * A core `Quill` renders directly: pass it to `engine.render(quill, doc)`.
128
130
  *
129
- * Selector refs (e.g. `"memo"`, `"memo@1"`) are resolved to canonical form
131
+ * Selector refs (e.g. `"memo"`, `"memo@1.0"`) are resolved to canonical form
130
132
  * first. Materializes once and caches per canonical ref — concurrent calls
131
133
  * coalesce into a single load.
132
134
  *
package/dist/ref.d.ts CHANGED
@@ -1,18 +1,20 @@
1
1
  /** Internal parsed representation of a quill reference. */
2
2
  export interface ParsedQuillRef {
3
+ /** The collection the ref names, the part before a `.`; absent on a bare name. */
4
+ collection?: string;
3
5
  name: string;
4
6
  /** Undefined selects the highest version present. */
5
7
  selector?: string;
6
8
  }
7
9
  /**
8
- * Whether `name` is a name a ref can spell. A quiver's own name and every catalog
9
- * row are held to it: a row seated outside it is one `quillNames` lists and
10
- * `getQuill` refuses.
10
+ * Whether `name` is a quill name: `[a-z][a-z0-9_]*`, at most 64 characters, the
11
+ * engine's rule. A quiver's own name and every catalog row are held to it.
11
12
  */
12
13
  export declare function isQuillName(name: string): boolean;
13
14
  /**
14
- * Throws QuiverError('invalid_ref') on malformed input. The name is
15
- * `[A-Za-z0-9_-]+`; the selector is `x`, `x.y` or `x.y.z`, digits only, with no
16
- * ranges or operators — a quiver resolves a prefix, it does not solve a range.
15
+ * Throws QuiverError('invalid_ref') on malformed input. The name is a quill name,
16
+ * optionally prefixed `<collection>.`; the selector is `x.y` or `x.y.z`, digits
17
+ * only, with no ranges or operators — a quiver resolves a prefix, it does not
18
+ * solve a range.
17
19
  */
18
20
  export declare function parseQuillRef(ref: string): ParsedQuillRef;
package/dist/ref.js CHANGED
@@ -1,18 +1,18 @@
1
1
  import { QuiverError } from './errors.js';
2
- const NAME_RE = /^[A-Za-z0-9_-]+$/;
3
- const SELECTOR_RE = /^\d+(\.\d+){0,2}$/;
2
+ const NAME_RE = /^[a-z][a-z0-9_]{0,63}$/;
3
+ const SELECTOR_RE = /^\d+\.\d+(\.\d+)?$/;
4
4
  /**
5
- * Whether `name` is a name a ref can spell. A quiver's own name and every catalog
6
- * row are held to it: a row seated outside it is one `quillNames` lists and
7
- * `getQuill` refuses.
5
+ * Whether `name` is a quill name: `[a-z][a-z0-9_]*`, at most 64 characters, the
6
+ * engine's rule. A quiver's own name and every catalog row are held to it.
8
7
  */
9
8
  export function isQuillName(name) {
10
9
  return NAME_RE.test(name);
11
10
  }
12
11
  /**
13
- * Throws QuiverError('invalid_ref') on malformed input. The name is
14
- * `[A-Za-z0-9_-]+`; the selector is `x`, `x.y` or `x.y.z`, digits only, with no
15
- * ranges or operators — a quiver resolves a prefix, it does not solve a range.
12
+ * Throws QuiverError('invalid_ref') on malformed input. The name is a quill name,
13
+ * optionally prefixed `<collection>.`; the selector is `x.y` or `x.y.z`, digits
14
+ * only, with no ranges or operators — a quiver resolves a prefix, it does not
15
+ * solve a range.
16
16
  */
17
17
  export function parseQuillRef(ref) {
18
18
  if (!ref) {
@@ -24,21 +24,30 @@ export function parseQuillRef(ref) {
24
24
  }
25
25
  // Split once: the charset the name is held to is one rule, and a ref with a selector
26
26
  // is under it exactly as a bare one is.
27
- const name = atIndex === -1 ? ref : ref.slice(0, atIndex);
27
+ const fullName = atIndex === -1 ? ref : ref.slice(0, atIndex);
28
28
  const selector = atIndex === -1 ? undefined : ref.slice(atIndex + 1);
29
29
  if (selector === '') {
30
30
  throw new QuiverError('invalid_ref', `Invalid ref: missing selector after "@" in "${ref}"`, {
31
31
  ref
32
32
  });
33
33
  }
34
- if (!NAME_RE.test(name)) {
35
- throw new QuiverError('invalid_ref', `Invalid ref: name "${name}" contains invalid characters`, { ref });
34
+ const dot = fullName.indexOf('.');
35
+ const collection = dot === -1 ? undefined : fullName.slice(0, dot);
36
+ const name = dot === -1 ? fullName : fullName.slice(dot + 1);
37
+ for (const segment of collection === undefined ? [name] : [collection, name]) {
38
+ if (!NAME_RE.test(segment)) {
39
+ throw new QuiverError('invalid_ref', `Invalid ref: name "${fullName}" is not [collection.]name, each [a-z][a-z0-9_]* and at most 64 characters`, { ref });
40
+ }
36
41
  }
42
+ const parsed = collection === undefined ? { name } : { collection, name };
37
43
  if (selector === undefined) {
38
- return { name };
44
+ return parsed;
45
+ }
46
+ if (/^\d+$/.test(selector)) {
47
+ throw new QuiverError('invalid_ref', `Invalid ref: selector "${selector}" is a major alone, which names no compatible series; write "${fullName}@${selector}.y" or "${fullName}@${selector}.y.z"`, { ref });
39
48
  }
40
49
  if (!SELECTOR_RE.test(selector)) {
41
- throw new QuiverError('invalid_ref', `Invalid ref: selector "${selector}" is not a valid semver selector (only x, x.y, x.y.z with digits allowed)`, { ref });
50
+ throw new QuiverError('invalid_ref', `Invalid ref: selector "${selector}" is not x.y (a compatible series) or x.y.z (an exact version), digits only`, { ref });
42
51
  }
43
- return { name, selector };
52
+ return { ...parsed, selector };
44
53
  }
package/dist/semver.d.ts CHANGED
@@ -3,9 +3,9 @@ export declare const MIN_PUBLISHED_VERSION = "0.1.0";
3
3
  /** Returns true for exactly `x.y.z` with non-negative integer parts. */
4
4
  export declare function isCanonicalSemver(version: string): boolean;
5
5
  /**
6
- * Returns true if `version` (canonical) matches `selector` (partial: `x`,
7
- * `x.y`, or `x.y.z`). `parseQuillRef` owns selector validation; this is a
8
- * prefix comparison over an already-valid selector.
6
+ * Returns true if `version` (canonical) matches `selector` (`x.y` or `x.y.z`).
7
+ * `parseQuillRef` owns selector validation; this is a prefix comparison over an
8
+ * already-valid selector.
9
9
  */
10
10
  export declare function matchesSemverSelector(version: string, selector: string): boolean;
11
11
  /** Compares two canonical semver strings. Returns <0, 0, or >0. Three positions,
package/dist/semver.js CHANGED
@@ -5,9 +5,9 @@ export function isCanonicalSemver(version) {
5
5
  return /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.test(version);
6
6
  }
7
7
  /**
8
- * Returns true if `version` (canonical) matches `selector` (partial: `x`,
9
- * `x.y`, or `x.y.z`). `parseQuillRef` owns selector validation; this is a
10
- * prefix comparison over an already-valid selector.
8
+ * Returns true if `version` (canonical) matches `selector` (`x.y` or `x.y.z`).
9
+ * `parseQuillRef` owns selector validation; this is a prefix comparison over an
10
+ * already-valid selector.
11
11
  */
12
12
  export function matchesSemverSelector(version, selector) {
13
13
  const selectorParts = selector.split('.');
@@ -12,9 +12,9 @@ import type { QuiverLoader } from './quiver.js';
12
12
  *
13
13
  * Throws:
14
14
  * - `quiver_invalid` if Quiver.yaml is invalid, any rung it opens or reads is a
15
- * symlink, a quill dir holding versions is named outside the ref charset, a
16
- * version dir name is non-canonical, or a version dir is missing its Quill.yaml
17
- * sentinel.
15
+ * symlink, a quill dir holding versions is not a quill name, a version dir
16
+ * name is non-canonical, or a version dir is missing its Quill.yaml or holds
17
+ * one naming another quill or version.
18
18
  * - `transport_error` for I/O failures (permissions, etc.).
19
19
  *
20
20
  * Missing `quills/` directory is not an error — the quiver is valid but empty.
@@ -4,6 +4,7 @@
4
4
  */
5
5
  import { readdir, readFile, lstat } from 'node:fs/promises';
6
6
  import { join, relative, sep } from 'node:path';
7
+ import { parse as parseYaml } from 'yaml';
7
8
  import { QuiverError } from './errors.js';
8
9
  import { parseQuiverYaml } from './quiver-yaml.js';
9
10
  import { isQuillName } from './ref.js';
@@ -20,6 +21,44 @@ function refuseSymlink(st, path) {
20
21
  throw new QuiverError('quiver_invalid', `"${path}" is a symlink; a quiver holds its own content, and nothing under it may point outside`);
21
22
  }
22
23
  }
24
+ /**
25
+ * Asked of a directory holding a quill rather than of every directory, so a stray
26
+ * one holding none stays ignored: what has to be addressable is a catalog row.
27
+ */
28
+ function refuseUnnamedQuill(quillName, quiverName) {
29
+ if (!isQuillName(quillName)) {
30
+ throw new QuiverError('quiver_invalid', `Quill directory "${quillName}" is not a quill name — a name is [a-z][a-z0-9_]*, at most 64 characters`, { quiverName });
31
+ }
32
+ }
33
+ /**
34
+ * Refuse a `Quill.yaml` whose `quill.name` or `quill.version` disagrees with the
35
+ * path it sits at: the catalog serves the path, and a document saved from the quill
36
+ * names what `Quill.yaml` says. A canonical version has one spelling, so the declared
37
+ * one is compared as written. A field absent or not a string is the engine's to report
38
+ * when the quill loads.
39
+ */
40
+ function refuseMisplacedQuill(raw, name, version, quiverName) {
41
+ let parsed;
42
+ try {
43
+ parsed = parseYaml(new TextDecoder().decode(raw), { schema: 'failsafe' });
44
+ }
45
+ catch {
46
+ return;
47
+ }
48
+ const quill = parsed?.quill;
49
+ if (typeof quill !== 'object' || quill === null)
50
+ return;
51
+ const declared = quill;
52
+ if (typeof declared.name === 'string' && declared.name !== name) {
53
+ throw new QuiverError('quiver_invalid', `Quill "${name}@${version}" declares quill.name "${declared.name}": a quill sits at quills/<quill.name>/<quill.version>/`, { quiverName, version });
54
+ }
55
+ if (typeof declared.version === 'string' && declared.version !== version) {
56
+ const rule = isCanonicalSemver(declared.version)
57
+ ? 'a quill sits at quills/<quill.name>/<quill.version>/'
58
+ : 'a quill version is x.y.z';
59
+ throw new QuiverError('quiver_invalid', `Quill "${name}@${version}" declares quill.version "${declared.version}": ${rule}`, { quiverName, version });
60
+ }
61
+ }
23
62
  /**
24
63
  * Scans a Source Quiver root directory.
25
64
  *
@@ -28,9 +67,9 @@ function refuseSymlink(st, path) {
28
67
  *
29
68
  * Throws:
30
69
  * - `quiver_invalid` if Quiver.yaml is invalid, any rung it opens or reads is a
31
- * symlink, a quill dir holding versions is named outside the ref charset, a
32
- * version dir name is non-canonical, or a version dir is missing its Quill.yaml
33
- * sentinel.
70
+ * symlink, a quill dir holding versions is not a quill name, a version dir
71
+ * name is non-canonical, or a version dir is missing its Quill.yaml or holds
72
+ * one naming another quill or version.
34
73
  * - `transport_error` for I/O failures (permissions, etc.).
35
74
  *
36
75
  * Missing `quills/` directory is not an error — the quiver is valid but empty.
@@ -106,8 +145,10 @@ export async function scanSourceQuiver(rootDir) {
106
145
  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 });
107
146
  }
108
147
  const quillYamlPath = join(versionPath, 'Quill.yaml');
148
+ let quillYaml;
109
149
  try {
110
150
  refuseSymlink(await lstat(quillYamlPath), quillYamlPath);
151
+ quillYaml = await readFile(quillYamlPath);
111
152
  }
112
153
  catch (err) {
113
154
  if (err instanceof QuiverError)
@@ -116,16 +157,13 @@ export async function scanSourceQuiver(rootDir) {
116
157
  if (code === 'ENOENT') {
117
158
  throw new QuiverError('quiver_invalid', `Quill "${quillName}@${versionDir}" is missing required "Quill.yaml"`, { quiverName: meta.name, version: versionDir });
118
159
  }
119
- throw new QuiverError('transport_error', `Failed to stat "Quill.yaml" at "${quillYamlPath}": ${err.message}`, { cause: err });
160
+ throw new QuiverError('transport_error', `Failed to read "Quill.yaml" at "${quillYamlPath}": ${err.message}`, { cause: err });
120
161
  }
162
+ refuseUnnamedQuill(quillName, meta.name);
163
+ refuseMisplacedQuill(quillYaml, quillName, versionDir, meta.name);
121
164
  versions.push(versionDir);
122
165
  }
123
166
  if (versions.length > 0) {
124
- // Asked here rather than at the directory, so a stray one holding no quill
125
- // stays ignored: what has to be addressable is a catalog row.
126
- if (!isQuillName(quillName)) {
127
- throw new QuiverError('quiver_invalid', `Quill directory "${quillName}" is not a name a ref can spell — only [A-Za-z0-9_-] are allowed`, { quiverName: meta.name });
128
- }
129
167
  versions.sort((a, b) => compareSemver(b, a));
130
168
  catalog.set(quillName, versions);
131
169
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quillmark/quiver",
3
- "version": "0.37.0",
3
+ "version": "0.38.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": ">=24"
24
+ "node": ">=22"
25
25
  },
26
26
  "exports": {
27
27
  ".": {
@@ -42,7 +42,7 @@
42
42
  "NOTICE"
43
43
  ],
44
44
  "peerDependencies": {
45
- "@quillmark/wasm": ">=0.125.0-0"
45
+ "@quillmark/wasm": ">=0.126.0-0"
46
46
  },
47
47
  "dependencies": {
48
48
  "fflate": "^0.8.2",