@entelekheia/ref-id
v0.7.1
Published
The ref: identifier scheme — a specification published as data with conformance vectors, and the package that consumes it.
Readme
@entelekheia/ref-id
Parse, serialise, digest and validate ref: identifiers — against a specification shipped as data.
The package embeds spec/ref-id.json and is held to it by that file's conformance vectors.
ref:[<version>:]<type>:<locator>[;<qualifier>=<value>]*[#<declared-name-path>[;<refinement>=<value>]*]import { parse } from "@entelekheia/ref-id"
parse("ref:pkg:npm/@acme/[email protected]#Observation")
// => {
// status: "ok",
// type: "pkg",
// locator: "npm/@acme/[email protected]",
// fragment: { path: "Observation", refinements: [] },
// delegated: "pkg:npm/@acme/[email protected]",
// canonical: "pkg:npm/%40acme/[email protected]",
// ...
// }Why
Naming a thing by its file path breaks on the first move; naming it by a content hash breaks on the first
edit. ref: names it by what its format declared — a Package URL when a manifest proves the name, a
declared corpus name when nothing does — and keeps state (;state=), instrument (;by=) and population
(;over=) as qualifiers rather than folding them into the name. One regular expression decomposes an
identifier; each captured part is handed to the validator that already owns that format.
Install
npm install @entelekheia/ref-idUsage
The entry points are parse, serialise, build, canonicalise, loadSpec, digest,
validateEnvelope, sameIdentifier, canonicalIdentifier, samePackage, covers, relate and verdict; their
contracts are the vectors in spec/ref-id.json (parse, roundtrip, build, digest, envelope,
canonical, comparison, relate, verdict). An unknown locator type parses and degrades to uncovered; it never
throws.
Three questions get three answers, and they are not interchangeable:
import { covers, sameIdentifier, samePackage } from "@entelekheia/ref-id"
sameIdentifier("ref:pkg:npm/[email protected]", "ref:pkg:npm/[email protected]") // false — two different releases, byte for byte
sameIdentifier("ref:pkg:npm/x;a=1;b=2", "ref:pkg:npm/x;b=2;a=1") // true — qualifier order never distinguishes
samePackage("ref:pkg:npm/[email protected]", "ref:pkg:npm/[email protected]") // true — one released thing, two versions
covers("ref:pkg:npm/x", "ref:pkg:npm/[email protected]") // true — the general covers the specific
covers("ref:pkg:npm/[email protected]", "ref:pkg:npm/x") // false — and never the reversesameIdentifier is what a digest and an envelope are built on, so it stays strict. samePackage is
symmetric and ignores the locator's version, and only where the type declares it carries one — a mailbox's
@ and a served model's quantisation key are not versions. covers is asymmetric: what the first
identifier leaves undeclared, the second may declare freely; what the first declares, the second must
declare identically — except a qualifier whose value is a nested ref: identifier on both sides,
which is compared with covers on the decoded pair instead, one level deep, so an unversioned instrument
covers every release of it. Any other qualifier value — a digest, a timestamp, plain text, or a nested
identifier on one side only — still must be identical, and so must a nested pair covers refuses (a
scheme version this package does not implement). That is what makes a partial identifier a query over a
store keyed by identifier.
relate(a, b) answers a different question: not whether one covers the other, but how the two relate in
each dimension — type, version, locator stem, locator version, the declared name and its refinements, and
each qualifier — as "equal" | "covers" | "coveredBy" | "differ", or null for a pair it refuses.
covers, samePackage (and their mirror) are reductions of this same result, never a second computation
that could disagree with it.
verdict(a, b) says what a difference means once some qualifiers are hints rather than identity: the
location qualifiers path=, origin= and corpus= narrow where to search without deciding what is
named. It returns an identity axis ("same" | "covers" | "coveredBy" | "distinct" | "undetermined"),
a content axis read from state= ("same" | "different" | "unknown") and decidedBy, or null for a
pair relate refuses. Two clones of one repository at two local paths come out undetermined, two
origin= values distinct, and one content under two names distinct with content: "same".
An identifier is a reference, not a copy of the record. Read it as
search(in: <type>:<locator>, for: #<declared name>, with: ;<qualifiers>): the first two are the pointer,
and a qualifier is enrichment, added where it tells two records apart. A nested identifier is a bare
pointer — a type, a locator and at most a fragment. Every other attribute goes in a column beside the
identifier.
ref:ai-model:example-app/org/repo;thinking=no;by=ref:pkg:github/ggml-org/llama.cpp ← a pointer
ref:ai-model:example-app/org/repo;by=ref:pkg:github/ggml-org/llama.cpp%3Bstate=none;latency-ms=812
← a copy of the recordBoth parse ok; write the first shape. The rules and their reasons are in
What an identifier carries.
Environments
Node.js 22 or later, and any browser reached through a bundler. The package ships two builds of one
source, and exports picks between them: a bundler that honours the browser condition takes the browser
build, everything else takes the default. Nothing to configure, and the public API is the same either way.
| | Node | Browser |
|---|---|---|
| Where the specification comes from | spec/ref-id.json, read from disk on first use | a constant compiled into the build |
| When its integrity is established | at load — the bytes are hashed and compared with the sidecar, and a mismatch throws | at build — the generator refuses to emit from a specification that fails its sidecar, and a staleness check refuses a compiled constant that no longer matches the file |
| sha256 for digest() | Node's crypto | @noble/hashes (pure JavaScript, no dependencies) |
What the browser build gives up, stated rather than discovered:
- No integrity check at runtime. There is no file to compare the constant against, and hashing it
against a digest compiled from the same source in the same build would be a check that cannot fail —
which reads as a guarantee and is a tautology. The build exports
SPEC_DIGEST, the digestspec/ref-id.jsoncarried when the constant was generated; it is a statement about provenance, not a verification, and it is labelled as one. - No
loadSpecFrom. It takes a directory to read a specification and its sidecar from. A browser has neither, so the browser build does not export it; every other export is present and identical. TypeScript will not warn you about that on the default configuration. Under"moduleResolution": "bundler"withoutcustomConditions, the compiler resolves types through the default condition — which does declareloadSpecFrom— while the bundler picks the browser build at build time.tsc --noEmitthen passes on an import that fails when the bundle is produced. Add"customConditions": ["browser"]to yourtsconfig.jsonand the compiler sees the same surface your bundler does. digest()is sha256 only. The algorithm is specification data, and the browser implementation honourssha256alone: a specification naming another one throws aSpecVersionErrornaming it, where Node would honour whatever its owncryptohonours.- The compiled-in specification costs bundle size. It is the whole published file, including the parts a given consumer never reads.
The two builds are held to each other by npm run test:differential, which runs the browser build as a
fifth implementation beside Node, Rust, Swift and Python over every input the specification names, and fails on
the first disagreement. npm run build additionally walks the emitted browser module graph and refuses it
if any Node builtin — or anything resolving a path against a module's own location — survives into it.
Runtime dependencies: packageurl-js and @noble/hashes, both pure JavaScript. No native modules.
packageurl-js decides whether a Package URL locator is valid and how its canonical field is spelled, and
the Rust, Swift and Python implementations use other validators. Where they differ is measured in
Known differences between implementations.
License
Apache-2.0 — see the repository's LICENSE.
