@entelekheia/ref-id 0.1.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 ADDED
@@ -0,0 +1,38 @@
1
+ # @entelekheia/ref-id
2
+
3
+ **Parse, serialise, digest and validate `ref:` identifiers — against a specification shipped as data.**
4
+ The package embeds `spec/ref-id.json` and is held to it by that file's conformance vectors.
5
+
6
+ ```text
7
+ ref:[<version>:]<type>:<locator>[;<qualifier>=<value>]*[#<declared-name-path>[;<refinement>=<value>]*]
8
+ ```
9
+
10
+ <!-- PROOF PLACEHOLDER: copy the smallest runnable example from test/ once the vectors pass. -->
11
+
12
+ ## Why
13
+
14
+ Naming a thing by its file path breaks on the first move; naming it by a content hash breaks on the first
15
+ edit. `ref:` names it by what its format declared — a Package URL when a manifest proves the name, a
16
+ declared corpus name when nothing does — and keeps state (`;at=`), instrument (`;by=`) and population
17
+ (`;over=`) as qualifiers rather than folding them into the name. One regular expression decomposes an
18
+ identifier; each captured part is handed to the validator that already owns that format.
19
+
20
+ ## Install
21
+
22
+ ```sh
23
+ npm install @entelekheia/ref-id
24
+ ```
25
+
26
+ ## Usage
27
+
28
+ The entry points are `parse`, `serialise`, `digest` and `validateEnvelope`; their contracts are the
29
+ vectors in `spec/ref-id.json` (`parse`, `roundtrip`, `digest`, `envelope`). An unknown locator type parses
30
+ and degrades to `uncovered`; it never throws.
31
+
32
+ ## Requirements
33
+
34
+ Node.js 22 or later. Runtime dependency: `packageurl-js` (pure JavaScript). No native modules.
35
+
36
+ ## License
37
+
38
+ Apache-2.0 — see the repository's `LICENSE`.
@@ -0,0 +1,3 @@
1
+ import type { BuildParts } from "./types.ts";
2
+ /** Builds a `ref:` identifier string. `parts.location` is ignored — it never reaches the identity. */
3
+ export declare function build(parts: BuildParts): string;
package/dist/build.js ADDED
@@ -0,0 +1,138 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ //
3
+ // Assembles a `ref:` identifier string from its parts. A part the grammar cannot carry is refused
4
+ // with a BuildError naming it — the builder never emits a string that means something else, which
5
+ // is proven at the end by parsing what was built and comparing it with what was asked. The
6
+ // nested-value encoding comes from the form's own table; a locator is never encoded.
7
+ import { containsAny, encodeReserved, tableFor } from "./encoding.js";
8
+ import { BuildError } from "./errors.js";
9
+ import { FIELD, FRAGMENT_INTRODUCER, LINE_BREAKS, PAIR, schemePrefix, statePairGrammar, topLevelGrammar } from "./grammar.js";
10
+ import { loadSpec, part, status } from "./spec.js";
11
+ import { parse } from "./parse.js";
12
+ import { foldsType } from "./validators.js";
13
+ /** The form, among a qualifier's declared forms, that nests an identifier — where the encoding table lives. */
14
+ function nestingForm(spec, key) {
15
+ const declared = spec.qualifiers[key];
16
+ if (!declared) {
17
+ return undefined;
18
+ }
19
+ return declared.forms.map((name) => spec.forms[name]).find((form) => form?.nested);
20
+ }
21
+ function refuse(spec, failedPart, why) {
22
+ throw new BuildError(part(spec, failedPart), `cannot build: ${why}`);
23
+ }
24
+ function isRecord(value) {
25
+ return typeof value === "object" && value !== null && !Array.isArray(value);
26
+ }
27
+ /** Builds a `ref:` identifier string. `parts.location` is ignored — it never reaches the identity. */
28
+ export function build(parts) {
29
+ const spec = loadSpec();
30
+ if (!isRecord(parts) || typeof parts.type !== "string") {
31
+ refuse(spec, "type", "parts must be an object with a string type");
32
+ }
33
+ if (typeof parts.locator !== "string") {
34
+ refuse(spec, "locator", "the locator must be a string");
35
+ }
36
+ const separators = [spec.grammar.state.separator, FRAGMENT_INTRODUCER, ...LINE_BREAKS];
37
+ let locator = parts.locator;
38
+ const fold = `${parts.type}${FIELD}`;
39
+ if (foldsType(spec, parts.type) && locator.startsWith(fold)) {
40
+ locator = locator.slice(fold.length);
41
+ }
42
+ if (locator === "" || containsAny(locator, separators)) {
43
+ refuse(spec, "locator", "a locator is handed to its validator verbatim and cannot carry a reserved character");
44
+ }
45
+ let out = `${schemePrefix(spec)}${parts.type}${FIELD}${locator}`;
46
+ const qualifiers = parts.qualifiers ?? [];
47
+ if (!Array.isArray(qualifiers)) {
48
+ refuse(spec, "state", "qualifiers must be an array of pairs");
49
+ }
50
+ if (qualifiers.length > 0) {
51
+ const pairGrammar = statePairGrammar(spec);
52
+ const rendered = [];
53
+ for (const entry of qualifiers) {
54
+ if (!Array.isArray(entry) || typeof entry[0] !== "string") {
55
+ refuse(spec, "state", "a qualifier is a [key, value] pair with a string key");
56
+ }
57
+ const [key, value] = entry;
58
+ let encoded;
59
+ if (typeof value === "string") {
60
+ if (value.startsWith(schemePrefix(spec)) || containsAny(value, separators)) {
61
+ refuse(spec, key, "a nested identifier is passed as { nested }, never as a plain string");
62
+ }
63
+ encoded = value;
64
+ }
65
+ else if (isRecord(value) && typeof value.nested === "string") {
66
+ const form = nestingForm(spec, key);
67
+ if (!form) {
68
+ refuse(spec, key, "this qualifier declares no nesting form");
69
+ }
70
+ encoded = encodeReserved(value.nested, tableFor(spec, form));
71
+ }
72
+ else {
73
+ refuse(spec, key, "a qualifier value is a string or { nested: string }");
74
+ }
75
+ const rendering = `${key}${PAIR}${encoded}`;
76
+ if (!pairGrammar.test(rendering)) {
77
+ refuse(spec, key, "the key does not fit the pair grammar");
78
+ }
79
+ rendered.push(rendering);
80
+ }
81
+ out += `${spec.grammar.state.separator}${rendered.join(spec.grammar.state.separator)}`;
82
+ }
83
+ let wantedPath;
84
+ let wantedRefinements = [];
85
+ if (parts.fragment !== undefined) {
86
+ const separator = spec.grammar.fragment.separator;
87
+ if (typeof parts.fragment === "string") {
88
+ wantedPath = parts.fragment;
89
+ }
90
+ else if (isRecord(parts.fragment) && typeof parts.fragment.path === "string") {
91
+ wantedPath = parts.fragment.path;
92
+ wantedRefinements = parts.fragment.refinements ?? [];
93
+ if (!Array.isArray(wantedRefinements)) {
94
+ refuse(spec, "fragment", "refinements must be an array of pairs");
95
+ }
96
+ }
97
+ else {
98
+ refuse(spec, "fragment", "a fragment is a string or { path, refinements }");
99
+ }
100
+ if (wantedPath === "" || containsAny(wantedPath, [separator, ...LINE_BREAKS])) {
101
+ refuse(spec, "fragment", "a declared-name path cannot be empty or carry the refinement separator");
102
+ }
103
+ for (const entry of wantedRefinements) {
104
+ if (!Array.isArray(entry) || typeof entry[0] !== "string" || typeof entry[1] !== "string") {
105
+ refuse(spec, "fragment", "a refinement is a [key, value] pair of strings");
106
+ }
107
+ if (containsAny(entry[1], [separator, ...LINE_BREAKS])) {
108
+ refuse(spec, entry[0], "a refinement value cannot carry the separator");
109
+ }
110
+ }
111
+ out += `${FRAGMENT_INTRODUCER}${wantedPath}`;
112
+ if (wantedRefinements.length > 0) {
113
+ out += `${separator}${wantedRefinements.map(([key, value]) => `${key}${PAIR}${value}`).join(separator)}`;
114
+ }
115
+ }
116
+ // The last word is the grammar's: what was built must decompose to exactly what was asked.
117
+ if (!topLevelGrammar(spec).test(out)) {
118
+ refuse(spec, "grammar", "the assembled string does not match the grammar");
119
+ }
120
+ const check = parse(out);
121
+ if (check.status === status(spec, "malformed")) {
122
+ refuse(spec, check.part ?? "grammar", "the assembled string is malformed");
123
+ }
124
+ if (check.explicitVersion || check.type !== parts.type) {
125
+ refuse(spec, "type", "the type re-split into other parts");
126
+ }
127
+ if (check.locator !== locator) {
128
+ refuse(spec, "locator", "the locator re-split into other parts");
129
+ }
130
+ if (check.qualifiers.length !== qualifiers.length) {
131
+ refuse(spec, "state", "a qualifier re-split into other parts");
132
+ }
133
+ if ((check.fragment?.path ?? undefined) !== wantedPath || (check.fragment?.refinements.length ?? 0) !== wantedRefinements.length) {
134
+ refuse(spec, "fragment", "the fragment re-split into other parts");
135
+ }
136
+ return out;
137
+ }
138
+ //# sourceMappingURL=build.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build.js","sourceRoot":"","sources":["../src/build.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ /** Digests an ordered, non-deduplicated sequence of identifier strings. */
2
+ export declare function digest(members: readonly string[]): string;
package/dist/digest.js ADDED
@@ -0,0 +1,26 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ //
3
+ // sha256 over the UTF-8 bytes of the joined identifier strings, per `spec.digest`: declared order,
4
+ // no deduplication, and a refusal for a member that carries the join character — without it two
5
+ // different sequences could share one digest, and the envelope invariant would admit both.
6
+ import { createHash } from "node:crypto";
7
+ import { DigestError } from "./errors.js";
8
+ import { FIELD } from "./grammar.js";
9
+ import { loadSpec, part } from "./spec.js";
10
+ /** Digests an ordered, non-deduplicated sequence of identifier strings. */
11
+ export function digest(members) {
12
+ const spec = loadSpec();
13
+ if (!Array.isArray(members)) {
14
+ throw new DigestError(part(spec, "member"), "members must be an array of strings");
15
+ }
16
+ const snapshot = Array.from(members);
17
+ for (const member of snapshot) {
18
+ if (typeof member !== "string" || member.includes(spec.digest.join)) {
19
+ throw new DigestError(part(spec, "member"), "a member must be a string that does not carry the join character");
20
+ }
21
+ }
22
+ const joined = snapshot.join(spec.digest.join);
23
+ const hex = createHash(spec.digest.algorithm).update(joined, spec.digest.encoding).digest("hex");
24
+ return `${spec.digest.algorithm}${FIELD}${hex}`;
25
+ }
26
+ //# sourceMappingURL=digest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"digest.js","sourceRoot":"","sources":["../src/digest.ts"],"names":[],"mappings":"AAAA,sCAAsC;AACtC,EAAE;AACF,mGAAmG;AACnG,gGAAgG;AAChG,2FAA2F;AAE3F,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AACxC,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,EAAE,KAAK,EAAE,MAAM,cAAc,CAAA;AACpC,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAE1C,2EAA2E;AAC3E,MAAM,UAAU,MAAM,CAAC,OAA0B;IAC/C,MAAM,IAAI,GAAG,QAAQ,EAAE,CAAA;IACvB,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;QAC5B,MAAM,IAAI,WAAW,CAAC,IAAI,CAAC,IAAI,EAAE,QAAQ,CAAC,EAAE,qCAAqC,CAAC,CAAA;IACpF,CAAC;IACD,MAAM,QAAQ,GAAG,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;IACpC,KAAK,MAAM,MAAM,IAAI,QAAQ,EAAE,CAAC;QAC9B,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC;YACpE,MAAM,IAAI,WAAW,CAAC,IAAI,CAAC,IAAI,EAAE,QAAQ,CAAC,EAAE,kEAAkE,CAAC,CAAA;QACjH,CAAC;IACH,CAAC;IACD,MAAM,MAAM,GAAG,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;IAC9C,MAAM,GAAG,GAAG,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,QAA0B,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;IAClH,OAAO,GAAG,IAAI,CAAC,MAAM,CAAC,SAAS,GAAG,KAAK,GAAG,GAAG,EAAE,CAAA;AACjD,CAAC"}
@@ -0,0 +1,18 @@
1
+ import type { RefIdSpec } from "./spec.ts";
2
+ /** The encoding table a form declares, or none when the form declares no encoding. */
3
+ export declare function tableFor(spec: RefIdSpec, form: {
4
+ encoding?: string;
5
+ }): Record<string, string>;
6
+ /** True when `raw` contains any of the given characters. */
7
+ export declare function containsAny(raw: string, characters: readonly string[]): boolean;
8
+ /**
9
+ * Encodes each reserved character in `raw` to its declared percent-form, in one left-to-right
10
+ * pass over the *source* characters (so a percent-form produced by this pass is never itself
11
+ * re-scanned).
12
+ */
13
+ export declare function encodeReserved(raw: string, table: Record<string, string>): string;
14
+ /**
15
+ * Decodes each percent-form declared in `table` back to its single character, in one
16
+ * left-to-right pass over `encoded` (so decoding `%2523` yields `%23`, never `#`).
17
+ */
18
+ export declare function decodeReserved(encoded: string, table: Record<string, string>): string;
@@ -0,0 +1,47 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ //
3
+ // Percent-encode/decode helpers driven by an `encoding.<name>.table` from the spec — never a
4
+ // hardcoded character list. Which table applies to a form is the form's own `encoding` field.
5
+ /** The encoding table a form declares, or none when the form declares no encoding. */
6
+ export function tableFor(spec, form) {
7
+ if (!form.encoding) {
8
+ return {};
9
+ }
10
+ return spec.encoding[form.encoding]?.table ?? {};
11
+ }
12
+ /** True when `raw` contains any of the given characters. */
13
+ export function containsAny(raw, characters) {
14
+ return characters.some((character) => raw.includes(character));
15
+ }
16
+ /**
17
+ * Encodes each reserved character in `raw` to its declared percent-form, in one left-to-right
18
+ * pass over the *source* characters (so a percent-form produced by this pass is never itself
19
+ * re-scanned).
20
+ */
21
+ export function encodeReserved(raw, table) {
22
+ if (Object.keys(table).length === 0) {
23
+ return raw;
24
+ }
25
+ let out = "";
26
+ for (const char of raw) {
27
+ out += table[char] ?? char;
28
+ }
29
+ return out;
30
+ }
31
+ /**
32
+ * Decodes each percent-form declared in `table` back to its single character, in one
33
+ * left-to-right pass over `encoded` (so decoding `%2523` yields `%23`, never `#`).
34
+ */
35
+ export function decodeReserved(encoded, table) {
36
+ const forms = Object.values(table);
37
+ if (forms.length === 0) {
38
+ return encoded;
39
+ }
40
+ const reverse = new Map();
41
+ for (const [char, form] of Object.entries(table)) {
42
+ reverse.set(form, char);
43
+ }
44
+ const pattern = new RegExp(forms.map((form) => form.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|"), "g");
45
+ return encoded.replace(pattern, (match) => reverse.get(match) ?? match);
46
+ }
47
+ //# sourceMappingURL=encoding.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"encoding.js","sourceRoot":"","sources":["../src/encoding.ts"],"names":[],"mappings":"AAAA,sCAAsC;AACtC,EAAE;AACF,6FAA6F;AAC7F,8FAA8F;AAI9F,sFAAsF;AACtF,MAAM,UAAU,QAAQ,CAAC,IAAe,EAAE,IAA2B;IACnE,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC;QACnB,OAAO,EAAE,CAAA;IACX,CAAC;IACD,OAAO,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,KAAK,IAAI,EAAE,CAAA;AAClD,CAAC;AAED,4DAA4D;AAC5D,MAAM,UAAU,WAAW,CAAC,GAAW,EAAE,UAA6B;IACpE,OAAO,UAAU,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,GAAG,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAA;AAChE,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAAC,GAAW,EAAE,KAA6B;IACvE,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACpC,OAAO,GAAG,CAAA;IACZ,CAAC;IACD,IAAI,GAAG,GAAG,EAAE,CAAA;IACZ,KAAK,MAAM,IAAI,IAAI,GAAG,EAAE,CAAC;QACvB,GAAG,IAAI,KAAK,CAAC,IAAI,CAAC,IAAI,IAAI,CAAA;IAC5B,CAAC;IACD,OAAO,GAAG,CAAA;AACZ,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,cAAc,CAAC,OAAe,EAAE,KAA6B;IAC3E,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;IAClC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACvB,OAAO,OAAO,CAAA;IAChB,CAAC;IACD,MAAM,OAAO,GAAG,IAAI,GAAG,EAAkB,CAAA;IACzC,KAAK,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACjD,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,CAAC,CAAA;IACzB,CAAC;IACD,MAAM,OAAO,GAAG,IAAI,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,qBAAqB,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,GAAG,CAAC,CAAA;IAC3G,OAAO,OAAO,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,CAAA;AACzE,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { EnvelopeResult } from "./types.ts";
2
+ /** Refuses an envelope whose self-reference or recomputed digests do not hold. */
3
+ export declare function validateEnvelope(requestedId: string, envelope: unknown): EnvelopeResult;
@@ -0,0 +1,67 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ //
3
+ // The envelope invariant from spec.envelope: an identifier carrying a digest is admissible only
4
+ // with an object whose sets entry for that qualifier recomputes to it, served under its own id.
5
+ import { digest } from "./digest.js";
6
+ import { DigestError } from "./errors.js";
7
+ import { pattern } from "./grammar.js";
8
+ import { parse } from "./parse.js";
9
+ import { loadSpec, SpecVersionError, status } from "./spec.js";
10
+ function isStringArray(value) {
11
+ return Array.isArray(value) && value.every((item) => typeof item === "string");
12
+ }
13
+ function ownField(record, field) {
14
+ return Object.hasOwn(record, field) ? record[field] : undefined;
15
+ }
16
+ /** Refuses an envelope whose self-reference or recomputed digests do not hold. */
17
+ export function validateEnvelope(requestedId, envelope) {
18
+ const spec = loadSpec();
19
+ const setsField = spec.envelope.setsField;
20
+ if (typeof envelope !== "object" || envelope === null || Array.isArray(envelope)) {
21
+ return { admissible: false, reason: "envelope is not an object" };
22
+ }
23
+ const record = envelope;
24
+ if (ownField(record, spec.envelope.selfReference) !== requestedId) {
25
+ return { admissible: false, reason: `envelope.${spec.envelope.selfReference} does not match the requested identifier` };
26
+ }
27
+ const parsed = parse(requestedId);
28
+ if (parsed.status === status(spec, "malformed") || parsed.status === status(spec, "unsupported")) {
29
+ return { admissible: false, reason: `the requested identifier is ${parsed.status}` };
30
+ }
31
+ // spec.envelope.digestForms: every form declaring digest: true. One without a pattern cannot be recognised — a spec this package cannot honour.
32
+ const digestForms = Object.entries(spec.forms)
33
+ .filter(([, form]) => form.digest)
34
+ .map(([name, form]) => {
35
+ if (!form.pattern) {
36
+ throw new SpecVersionError(`spec.forms.${name} declares digest: true without a pattern; this package cannot recognise it`);
37
+ }
38
+ return pattern(spec, form.pattern);
39
+ });
40
+ const sets = ownField(record, setsField);
41
+ for (const [key, value] of parsed.qualifiers) {
42
+ if (!digestForms.some((form) => form.test(value))) {
43
+ continue;
44
+ }
45
+ const entry = typeof sets === "object" && sets !== null && !Array.isArray(sets) ? ownField(sets, key) : undefined;
46
+ // One read, then a copy: the members that are checked are the members that are hashed.
47
+ const members = Array.isArray(entry) ? Array.from(entry) : undefined;
48
+ if (!isStringArray(members)) {
49
+ return { admissible: false, reason: `${setsField}.${key} is missing or is not an array of strings` };
50
+ }
51
+ let recomputed;
52
+ try {
53
+ recomputed = digest(members);
54
+ }
55
+ catch (error) {
56
+ if (error instanceof DigestError) {
57
+ return { admissible: false, reason: `${setsField}.${key} carries a member with the join character` };
58
+ }
59
+ throw error;
60
+ }
61
+ if (recomputed !== value) {
62
+ return { admissible: false, reason: `${setsField}.${key} does not recompute to the declared digest` };
63
+ }
64
+ }
65
+ return { admissible: true };
66
+ }
67
+ //# sourceMappingURL=envelope.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"envelope.js","sourceRoot":"","sources":["../src/envelope.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ /** Base of every error that names the part of an identifier it concerns. */
2
+ export declare class RefIdError extends Error {
3
+ readonly part: string;
4
+ constructor(name: string, part: string, message: string);
5
+ }
6
+ /** `build` was given a part the grammar cannot carry. */
7
+ export declare class BuildError extends RefIdError {
8
+ constructor(part: string, message: string);
9
+ }
10
+ /** `digest` was given a member that is not one identifier string. */
11
+ export declare class DigestError extends RefIdError {
12
+ constructor(part: string, message: string);
13
+ }
14
+ /** `serialise` was given a result that has no faithful string form. */
15
+ export declare class SerialiseError extends RefIdError {
16
+ constructor(part: string, message: string);
17
+ }
package/dist/errors.js ADDED
@@ -0,0 +1,34 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ //
3
+ // The typed errors the producer-side API raises. `parse` never throws for an identifier problem;
4
+ // `build`, `digest` and `serialise` do, because a producer asked for something the scheme cannot
5
+ // represent, and returning a string that means something else would be the silent failure the
6
+ // specification names.
7
+ /** Base of every error that names the part of an identifier it concerns. */
8
+ export class RefIdError extends Error {
9
+ part;
10
+ constructor(name, part, message) {
11
+ super(message);
12
+ this.name = name;
13
+ this.part = part;
14
+ }
15
+ }
16
+ /** `build` was given a part the grammar cannot carry. */
17
+ export class BuildError extends RefIdError {
18
+ constructor(part, message) {
19
+ super("BuildError", part, message);
20
+ }
21
+ }
22
+ /** `digest` was given a member that is not one identifier string. */
23
+ export class DigestError extends RefIdError {
24
+ constructor(part, message) {
25
+ super("DigestError", part, message);
26
+ }
27
+ }
28
+ /** `serialise` was given a result that has no faithful string form. */
29
+ export class SerialiseError extends RefIdError {
30
+ constructor(part, message) {
31
+ super("SerialiseError", part, message);
32
+ }
33
+ }
34
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,sCAAsC;AACtC,EAAE;AACF,iGAAiG;AACjG,iGAAiG;AACjG,8FAA8F;AAC9F,uBAAuB;AAEvB,4EAA4E;AAC5E,MAAM,OAAO,UAAW,SAAQ,KAAK;IAC1B,IAAI,CAAQ;IACrB,YAAY,IAAY,EAAE,IAAY,EAAE,OAAe;QACrD,KAAK,CAAC,OAAO,CAAC,CAAA;QACd,IAAI,CAAC,IAAI,GAAG,IAAI,CAAA;QAChB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAA;IAClB,CAAC;CACF;AAED,yDAAyD;AACzD,MAAM,OAAO,UAAW,SAAQ,UAAU;IACxC,YAAY,IAAY,EAAE,OAAe;QACvC,KAAK,CAAC,YAAY,EAAE,IAAI,EAAE,OAAO,CAAC,CAAA;IACpC,CAAC;CACF;AAED,qEAAqE;AACrE,MAAM,OAAO,WAAY,SAAQ,UAAU;IACzC,YAAY,IAAY,EAAE,OAAe;QACvC,KAAK,CAAC,aAAa,EAAE,IAAI,EAAE,OAAO,CAAC,CAAA;IACrC,CAAC;CACF;AAED,uEAAuE;AACvE,MAAM,OAAO,cAAe,SAAQ,UAAU;IAC5C,YAAY,IAAY,EAAE,OAAe;QACvC,KAAK,CAAC,gBAAgB,EAAE,IAAI,EAAE,OAAO,CAAC,CAAA;IACxC,CAAC;CACF"}
@@ -0,0 +1,28 @@
1
+ import { type RefIdSpec } from "./spec.ts";
2
+ /**
3
+ * The structural literals of the scheme, each written exactly once, here. Their sources are
4
+ * `spec.grammar.expression` (the field separator after the scheme, the version and the type; the
5
+ * fragment introducer; the line breaks the character classes exclude) and the two pair grammars (the
6
+ * key/value separator). `assertStructure` checks the patterns really carry them, so a spec that moved
7
+ * them is refused rather than misread.
8
+ */
9
+ export declare const FIELD = ":";
10
+ export declare const FRAGMENT_INTRODUCER = "#";
11
+ export declare const PAIR = "=";
12
+ export declare const LINE_BREAKS: readonly ["\r", "\n"];
13
+ /** `ref:` — the scheme name from the spec followed by the field separator. */
14
+ export declare function schemePrefix(spec: RefIdSpec): string;
15
+ /**
16
+ * Compiles one pattern string for the spec's declared dialect: every `adaptations[dialect].replace`
17
+ * pair is applied in order — for `ecmascript-2018` the list is empty, so the pattern compiles
18
+ * unchanged. A port applies its own dialect's list to every pattern in the file the same way.
19
+ */
20
+ export declare function compilePattern(spec: RefIdSpec, pattern: string): RegExp;
21
+ /** The top-level `ref:` grammar, compiled for the spec's declared dialect. */
22
+ export declare function topLevelGrammar(spec: RefIdSpec): RegExp;
23
+ /** The `key=value` grammar for one qualifier (state) pair. */
24
+ export declare function statePairGrammar(spec: RefIdSpec): RegExp;
25
+ /** The `key=value` grammar for one refinement pair. */
26
+ export declare function fragmentPairGrammar(spec: RefIdSpec): RegExp;
27
+ /** Any other pattern the spec declares (a form, a dispatch entry, a refinement), compiled once and cached. */
28
+ export declare function pattern(spec: RefIdSpec, source: string): RegExp;
@@ -0,0 +1,90 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ //
3
+ // Compiles every pattern the spec declares — the top-level grammar, the two pair grammars, and any
4
+ // form, dispatch or refinement pattern — through the dialect's declared adaptations, and holds the
5
+ // only structural literals this package writes.
6
+ import { SpecVersionError } from "./spec.js";
7
+ /**
8
+ * The structural literals of the scheme, each written exactly once, here. Their sources are
9
+ * `spec.grammar.expression` (the field separator after the scheme, the version and the type; the
10
+ * fragment introducer; the line breaks the character classes exclude) and the two pair grammars (the
11
+ * key/value separator). `assertStructure` checks the patterns really carry them, so a spec that moved
12
+ * them is refused rather than misread.
13
+ */
14
+ export const FIELD = ":";
15
+ export const FRAGMENT_INTRODUCER = "#";
16
+ export const PAIR = "=";
17
+ export const LINE_BREAKS = ["\r", "\n"];
18
+ /** `ref:` — the scheme name from the spec followed by the field separator. */
19
+ export function schemePrefix(spec) {
20
+ return `${spec.scheme}${FIELD}`;
21
+ }
22
+ const checked = new WeakSet();
23
+ function assertStructure(spec) {
24
+ if (checked.has(spec)) {
25
+ return;
26
+ }
27
+ const expression = spec.grammar.expression;
28
+ for (const literal of [`^${schemePrefix(spec)}`, FRAGMENT_INTRODUCER, "\\r", "\\n"]) {
29
+ if (!expression.includes(literal)) {
30
+ throw new SpecVersionError(`spec.grammar.expression does not carry ${JSON.stringify(literal)}; this package's structural literals do not match`);
31
+ }
32
+ }
33
+ for (const pair of [spec.grammar.state.pair, spec.grammar.fragment.pair]) {
34
+ if (!pair.includes(`)${PAIR}(`)) {
35
+ throw new SpecVersionError(`a pair grammar does not separate key and value with ${JSON.stringify(PAIR)}; this package's structural literals do not match`);
36
+ }
37
+ }
38
+ checked.add(spec);
39
+ }
40
+ /**
41
+ * Compiles one pattern string for the spec's declared dialect: every `adaptations[dialect].replace`
42
+ * pair is applied in order — for `ecmascript-2018` the list is empty, so the pattern compiles
43
+ * unchanged. A port applies its own dialect's list to every pattern in the file the same way.
44
+ */
45
+ export function compilePattern(spec, pattern) {
46
+ const replacements = spec.grammar.adaptations[spec.grammar.dialect]?.replace ?? [];
47
+ let adapted = pattern;
48
+ for (const [from, to] of replacements) {
49
+ adapted = adapted.split(from).join(to);
50
+ }
51
+ return new RegExp(adapted);
52
+ }
53
+ const compiled = new WeakMap();
54
+ function grammars(spec) {
55
+ let entry = compiled.get(spec);
56
+ if (!entry) {
57
+ assertStructure(spec);
58
+ entry = {
59
+ top: compilePattern(spec, spec.grammar.expression),
60
+ statePair: compilePattern(spec, spec.grammar.state.pair),
61
+ fragmentPair: compilePattern(spec, spec.grammar.fragment.pair),
62
+ others: new Map(),
63
+ };
64
+ compiled.set(spec, entry);
65
+ }
66
+ return entry;
67
+ }
68
+ /** The top-level `ref:` grammar, compiled for the spec's declared dialect. */
69
+ export function topLevelGrammar(spec) {
70
+ return grammars(spec).top;
71
+ }
72
+ /** The `key=value` grammar for one qualifier (state) pair. */
73
+ export function statePairGrammar(spec) {
74
+ return grammars(spec).statePair;
75
+ }
76
+ /** The `key=value` grammar for one refinement pair. */
77
+ export function fragmentPairGrammar(spec) {
78
+ return grammars(spec).fragmentPair;
79
+ }
80
+ /** Any other pattern the spec declares (a form, a dispatch entry, a refinement), compiled once and cached. */
81
+ export function pattern(spec, source) {
82
+ const cache = grammars(spec).others;
83
+ let regexp = cache.get(source);
84
+ if (!regexp) {
85
+ regexp = compilePattern(spec, source);
86
+ cache.set(source, regexp);
87
+ }
88
+ return regexp;
89
+ }
90
+ //# sourceMappingURL=grammar.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"grammar.js","sourceRoot":"","sources":["../src/grammar.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ export { build } from "./build.ts";
2
+ export { digest } from "./digest.ts";
3
+ export { validateEnvelope } from "./envelope.ts";
4
+ export { BuildError, DigestError, RefIdError, SerialiseError } from "./errors.ts";
5
+ export { parse } from "./parse.ts";
6
+ export { serialise } from "./serialise.ts";
7
+ export { canonicalise, loadSpec, loadSpecFrom, SpecIntegrityError, SpecVersionError, type RefIdSpec } from "./spec.ts";
8
+ export type { BuildParts, EnvelopeResult, NestedQualifierValue, Pair, ParsedFragment, ParseResult, ParseStatus } from "./types.ts";
package/dist/index.js ADDED
@@ -0,0 +1,9 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ export { build } from "./build.js";
3
+ export { digest } from "./digest.js";
4
+ export { validateEnvelope } from "./envelope.js";
5
+ export { BuildError, DigestError, RefIdError, SerialiseError } from "./errors.js";
6
+ export { parse } from "./parse.js";
7
+ export { serialise } from "./serialise.js";
8
+ export { canonicalise, loadSpec, loadSpecFrom, SpecIntegrityError, SpecVersionError } from "./spec.js";
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,sCAAsC;AAEtC,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAA;AAClC,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACpC,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAA;AAChD,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AACjF,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAA;AAClC,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAC1C,OAAO,EAAE,YAAY,EAAE,QAAQ,EAAE,YAAY,EAAE,kBAAkB,EAAE,gBAAgB,EAAkB,MAAM,WAAW,CAAA"}
@@ -0,0 +1,3 @@
1
+ import type { ParseResult } from "./types.ts";
2
+ /** Parses a `ref:` identifier string against the loaded spec. Never throws for an identifier problem. */
3
+ export declare function parse(input: string): ParseResult;