A service declares what it needs to be deployed; whoever answers for a need declares it on its own side; the bond carries neither
Accepted
Context
An infrastructure that mounts a service reconstructs what the service needs by archaeology, and nothing tells either side when the reconstruction is wrong. Measured 2026-10-03/04, re-read 2026-10-09:
- workspaces/libre-wuji/appserv/libre-wuji/ontoref-website.ncl declares RUSTELO_SITE_ROUTES, SITE_BASE_URL and SITE_NAME itself, citing config.rs:68, seo.rs:361 and Dockerfile.htmx-ssr:124 of other repos. SITE_BASE_URL and SITE_NAME were declared in NO layer; ontoref.dev served canonicals and og:image against http://127.0.0.1:3030 with a 200. The infra closed the symptom; the service still declares nothing. - Two SERVICE-side declarations already exist, both published by the domain that owns the machinery: rustelo's website-htmx-rustelo/code/provisioning/distro.ncl (image layer vs site layer, reload classes hot / config_reload / restart / rebuild, writable migrations) and opus's opus-ontoref/code/crates/opus-editorial/contracts/instance-tree.ncl (parts, reload hot / restart, `writable`, `layered` over the binary; «declares no origin and no way to fetch: how an instance reaches a host is the deploy's decision»). DD-eca deleted its own copy on 2026-10-07 because «the tree is the domain's» (DD-eca/code 6ed0e98). The service side is not missing; a COMMON SHAPE for it is. - distro.ncl:3 names lian-build/build_directives.ncl as its consumer; build_directives.ncl imports only defaults/build_directives.ncl and jpl-project.ncl, and `image_only` appears nowhere under code/lian-build/. A declared consumer that does not read (adr-070). - provisioning/catalog/components/opus_editorial/nickel/contracts.ncl restates «TLS ends at the Gateway listener» by hand; nothing binds it to the instance tree. - outreach/site/update-content.sh → website-htmx-rustelo/code/provisioning/content.nu publish: the sync of what is published, with its reload classes and delivery stamp inside the pod, lives in a loose script the infra knows the location of and not the meaning. - libre-wuji measured the cost of two writers and did NOT conclude «one owner always»: adr-021 one writer per FIP CLASS, adr-022 multi-writer resources applied server-side with a stable field-manager, adr-023 a fact (ALPN) the infra knows and the service does not. Cardinality, not uniqueness, is the practice. - code/ontology/schemas/stewards.ncl DisjointSurfaces (adr-121, superseded) typed «who answers» with one steward per surface by contract — the uniqueness the deploy evidence refutes. - The only protocol notion of «what it needs to run» is manifest.ncl `requirement_type`: per project, prose, no context. No `deploy` or `needs` verb exists (`ontoref help`). - The bonds between site, machinery and provider already exist (.domains-ontoref/{provisioning,rustelo} in outreach/site and here). What crosses them does not.
The open question the framework brief left to this record: is the relation between a service's needs and those who answer for them a bond (adr-073), an extension of the bond, or a new relation?
Decision
THE NEED IS THE SERVICE'S, DECLARED ONCE, IN THREE LAYERS. The protocol types the base shape of a deploy need in code/ontology/schemas/deploy.ncl; the domain that owns a service class publishes its class over that base (rustelo: what a site needs from the rustelo image; opus: an instance of opus-editorial); the project gives the values. A need carries an id, a kind, why it is needed (the case that paid for it), and whether it is required. Its kinds are seeded ONLY with the witnessed ones — env, secret, config, state, asset, layer, port, tls, liveness, sync — and grow by evidence. A need that is a part of an instance also says its layer ('Image | 'Instance), its reload class ('Hot | 'Reload | 'Restart | 'Rebuild) and whether the service writes it: the common shape of distro.ncl and instance-tree.ncl, which become the domains' classes over the base, not copies of it. A need names what is required and why — never how a host realises it, never an origin, never an operation.
The carrier is the bond's own: `.domains-ontoref/<domain>/needs.ncl`, beside the bonds.ncl of the domain whose service class the project instantiates. A project at the minimal altitude (bonds only, no spine, like outreach/site) declares its needs without adopting anything else.
WHOEVER ANSWERS FOR A NEED DECLARES IT, ON ITS OWN SIDE. Building and rebuilding the image, publishing, syncing, terminating TLS, holding secrets, keeping state: each is an answer, declared by the party that gives it in `.domains-ontoref/<domain>/answers.ncl`, naming the service and the need id it answers, the act, and its CARDINALITY — 'Sole, or 'Shared with the mechanism that reconciles the writers (libre-wuji adr-022). A party answers for a domain's service class only from a carrier of that domain, i.e. only if it bonds to it: the image builder and the infra bond to the domains whose services they build and run, on the Dependency axis, as the providers they are. How the infra realises an answer (IPs, gateways, nodes, listeners) stays in the infra's repo; the shared answer carries the act, the responsible party and a reference to the artefact, nothing more.
THE BOND CARRIES NEITHER. No field is added to bond.ncl and no new relation is introduced. A bond already resolves against the roots of its endpoints and never on the edge (glossary::bond); a need and an answer are exactly what lives at the roots, joined by reference (service id + need id) as a bond's `capabilities` reference a domain's catalog. Who answers a need is COMPOSED when asked — the service's needs, the registry's projects (adr-129) that carry an answers.ncl naming that service — never a copy kept by hand on either side.
THE CHECK REPORTS DIRECTION, PER ITEM. Comparing a service's declaration with an infra's artefact yields, for each need, 'InStep, 'ServiceAhead (declared, unanswered or absent from the artefact), 'InfraAhead (the artefact sets what no need declares — adr-023's ALPN), or 'Diverged. Neither side wins by construction. Only a REQUIRED need with no answer, or two 'Sole answers to one act, is a refusal; the rest is a report.
OPERATIONS ARE MODES OF WHOEVER ANSWERS. Publishing, syncing, rebuilding and rotating are becoming, not being: they are modes (reflection) of the party that answers for them, referenced from its answer, and never fields of a need. update-content.sh becomes the publish mode of whoever answers for the site's sync.
VERBS. `ontoref deploy needs [<project>]` (what do you need?), `ontoref deploy who <project> [<need>]` (who answers this, and is it one or shared?), `ontoref deploy check <project> --against <answers>` (per-item direction), `ontoref deploy audit <project>` (what a declaration lacks against its domain's class). They read and report; they never write another party's carrier.
OPT-IN. A project that declares no needs.ncl is not bound; a party that answers nothing carries no answers.ncl. The schema, the verbs and the witness case (ontoref's site) are a successor SOW to the one that gates this decision; protocol_version does not move with it (adr-069).
Constraints
- Hard bond.ncl declares no need, answer, responsibility or cardinality field: deploy facts live at the roots
- Hard manifest.ncl grows no deploy field: needs are declared in the bond carrier, reachable at the minimal altitude
- Soft The base Need type carries no command, script, mode, origin or fetch field; an operation is a mode of whoever answers
- Soft `ontoref deploy who` composes answers from the parties' carriers; it refuses a required need with no answer and two 'Sole answers to one act, and reports every 'Shared answer with its reconciliation
- Soft `ontoref deploy check` classifies every need and every undeclared artefact fact as InStep, ServiceAhead, InfraAhead or Diverged, and never rewrites either side
Alternatives considered
- Extend the bond: a responsibility edge on the Dependency axis carrying act, cardinality and reconciliation — rejected: It puts definitions on the edge, which the bond refuses by its own definition, and needs edges between projects that are not domain roots (the builder, the infra workspace). The bond would stop meaning what the glossary freezes.
- A new relation `answers` between parts (a need, a layer) and parties — rejected: It closes adr-086's open relation through one case and freezes a general vocabulary from a shape that turns out to need only a reference. A second relation word next to bond is also a second place for project↔project semantics to diverge.
- Reuse stewards.ncl DisjointSurfaces as the type of «who answers» — rejected: It enforces one steward per surface; libre-wuji's adr-021/022 measure that multi-owner, reconciled resources are legitimate. It would also give «responsibility» two senses in the protocol.
- Declare the needs on provisioning's side (ServiceDefinition / component contracts) — rejected: That is today's state: the platform restates what each service knows, and the restatement drifts unseen (opus_editorial's «TLS ends at the Gateway»). The provider becomes a reader of the service's declaration instead.
- Grow manifest.ncl `requirement_type` into typed deploy needs — rejected: The manifest is the managed project's identity; a project at the minimal altitude has none, and adr-073 already kept membership out of it for that reason. requirement_type is also per project, with no class a domain could publish.
Anti-patterns
- Needs rebuilt by archaeology — An infra declaration states what a service needs by citing the service's source lines in other repos, so the service never declares it and the infra's copy drifts unseen.
- The declaration wins — A deploy check compares one way and overwrites or fails the infra wherever it differs from the service's declaration, erasing what the infra knows and the service does not.
- One owner by contract — Every responsibility is forced to a single answerer, so legitimately shared, reconciled resources must be misdeclared or split.
Related ADRs
ADR-073 · ADR-086 · ADR-070 · ADR-074 · ADR-088 · ADR-120 · ADR-121 · ADR-125 · ADR-129 · ADR-069
· reads
Perspectives