56. Every object of the layer has a derived address, both directions of every relation, and a declared place of publication
Extends ADR 4 and ADR 13.
Context
ADR 2 made every interface of the executable a projection of one capability registry, and
ADR 4 made one canonical declaration the only place a capability is written down. The
declarative half of the repository — every file under .ai/, indexed as a typed object with
a kind, an identity and a majordomus:// URI — was already read that way: one relation table
resolves every reference an object declares, graph.compose draws it, and every object is an
MCP resource.
Three things were missing, and each was the kind of gap that looks like polish until a reader meets it.
An identity is not an address. An object could be read in the Cockpit at
/cockpit/object?uri=majordomus%3A%2F%2Frule%2Fproject.x%401 — a query parameter carrying a
percent-encoded URI. Nothing linked to it, nothing was bookmarkable, no index of a kind
existed, and the composed graph's nodes pointed at that address, so the one place the
relations were drawn sent every click into a query string.
The relation table was read forward only. A rule declared what it depends on; nothing answered what depends on this, although the edge that answers it was already in the graph pointing the other way. The same was true of a decision and what it put in force, of a claim and its implementation, of an application and the use cases that serve it.
Nothing said where a kind is published. The public site had editorial sections for skills, commands, use cases, features, claims, sessions, profiles and the Why catalogue — and no page at all for a decision or for this repository's own 78 rules. Which kinds reach the site was implicit in twenty lines of a shell generator, so a kind could be added to the layer and reach no public page with nothing anywhere saying so. That is the silent discovery failure this repository has already met twice, in a glob that matched no ADR and in a site-check whose subject set was empty.
Decision
An address is derived, never assigned. entity::slug reduces an identity to one path
segment — lowercase, every run of anything else a single -, trimmed — and the address is
the kind and that segment. There is no table mapping objects to routes, because a table can
be incomplete and a function cannot. A function can, however, map two identities of one kind
onto one segment; that is a collision, entity::collisions finds every one, and
scripts/ci/entity-check refuses the tree while one exists. A route that answers with one of
two objects is worse than no route, because the reader cannot tell which one they were given.
One capability answers for an entity, on every surface. entity.show returns the
identity, the address, the provenance, the front matter, the content, the outgoing edges, the
incoming edges, the surfaces that answer for the object and what can be said about the
executable artefacts it names. It is projected to MCP as majordomus_entity, to HTTP as
GET /api/v1/entity, to the command line as majordomus entity show, and it is what the
Cockpit's entity page lays out. entity.kinds is the same for the catalogue of kinds.
Backlinks are derived from the forward declaration. entity::edges reads
graph::RELATIONS — the one table the composed graph and the dangling-reference check
already share — over every object, and keeps the edges whose either end is the subject. A
relation added to that table becomes a forward edge on one page and a backlink on the other,
with nothing declared twice. Nothing in the layer declares a reverse reference, and nothing
may: half a relationship is the failure mode this forecloses.
Every kind declares where it is published. site/data/publication.toml carries one entry
per kind of the index: published as entity pages at a route, published by an editorial
section already, or not published with the reason written down. scripts/ci/entity-check
reads that file and majordomus entity kinds and refuses both directions — a kind the index
holds that the declaration does not mention, and a kind the declaration names that the index
does not hold.
The public entity pages are one projection, not one per kind. Two templates —
entity.html and entities-section.html — serve every kind that publishes entity pages, and
neither names a kind. The gate refuses a kind name appearing in either, because a template
that named one would make adding a kind an edit to a consumer.
The documentation hub reads the same two declarations, and the index is the build's.
Added 2026-09-24. The top of /docs/ lists every kind with its count and where it is
published, or the reason it is not. Those rows are the catalogue of entities.json: the
publication declaration joined to the index. Beside it are the interface counts (capabilities,
command-line operations, HTTP routes, MCP tools and resources, modules), which are the
registry's own summary. /docs/index.json is written by scripts/lib/docs-index.mjs after
the build: it walks the pages Zola produced, not a list. So it holds every page a visitor can
open, joins each entity page to its object, carries the /build.json identity of the build
that wrote it, and refuses the build (exit 10) when a route the catalogue publishes was not
built. The hub's search reads that file, so a search result can only be a page that exists.
scripts/pages verify-docs asks the public site for the same file, checks that it names the
deployed commit, and probes every route it publishes. It runs as the step of the Pages
workflow after the site serves the commit. Case 500 holds all four.
Alternatives rejected
A route table in the Cockpit. The obvious shape, and the one the mandate that produced this work explicitly forbade: it is an inventory, it goes stale silently, and it makes adding a rule an edit to the router.
Publishing every kind. 1309 objects across 29 kinds. Most already have an editorial page
— an issue on /plan/, a claim on /guarantees/, a session on /sessions/ — and a second
address for the same object is the duplication the whole subsystem removes. Some are facts
about a checkout rather than about the product: a mesh identity, a scope declaration, a
prompt. The decision is not "publish everything" but "decide about everything, in writing,
where a check can read it".
Declaring publication in share/kinds.yaml. That file is the tool distribution's, shared
with every supervised repository; where this website publishes a kind is not a property of
the kind everywhere. It belongs to the site, and it sits with the site's other declaration,
nav.toml.
A second dataset for the site. docs/generated/graph.json already carries every node and
every edge, is generated by the executable and is drift-checked. The site's entities.json
is a projection of it, not a second reading of the layer.
A search service, or Zola's built-in search index. A remote service is a dependency a
static site does not need. Zola's index covers every word of every page, adds a runtime
library, and would be a second listing of the pages beside the one this build already
writes. A filter over the titles and descriptions of /docs/index.json answers "where is
it", which is the question a documentation index is for.
ExDoc. The mission that asked for the hub named it. Majordomus has no Elixir code, so
running ExDoc would mean running a toolchain to imitate its own output. The properties that
make ExDoc useful are an index by kind, a stable page per entity, source links, cross
references, search and deterministic output, and each of those is a projection this
repository already had or has here. The crate's own API reference is cargo doc, which
another change publishes.
Consequences
- A file added under
.ai/gets an index row, an address, a page, its place in every other page's backlinks, an MCP resource, an HTTP answer and a command-line answer, with no edit to any consumer. The acceptance test writes one rule and asserts exactly that. - A kind added to the layer fails
entity-checkuntil somebody decides, in writing, whether the site publishes it. That is deliberate friction in the one place silence was expensive. graph::RELATIONSbecomes load-bearing for a user-facing page rather than only for a drawing and a check. A relation declared there is now visible in both directions immediately, which is the point, and a relation left out of it is invisible in both.- The published site grows one page per decision and per rule. They are generated from the objects' own files, so the prose has one home and the pages cannot drift from it.
- An entity page may say what an object names as its executable artefacts and whether the
tree holds them. It may not say the object is enforced:
resolvedis a statement about paths, and whether any of them ever ran isrules.show's question, which the page links to rather than answering. /docs/counts nothing of its own, and/docs/index.jsonlists nothing of its own. A kind, a page or an interface added to the repository appears in both because it was added. A declared route that fails to build fails the build, and a deployed commit whose documentation does not answer fails the Pages run.