@entelekheia/ref-id 0.2.0 → 0.3.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.
@@ -0,0 +1,17 @@
1
+ import type { ParseResult } from "./types.ts";
2
+ /**
3
+ * The canonical form of an identifier: the same identifier with its qualifiers sorted by key.
4
+ *
5
+ * Sorting is by UTF-16 code unit, the order this specification already uses for its own file, and it is
6
+ * the cheapest canonicalisation rather than the point. What is load-bearing is that one deterministic
7
+ * order exists, so any two implementations reach the same answer about whether two identifiers are one.
8
+ *
9
+ * Every other part is left exactly as parsed. **Refinements are not sorted** — they sit on the fragment
10
+ * side and are positional, `lines=1,20` being a range, so reordering them would change what is named.
11
+ *
12
+ * A malformed identifier has no canonical form and is refused, for the reason `serialise` gives: there is
13
+ * no faithful way to write back a string whose failing part was never decomposed.
14
+ */
15
+ export declare function canonical(identifier: string | ParseResult): string;
16
+ /** Whether two identifiers name one thing. Order of qualifiers does not distinguish; everything else does. */
17
+ export declare function sameIdentifier(a: string | ParseResult, b: string | ParseResult): boolean;
@@ -0,0 +1,33 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ //
3
+ // The form two identifiers are compared in. `;a=1;b=2` and `;b=2;a=1` name one thing, so identity is
4
+ // judged on this rather than on the bytes a producer happened to write.
5
+ import { parse } from "./parse.js";
6
+ import { serialise } from "./serialise.js";
7
+ import { loadSpec, status } from "./spec.js";
8
+ /**
9
+ * The canonical form of an identifier: the same identifier with its qualifiers sorted by key.
10
+ *
11
+ * Sorting is by UTF-16 code unit, the order this specification already uses for its own file, and it is
12
+ * the cheapest canonicalisation rather than the point. What is load-bearing is that one deterministic
13
+ * order exists, so any two implementations reach the same answer about whether two identifiers are one.
14
+ *
15
+ * Every other part is left exactly as parsed. **Refinements are not sorted** — they sit on the fragment
16
+ * side and are positional, `lines=1,20` being a range, so reordering them would change what is named.
17
+ *
18
+ * A malformed identifier has no canonical form and is refused, for the reason `serialise` gives: there is
19
+ * no faithful way to write back a string whose failing part was never decomposed.
20
+ */
21
+ export function canonical(identifier) {
22
+ const spec = loadSpec();
23
+ const parsed = typeof identifier === "string" ? parse(identifier) : identifier;
24
+ if (parsed.status === status(spec, "malformed"))
25
+ return serialise(parsed); // throws, naming the failing part
26
+ const sorted = [...parsed.qualifiers].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
27
+ return serialise({ ...parsed, qualifiers: sorted });
28
+ }
29
+ /** Whether two identifiers name one thing. Order of qualifiers does not distinguish; everything else does. */
30
+ export function sameIdentifier(a, b) {
31
+ return canonical(a) === canonical(b);
32
+ }
33
+ //# sourceMappingURL=canonical.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"canonical.js","sourceRoot":"","sources":["../src/canonical.ts"],"names":[],"mappings":"AAAA,sCAAsC;AACtC,EAAE;AACF,qGAAqG;AACrG,wEAAwE;AAExE,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAA;AAClC,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAC1C,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,WAAW,CAAA;AAG5C;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,SAAS,CAAC,UAAgC;IACxD,MAAM,IAAI,GAAG,QAAQ,EAAE,CAAA;IACvB,MAAM,MAAM,GAAG,OAAO,UAAU,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,UAAU,CAAA;IAC9E,IAAI,MAAM,CAAC,MAAM,KAAK,MAAM,CAAC,IAAI,EAAE,WAAW,CAAC;QAAE,OAAO,SAAS,CAAC,MAAM,CAAC,CAAA,CAAC,kCAAkC;IAC5G,MAAM,MAAM,GAAG,CAAC,GAAG,MAAM,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IACtF,OAAO,SAAS,CAAC,EAAE,GAAG,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,CAAC,CAAA;AACrD,CAAC;AAED,8GAA8G;AAC9G,MAAM,UAAU,cAAc,CAAC,CAAuB,EAAE,CAAuB;IAC7E,OAAO,SAAS,CAAC,CAAC,CAAC,KAAK,SAAS,CAAC,CAAC,CAAC,CAAA;AACtC,CAAC"}
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export { build } from "./build.ts";
2
+ export { canonical, sameIdentifier } from "./canonical.ts";
2
3
  export { digest } from "./digest.ts";
3
4
  export { validateEnvelope } from "./envelope.ts";
4
5
  export { BuildError, DigestError, RefIdError, SerialiseError } from "./errors.ts";
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  export { build } from "./build.js";
3
+ export { canonical, sameIdentifier } from "./canonical.js";
3
4
  export { digest } from "./digest.js";
4
5
  export { validateEnvelope } from "./envelope.js";
5
6
  export { BuildError, DigestError, RefIdError, SerialiseError } from "./errors.js";
package/dist/index.js.map CHANGED
@@ -1 +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"}
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,SAAS,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AAC1D,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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@entelekheia/ref-id",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "The ref: identifier scheme — a specification published as data with conformance vectors, and the package that consumes it.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
package/spec/ref-id.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "specVersion": "1.2.0",
2
+ "specVersion": "1.3.0",
3
3
  "scheme": "ref",
4
4
  "grammar": {
5
5
  "dialect": "ecmascript-2018",
@@ -301,6 +301,14 @@
301
301
  "dedupe": false,
302
302
  "output": "sha256:<64 lowercase hex>"
303
303
  },
304
+ "identifierEquivalence": {
305
+ "qualifierOrder": "insignificant",
306
+ "refinementOrder": "significant",
307
+ "canonicalForm": "re-serialise the parsed identifier with its qualifiers sorted by key in UTF-16 code unit order; every other part verbatim, refinements included",
308
+ "comparison": "two identifiers name one thing when their canonical forms are equal byte for byte",
309
+ "reserialisation": "SHOULD preserve the order written; MUST NOT be relied on to tell two identifiers apart",
310
+ "note": "distinct from `canonicalisation`, which is the form of THIS FILE for its own digest"
311
+ },
304
312
  "canonicalisation": {
305
313
  "form": "json-sorted-keys-compact",
306
314
  "rules": [
@@ -1395,6 +1403,63 @@
1395
1403
  }
1396
1404
  }
1397
1405
  ],
1406
+ "canonical": [
1407
+ {
1408
+ "name": "one qualifier is already canonical",
1409
+ "input": "ref:eita:structured-output@1/validity;when=2026-08-02T18:31:31.870Z",
1410
+ "expect": "ref:eita:structured-output@1/validity;when=2026-08-02T18:31:31.870Z"
1411
+ },
1412
+ {
1413
+ "name": "two qualifiers written out of order sort by key",
1414
+ "input": "ref:eita:structured-output@1/validity;when=2026-08-02T18:31:31.870Z;over=unknown",
1415
+ "expect": "ref:eita:structured-output@1/validity;over=unknown;when=2026-08-02T18:31:31.870Z"
1416
+ },
1417
+ {
1418
+ "name": "the same qualifiers in the other order reach the same form",
1419
+ "input": "ref:eita:structured-output@1/validity;over=unknown;when=2026-08-02T18:31:31.870Z",
1420
+ "expect": "ref:eita:structured-output@1/validity;over=unknown;when=2026-08-02T18:31:31.870Z"
1421
+ },
1422
+ {
1423
+ "name": "an unknown key sorts beside a declared one, carried through",
1424
+ "input": "ref:eita:citation-fidelity@1/exact;when=2026-08-14T23:38:26.089Z;context-tokens=8000;over=unknown",
1425
+ "expect": "ref:eita:citation-fidelity@1/exact;context-tokens=8000;over=unknown;when=2026-08-14T23:38:26.089Z"
1426
+ },
1427
+ {
1428
+ "name": "sorting is by UTF-16 code unit, so a hyphen precedes a letter",
1429
+ "input": "ref:eita:t@1/o;alphaone=1;alpha-two=2;alpha=3",
1430
+ "expect": "ref:eita:t@1/o;alpha=3;alpha-two=2;alphaone=1"
1431
+ },
1432
+ {
1433
+ "name": "a nested value is not decoded by canonicalisation",
1434
+ "input": "ref:eita:t@1/o;when=2026-08-02T18:31:31.870Z;over=ref:ai-model:Qwen3-4B-Instruct-2507-4bit",
1435
+ "expect": "ref:eita:t@1/o;over=ref:ai-model:Qwen3-4B-Instruct-2507-4bit;when=2026-08-02T18:31:31.870Z"
1436
+ },
1437
+ {
1438
+ "name": "no qualifiers, nothing to sort",
1439
+ "input": "ref:pkg:npm/@acme/scanner-core@0.1.0#Observation",
1440
+ "expect": "ref:pkg:npm/@acme/scanner-core@0.1.0#Observation"
1441
+ },
1442
+ {
1443
+ "name": "an explicit version survives canonicalisation verbatim",
1444
+ "input": "ref:1:pkg:npm/x@1.0.0;state=none;by=sha256:0000000000000000000000000000000000000000000000000000000000000000",
1445
+ "expect": "ref:1:pkg:npm/x@1.0.0;by=sha256:0000000000000000000000000000000000000000000000000000000000000000;state=none"
1446
+ },
1447
+ {
1448
+ "name": "refinements are positional and keep the order they were written",
1449
+ "input": "ref:folder:acme-governance;state=none#AGENTS.md;lines=1,20;item=3",
1450
+ "expect": "ref:folder:acme-governance;state=none#AGENTS.md;lines=1,20;item=3"
1451
+ },
1452
+ {
1453
+ "name": "qualifiers sort while the fragment beside them is untouched",
1454
+ "input": "ref:folder:acme-governance;when=2026-08-02T18:31:31.870Z;state=none#AGENTS.md;lines=1,20",
1455
+ "expect": "ref:folder:acme-governance;state=none;when=2026-08-02T18:31:31.870Z#AGENTS.md;lines=1,20"
1456
+ },
1457
+ {
1458
+ "name": "a malformed identifier has no canonical form",
1459
+ "input": "ref:eita:t@1/o;state=none;state=none",
1460
+ "expect": { "error": "state" }
1461
+ }
1462
+ ],
1398
1463
  "roundtrip": [
1399
1464
  "ref:pkg:npm/@acme/scanner-core@0.1.0#Observation",
1400
1465
  "ref:1:pkg:npm/x@1.0.0",
@@ -1 +1 @@
1
- ee34597292d313a7c424d09c050d2e1bf6cc9507b9b2627f63c684131c6fd3c6
1
+ d10d07be0fbf02acef1717de1945ec4a2e2ceef954f80017051127a1c0fb3b0a