Identifiers, references and URLs
Normative for RUNBOOK.md §7 and §9. Implementation: packages/registry/src/identifiers.ts.
Data: registry/records.json.
URLs are the most permanent thing this project ships. Agent pins point at them, external documentation links to them, indexes cache them. Everything else can be rewritten; these cannot.
The reference grammar
runbook-ref = publisher "/" slug [ "@" semver ]
publisher = "std" / verified-name / github-ns / domain-ns
verified-name= lower *(lower / DIGIT / "-") ; 1*32, no leading or trailing "-"
github-ns = "github:" org ; org per GitHub: 1*39 alnum or "-"
domain-ns = "domain:" fqdn ; RFC 1123, lowercased
slug = lower *(lower / DIGIT / "-") ; 1*64, no leading or trailing "-"
semver = 1*DIGIT "." 1*DIGIT "." 1*DIGIT
lower = %x61-7A
The awkward cases are decided here rather than discovered by each parser:
@belongs to the reference, never to a slug. A slug containing one would makea/b@1.0.0ambiguous, and every parser would resolve the ambiguity differently.- The publisher is everything before the first
/. Agithub:org may contain hyphens and adomain:publisher contains dots; splitting on the last/would work by luck. - Case is significant and everything is lowercase, except a GitHub org, which GitHub itself treats as case-preserving. A reference is compared byte for byte.
- No trailing slash.
/r/std/xand/r/std/x/are not two URLs, and the one with the slash is not ours. - Percent-encoding only where a segment needs it — a capability URN in
/cap/{urn}contains a colon. Nothing else is encoded, because a URL somebody cannot type is a URL they cannot share at 3am.
The record identifier
Every published record has an rb_ ULID, assigned once and never changed. It survives a
rename, a republication and a change of publisher tier.
It is stored in Git, not minted at build time. A build that minted one would produce a different catalog on every run, and an identifier meant to outlive a slug cannot be derived from the slug.
Renames
A rename adds the old slug to previousSlugs and changes slug. That produces:
- a permanent redirect from the old path to the new one — 301, because §9 says a URL is permanent and a 302 tells a crawler to keep asking;
- no change to the
rb_id, so a pin by identifier still resolves; - a slug that is never granted to another record, past or present. A slug that once pointed at one record and now points at another is a link that keeps working and answers a different question, which is worse than one that breaks.
A diff's {range} is one segment of the form {a}..{b}, two semvers of the same record —
1.0.0..1.1.0. Two dots because a range is one thing: a slash would make the older
version a directory containing the newer one, which is not the relationship. The URL
exists only where a record has both versions.
A simulation is addressed by the rb_ id rather than by the slug. A dry pass is about
the procedure rather than about its current name, and a link to one should survive the
rename that the id exists to survive. Nobody types it: the record page links there.
The URL table
| Path | What | Rendering |
|---|---|---|
/ |
home | static |
/catalog |
the faceted catalog | static |
/catalog/{facet}/{value} |
a filtered view, as a link | ssg |
/r/{publisher}/{slug} |
a record, latest version | ssg |
/r/{publisher}/{slug}/v/{semver} |
a pinned version | ssg, noindex |
/r/{publisher}/{slug}/diff/{range} |
what changed between two versions | ssg, noindex, only where a record has two |
/r/{publisher}/{slug}/graph |
the procedure as a graph, on a page of its own | ssg, noindex |
/r/{publisher}/{slug}/playbook |
the composition this record is the root of | ssg, noindex, only where it composes |
/r/{publisher}/{slug}/export/{skill.md,workflow.yaml,record.json,graph.svg} |
the record in the four forms a runner takes | artifact, not crawled |
/r/{publisher}/{slug}/source.md |
the document as authored, which content_hash was computed over |
artifact, not crawled |
/t/{topic} |
one problem, several solutions | ssg, threshold-gated |
/s/{target} |
everything about a system | ssg, threshold-gated |
/cap/{urn} |
what a capability unlocks | ssg, threshold-gated |
/p/{publisher} |
who the author is | ssg |
/trust |
how a level is computed | static |
/status |
what the catalog says about itself | static |
/submit |
how to send a runbook, and what happens then | static |
/new |
the constructor | static |
/stack |
what applies to the systems you run | static |
/import |
lift an existing page into a record | static |
/simulate/{id} |
a dry pass over a record, by rb_ id |
ssg |
/guide |
the guides index | static |
/guide/{slug} |
one guide | static |
/spec/v1 |
the specification index | static |
/spec/v1/{slug} |
one normative document | static |
/v1/index.json |
the machine index | artifact |
/llms.txt |
discovery | artifact |
/agent.txt |
the obligations, where an agent will read them | artifact |
Every path here has a route in the application and every route is here; a mismatch in either direction is a bug in one of the two, and the test says which.
Those are two lists compared with each other, which is a comparison that cannot see a URL
missing from both. So the record's own namespace is held to a third thing — what the build
actually writes: every path served under /r/ is a row in this table. The exports were
absent from it for as long as they have existed. A hundred and twenty permanent addresses,
linked from every record page and named in llms.txt, in the one document an integrator
reads to find out which URLs are permanent.
The exports are addresses rather than pages: they are what a runner fetches, so they are
Disallowed in robots.txt and appear in no sitemap, and — like every other path here —
they do not move. /v1 and /spec/v1 artifacts are not enumerated above because
/llms.txt enumerates them with a purpose each, and this table names the
three doors into them rather than repeating the room behind each one.
Why a pinned version is noindex
It is an address for an agent, not a page competing with the record it pins. Indexing both puts two URLs for one procedure into every search result, and the one a reader should land on is the current one. This is not an oversight to be fixed for SEO later.
The hub threshold as a routing rule
A hub exists only above five T2+ records (§9). Below that the URL is a 404, not an empty page: a farm of empty hubs is an SEO penalty and, worse, a promise the catalog cannot keep. The build decides eligibility, so the rule is applied without a running service — a hub whose members fall below the threshold stops having a page at the next build, and its records are still reachable everywhere else.