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:

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 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.