A domain's page resolves from the project that PROVIDES the domain — the stem names a file, `template_root` names which tree, and a contested route registers for nobody

Accepted

ontoref
ADR-012 gave a domain a typed page declaration — id, route, title, template,

Context

ADR-012 gave a domain a typed page declaration — id, route, title, template, nav_label — and typed `template` as «filename stem in domain's pages/ dir». The INTENT was therefore written from the start: the template belongs to the domain. Two things were missing, and only one of them was visible.

NOTHING READ THE DECLARATION. `daemon/src/domain.rs` greps four scalar fields for light metadata and never reaches `pages`. `ui/handlers/pages.rs` rendered a hardcoded `pages/provisioning.html`, and `ui/mod.rs` hand-registered three routes. So `route`, `title` and `nav_label` had been declarable and inert since adr-012: an Ontology pole with no Reflection counterpart, a surface that describes and is never enacted.

NOTHING NAMED THE ROOT THE STEM HANGS OFF. `template` is a bare filename stem and no field said which tree to join it against. The biop cell `domain-page-lives-in-its-own-project` could therefore assert only the negative half — ontoref does not carry it — and never the positive one: it resolves from the provider.

MEASURED 2026-08-19. `provisioning`'s project root is /Users/Akasha/Development/provisioning, a separate ontoref-adopted repository carrying its own `.ontoref/`, AND this tree carried its page. `framework` and `personal` skipped because their roots do not resolve — so the one RED was a measurement, not an artefact of a broad sweep. `personal` in particular declares `personal` and `career`, and this tree carries both; they escape only because their provider cannot be located.

WHY THIS IS NOT A TIDINESS QUESTION. `Voluntary Adoption` is an invariant: projects adopt ontoref by choice and the protocol never imposes itself. A provisioning install that manages its own workspaces, infras and ops must not require ontoref's tree to render its own management surface, or the domain is hostage to a project-manager the operator may never adopt. ADR-001 makes the same cut on the code axis; this is its UI half.

AND ONE COLLISION WAS SITTING IN THE DECLARATIONS. `framework/domain.ncl:27` and `provisioning/domain.ncl:102` both declare route `/<slug>/provisioning` with template stem `provisioning`, differing only in id and nav_label. Inert while nothing read `pages[]`; a live conflict the moment anything did, and axum panics on a duplicate route rather than choosing.

Decision

`domain_page_type` gains `template_root`, an ENUM and never a path:

'Provider the project that provides the domain, located by `domain-root <id>`. THE DEFAULT. Its template lives at <provider_root>/.ontoref/domains/<id>/pages/<template>.html (amended 2026-08-26 by adr-102; it was `.ontoref/pages/` here, flat and provider-wide, which one provider providing two domains could not express). 'Protocol ontoref's own tree — the self-domain exemption and nothing else.

The daemon READS the declaration: `domain.rs` exports `pages[]` with nickel and resolves a template through `resolve_page_template`, which takes an already-resolved provider root as an ARGUMENT so it can never resolve one itself. `ui/mod.rs` projects declared pages into routes.

A page that cannot be resolved answers with a NAMED diagnostic — `ProviderRootUnresolved` or `TemplateMissing`, each carrying the domain, the page and where it looked — never a silent 404 and never a fallback that returns 200 over an empty body.

A route claimed by more than one domain is registered for NEITHER claimant and both are named in an error.

Constraints

  • Hard `template_root` MUST be an enum over declared roots. It may never be typed as a String path.
  • Hard `template_root` MUST default to 'Provider. A page that declares nothing gets the strict reading and never ontoref's tree by accident.
  • Hard The page resolver MUST receive an already-resolved provider root as an argument. It may never locate one itself.
  • Hard A declared page that cannot be resolved MUST produce a diagnostic naming the domain, the page and the location searched. Never a bare 404, never a fallback body.
  • Hard A route claimed by more than one domain MUST be registered for none of them, and every claimant named.
  • Hard ontoref MUST NOT carry a page template for a domain whose project root is not this tree.

Alternatives considered

  • Type `template_root` as a String pathrejected: Makes a location true on one machine into a contract (bl-048), and would serialise an absolute host path into every consumer's exported domain data. The biop cell already names this as the reason it can assert only the negative half of its claim.
  • Land the provider's template at `<provider_root>/pages/`rejected: The most literal reading of adr-012's «domain's pages/ dir», and rejected because it plants an ontoref-owned directory at the top of a repository that may barely use ontoref. The spine already exists in any project whose root resolves.
  • Keep the template in `code/domains/<id>/pages/`rejected: The wrong cure the biop cell was rewritten to close: it satisfies «the daemon does not hand-list it» while moving the coupling one directory. The template would still be inside ontoref, and the domain still hostage to it.
  • Resolve a contested route first-wins, or last-wins, with a warningrejected: Makes ownership depend on directory read order — machine-dependent, silent, and changed by a rename. The page keeps working while nobody can say which domain owns it, which is worse than losing it loudly.
  • Fix framework/domain.ncl so no collision exists, and let the reader assume uniquenessrejected: Smallest code, and it leaves the next collision undetected while removing the only instance that exercises the rule. A mechanism nothing runs is the failure adr-063 measured directly.

Anti-patterns

  • The enum grows a path variant — A domain needs its pages somewhere unusual and the shortest fix is a third variant carrying a String — or the enum is replaced by a path outright «just for this case». The declaration stops being portable, and one machine's layout enters every consumer's exported data.
  • The gap is papered over with a generic page — A provider has not written its template yet, the route 500s, and someone adds a default body so the page «at least renders». It returns 200 over an empty heading, nothing logs it, and the difference between a page nobody wrote and a page that works disappears.
  • First-wins arrives as a convenience — The contested route annoys someone during a demo and the projection takes the first claimant with a warning. The page works again, and which domain owns the route now depends on directory read order — invisible until a rename changes it.

Related ADRs

ADR-001 · ADR-012 · ADR-076 · ADR-081 · ADR-086 · ADR-087

Was this useful? Rate it
Got something to add? Tell me what you think, what you'd suggest, or whether we should keep exploring this topic.
· reads

We use cookies to help this site function, understand service usage, and support marketing efforts. Cookie Policy for more info.