Compare commits

..
Author SHA1 Message Date
mesh-admin 5a6b7ca4f8 Merge pull request 'Issue 178: a routed name resolves to a provider merely told it, and flips between plans' (#241) from fix/227-a-name-resolves-to-the-node-that-serves-it into main 2026-10-01 00:05:14 +00:00
jschoubben e1f2c6bd5b Issue 178: a routed name resolves to a provider merely told it, and flips between plans (fixed, controller PR 181) 2026-10-01 02:04:55 +02:00
mesh-admin cf8134e318 Merge pull request 'Issue 177: the controller's check is run by nobody, and two of its tests failed for days unseen' (#240) from fix/the-converged-declaration-guard into main 2026-09-30 23:38:36 +00:00
jschoubben 35f7f4401b Issue 177: the controller's check is run by nobody; the two rotted tests fixed (controller PR 180), the process half open 2026-10-01 01:38:22 +02:00
mesh-admin 62d61938ad Merge pull request 'Issue 176 resolved: a build is taken in where its outcome is heard' (#239) from fix/176-a-build-is-registered-where-it-is-heard into main 2026-09-30 23:28:10 +00:00
jschoubben f841845b0d Issue 176 resolved: a build is taken in where its outcome is heard; the build tool answers with the id 2026-10-01 01:27:49 +02:00
mesh-admin 3627f7e9db Merge pull request 'ADR 0157: a build says what it does on the bus, as it happens' (#238) from feat/a-build-says-what-it-does into main 2026-09-30 22:43:59 +00:00
jschoubben 2ffe1d0915 ADR 0157: a build says what it does on the bus, as it happens; designs 25 and 18 carry it 2026-10-01 00:43:26 +02:00
mesh-admin 763e327610 Merge pull request 'Issue 176: the console's build tool neither waits nor registers, and does not take a forge path' (#237) from issue/176-the-consoles-build-tool-neither-waits-nor-registers into main 2026-09-30 22:29:25 +00:00
jschoubben 93f828c5eb Issue 176: the console's build tool neither waits nor registers, and does not take a forge path 2026-10-01 00:29:05 +02:00
mesh-admin fe706af63a Merge pull request 'Issues 173 and 174 resolved; the installation check refuses at registration' (#236) from feat/the-mesh-places-its-own-files into main 2026-09-30 22:07:38 +00:00
jschoubben 52e9df0f02 Issue 153 resolved: an assignment places a module's directories and its accesses
mesh-controller PR 176 and mesh-catalog PR 198. Designs 27 and 18 carry the words: places,
accesses, ${access:<id>}, the default a definition still holds while the catalogue converts.
2026-10-01 00:04:10 +02:00
jschoubben 36454d7e4a Issues 173 and 174 resolved; the installation check refuses at registration (ADR 0155)
A setting overrides a key a contribution or served fact declares and adds none; a provider that must
tell its consumers an operator's value declares it as ${setting:…} (173). The mesh's own files for a
module are a placed directory, `place: "mesh"`, and forty-eight definitions name no host path for
them (174). Registration refuses a definition naming an installation, the day the list emptied
rather than a release later (0155, progressive insight; 134). Designs 27 and 18 carry the rules.
2026-09-30 22:36:20 +02:00
jschoubben e84c822e89 Merge pull request 'Issue 175: the link to issue 127 resolves' (#235) from fix/issue-175-link into main 2026-09-30 20:10:41 +00:00
jschoubben a170913202 Issue 175: the link to issue 127 resolves 2026-09-30 22:10:38 +02:00
jschoubben 9a20c16d9b Merge pull request 'Issue 175: an announcement queued behind a long build came back, and the build ran again' (#234) from fix/one-announcement-at-a-time into main 2026-09-30 19:38:49 +00:00
jschoubben 8bd0ca0bdc Issue 175: an announcement queued behind a long build came back, and the build ran again 2026-09-30 21:38:45 +02:00
jschoubben 598f6a8952 Merge pull request 'ADR 0156: an artifact is what a build produces, and the store's seat is named for its scope (group 4, step 3)' (#233) from feat/the-artifact-store-seat-is-named-for-its-scope into main
Reviewed-on: #233
2026-09-30 19:17:28 +00:00
jschoubben 3341c037cb Merge pull request 'Issue 119 resolved for a module's own data; issue 174 for the mesh's files (group 4, step 2)' (#232) from feat/definitions-place-their-directories into main
Reviewed-on: #232
2026-09-30 19:17:21 +00:00
jschoubben 22a28ad548 ADR 0156: an artifact is what a build produces, and the store's seat is named for its scope
Issue 123 resolved; glossary corrected; design 26 and ADR 0121 point at the rename.
2026-09-30 21:14:40 +02:00
jschoubben 1e1957a9c4 Issue 119 resolved for a module's own data; issue 174 for the mesh's files; design 27 phase 3 in part 2026-09-30 21:11:42 +02:00
jschoubben 5292f4176a Merge pull request 'Issue 173: a module's settings reach every fact it contributes; what the site's rename cost' (#230) from feat/group-4-step-1-closed into main 2026-09-30 19:04:36 +00:00
jschoubben 822e8b03f8 Issue 173: a module's settings reach every fact it contributes; what the site's rename cost 2026-09-30 21:04:34 +02:00
jschoubben a82941ee0c Merge pull request 'ADR 0155: a definition names no installation, how that is checked, and the three ways out (group 4, step 1)' (#226) from feat/a-definition-names-no-installation into main
Reviewed-on: #226
2026-09-30 18:36:42 +00:00
jschoubben 9ffb7eec55 ADR 0155: a definition names no installation, how that is checked, and the three ways out
Issues 122 and 134 resolved; design 27 in progress with its first cases; design 18 names the words.
2026-09-30 18:40:05 +02:00
jschoubben 7499f1e50c Merge pull request 'Issue 006 resolved: the record is read where it is written, and the console lists it' (#225) from feat/group-3-closed into main
Reviewed-on: #225
2026-09-30 16:19:50 +00:00
jschoubben 214b486a50 Design 33 implemented: the mesh's verbs answer through the console, and what shipped bent 2026-09-30 18:18:44 +02:00
jschoubben 90b44a48df Issue 006 resolved: the record is read where it is written, and the console lists it
Design 35 implemented with what shipped and the live check; 006 closes on ADR 0025's own test,
run through the console.
2026-09-30 18:10:18 +02:00
jschoubben 860331dc37 Merge pull request 'ADR 0153 and ADR 0154: the record is read by a module, and the mesh's verbs are its seat's tools' (#224) from feat/the-mesh-answers-for-itself into main
Reviewed-on: #224
2026-09-30 15:55:18 +00:00
25 changed files with 948 additions and 36 deletions
+5 -2
View File
@@ -52,8 +52,11 @@ another — and a mesh you cannot name precisely is a mesh two people describe d
- **package** — what code resolves when it is **compiled**: an npm/cargo/pypi dependency, by
**version**. Served by the **package-registry** (gitea). Only a builder talks to it.
- **artifact** — what the mesh delivers to a machine to **install and run**: an OCI image, by
**digest**. Served by the **artifact-store** (distribution). Every node pulls from it.
- **artifact** — anything a build produces and the mesh delivers to a machine by **digest**: an
`image`, a mirrored `upstream` image, a `bundle` of the module's own code, an `archive`. Served by
the **artifact-store**, an OCI registry that holds every kind as content-addressed blobs
([ADR 0156](../02-DECISIONS/0156-an-artifact-is-what-a-build-produces-and-the-store-is-named-for-its-scope.md)).
Every node pulls from it. An image is one kind of artifact, and a module is not an image.
- These are two protocols, not one store being weak — see [ADR 0075](../02-DECISIONS/0075-two-stores-and-which-provides-what.md).
## How modules relate to the mesh
@@ -81,7 +81,9 @@ closed set stays what its name says it is: the *system's* roles, not everyone's.
- **`the-uplink` → `node-uplink`** ([ADR 0125](0117-a-machines-uplink-is-a-seat.md)). Unheld, so it
renames with no migration.
- **The registry seats — `the-artifact-store`, `npm-package-registry` (→ `mesh-artifact-store`,
`mesh-npm-package-registry`) — and `git` (→ `mesh-git`) — are decided but deferred.** They each
`mesh-npm-package-registry`) — and `git` (→ `mesh-git`) — are decided but deferred.** *(The
mechanism changed — 2026-09-30, by ADR 0156: `the-artifact-store` is renamed, one update and one
alias under ADR 0122; the other two stay deferred.)* They each
*deliver* a provision, so renaming them is a delivering-seat migration: a holder that stops
resolving mid-flight takes a provision away from every consumer. That risk is not worth carrying in
the same pass as the node-* renames, so they keep their names until done deliberately.
@@ -0,0 +1,128 @@
---
topic: what runs on it
status: accepted
date: 2026-09-30
deciders: jochen
reconstructed: false
extends: 02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md
---
# 155. A definition names no installation: how that is checked, and the three ways a value that did gets out
## Context
[ADR 0112](0112-a-module-definition-names-no-node-mesh-or-path.md) decided that a module definition
names no node, no mesh and no host path, and said how the name half is checked: *a catalogue test
finds no domain name in any definition value*. No such test existed
([issue 134](../04-ISSUES/134-a-definition-may-still-name-the-mesh/00-report.md)). Written and run
over the 77 definitions on 2026-09-30, the check it describes finds **42 values**, in 15 definitions,
and they are of four kinds that want four different answers:
| kind | count | example |
|---|---|---|
| a service told its own public name as a literal | 5 | an identity provider's `KC_HOSTNAME`, an object store's console redirect, an automation tool's webhook URL |
| an operator's value written into the definition | 10 | a mail server's domain, site name, website, and the address it trusts a real-IP header from |
| this mesh's forge, by URL, as a recipe's build context | 2 | the builder and the proxy, which package the controller's source |
| an application built outside the mesh, pulled from this mesh's registry | 7 | four sites and tools whose repositories are the operator's own |
| the world's servers, named by upstream defaults | 10 | a Matrix homeserver's trusted key server, Element's integration manager |
| a module named after the domain it serves | 8 | one site module, with its paths and network named after it |
Not one was careless. Each was the value the software needs, and until today there was nowhere else
to put it ([issue 122](../04-ISSUES/122-a-module-cannot-ask-for-its-own-public-name/00-report.md)).
Two of the answers were built before this record: a module is told the name its route composes
(`${bound:<route>:name}`, controller PR 149, 2026-09-30), and a source may be a path on the git seat
([ADR 0111](0111-a-build-source-is-on-the-git-seat-or-external.md)). What was missing: an operator's
value in a file the software reads, the same for a build *context*, a way to say a name is meant, and
the check.
## Considered Options
**1. A string search for the installation's own names.** Rejected. The controller is as
mesh-agnostic as the definitions; it does not know which names are "this mesh's", and a check that had
to be told would be configured per installation and pass everywhere else. What it can know is the
*shape*: a name under a public top-level domain, a public address.
**2. Report every such shape.** Rejected. Eight of the 42 were `why` strings — prose the mesh never
reads, explaining what a port is for — and a check that reports those beside `KC_HOSTNAME` teaches
people to ignore the report. And a Matrix homeserver *must* name the federation's public key server;
a check with no way to say so would be a check people argue with rather than obey.
**3. Judge what the mesh acts on; let a definition say which names it means, one by one, with a
reason; exempt the world's services that a definition may name as a policy default.** Chosen.
**For an operator's value**, one option was to wait for design 27's requirement form in full. Rejected
for the reason 0112 gave against a slow operator provider: if asking a person for a value takes more
than a setting, module authors route around it and the literals come back. `${setting:<key>}` is the
operator provider in its first form, on the settings a module already has.
## Decision
**The check.** Every string value of a definition that the mesh acts on is judged for a hostname under
a public top-level domain and for a public address. Not judged: `why` and `description`, which are
prose. Allowed where they can only mean the world: the public registries an `image` may be pulled
from, the public resolvers a machine may forward to, and the public certificate authorities' ACME
directories. The container runtime's alias for its own host is the runtime's. A module's own name is a
value too. The check runs in `module check` and as a catalogue-wide test; **it does not yet refuse at
registration**, because the list it prints is the list that shrinks, and a registration that refused a
manifest whose only remedy is a merge elsewhere would refuse the mesh's own catalogue on the day the
check landed. It moves to registration when the list has been empty for a release.
> **Progressive insight — 2026-09-30.** The list was empty the day the check landed — every remaining name declared with its reason — and the operator asked for registration to refuse at once rather than after a release. It does, since mesh-controller PR 175: `module add` and a build's result are refused in the check's words, naming the way out, and the build stays recorded. The decision stands; only the day moved.
**A name a definition means is declared with its reason.** `names-on-purpose` on a resource maps each
such name to why: *the federation's public key server, the world's*; *built outside the mesh, from the
application's own repository, until that repository is a build source on the git seat*. A name the map
does not cover is still reported. The host never sees the word.
**An operator's value reaches a file as `${setting:<key>}`**, filled from the module's settings
layers — the mesh's, then the node's — the same layers a mergeable file and a contribution take, so
`settings set <module>` stays the one place a person's values go. Refused, naming the key and the
command, when nothing set it: a default for a mail domain would be the literal this removes, and a
blank written silently would be a service that comes up wrong somewhere that names nothing.
**A build context may live on the git seat.** `context: {"seat": "git", "repository": "<owner>/<name>"}`
is composed by the mesh that builds it: the request carries each seat's clone base, and a builder told
no base for a seat a context names refuses the build by the seat's name rather than guessing a forge.
**A module is named for what it is.** The site module named after its domain is `website`.
## Consequences
- **The catalogue names no installation, and a test says so.** The forty-two became zero the same day,
by the four answers above; seven of them are declared on purpose and stay visible as the list to
shrink — four applications the mesh does not build yet.
- **An operator's values are the assignment's.** The mail module takes its domain, its site name, its
website and the address it trusts a real-IP header from as settings; a mesh that installs it without
them is refused at composition, by name, which is the right moment. The module's own README says
which.
- **What got harder:** a manifest reviewer has one more word to read, and `names-on-purpose` on an
application's image is a debt visible in the definition until the application is built here. A
reader of `settings set` output sees more keys than files, because a key a file asks for is a
destination too.
- **Not decided here:** ADR 0112's requirement form (design 27) still replaces `${setting:…}` and the
other placeholders when it lands; this is its first case, the way [ADR 0038](0038-the-mesh-assigns-the-port.md)
was for ports. Host paths ([issue 119](../04-ISSUES/119-a-module-definition-decides-where-its-files-live/00-report.md))
are the next step of the same group, and the registry's name ([issue 123](../04-ISSUES/123-the-image-registry-is-named-after-a-role/00-report.md))
the one after.
## How this is checked
| Rule | Checked by |
|---|---|
| A value naming an installation is reported at its path, in the definition's words | a catalogue unit test over a definition with a hostname in an env value and a public address in a file |
| Prose, the world's registries in an image, public resolvers, ACME directories and the runtime's own alias are not reported | the same tests |
| A name declared on purpose is not reported; a name beside it that is not declared is | a test with a homeserver's config |
| An image from an installation's registry needs a reason | a test without and with the word |
| A module named after a domain is reported | a test |
| No definition in the catalogue names an installation | `TestNoCatalogueManifestNamesAnInstallation` over the checkout, and `module check modules/` |
| A definition naming an installation is refused at registration, and one declaring its names passes | `TestRegistrationRefusesADefinitionNamingAnInstallation` (2026-09-30) |
| `${setting:key}` fills from the layers, node over mesh; refused by name when unset; not stray when set | three tests |
| A context on a seat is cloned from the base the mesh sent; a seat with no base is refused by name | the builder's test |
## References
- [ADR 0112](0112-a-module-definition-names-no-node-mesh-or-path.md) — extended: the check it promised, and the operator provider's first form
- [ADR 0111](0111-a-build-source-is-on-the-git-seat-or-external.md) — a source on the seat; now a context too
- [issue 122](../04-ISSUES/122-a-module-cannot-ask-for-its-own-public-name/00-report.md), [issue 134](../04-ISSUES/134-a-definition-may-still-name-the-mesh/00-report.md) — what this closes
- [27 — A module requires, the mesh resolves](../03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md) — where this sits in the larger design
- mesh-controller PR 169, mesh-catalog PR 188 — the check, the words, and the catalogue that passes it
@@ -0,0 +1,83 @@
---
topic: the mesh
status: accepted
date: 2026-09-30
deciders: jochen
reconstructed: false
extends: 02-DECISIONS/0075-two-stores-and-which-provides-what.md
---
# 156. An artifact is what a build produces, the artifact store serves every kind, and its seat is named for its scope
## Context
[Issue 123](../04-ISSUES/123-the-image-registry-is-named-after-a-role/00-report.md) found three
wordings disagreeing about the mesh's registry. The glossary defined *artifact* as "an OCI image, by
digest"; the manifest's build vocabulary names four kinds — `image`, `upstream`, `bundle`, `archive` —
and the catalogue builds all four; the seat was `the-artifact-store`, the last of the mesh's own seats
named after the job it does rather than for the mesh
([ADR 0121](0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md) decided the
rename and deferred it). The issue asked whether the seat and provision should be renamed after
images, and whether the mesh needs two registry implementations at all.
Reading what the store actually serves settles the first question the other way. A kept reference
has two shapes — `artifact-store://<module>/<artifact>@sha256:…` for an image and
`artifact-store://<module>/<artifact>/blobs/sha256:…` for an archive — and both are served by the
same OCI registry, by digest. [ADR 0075](0075-two-stores-and-which-provides-what.md) already
defined the provision that way: *content-addressed blobs, pinned by digest, no versions, no ranges;
what the mesh delivers to machines*. The provision was never an image registry. Only the glossary said
so, and only the seat's name was odd.
## Considered Options
**1. Rename the seat and the provision after images.** Rejected. The store serves archives too, by the
same protocol; naming it for one kind would be the glossary's mistake made permanent, in the name
every manifest uses.
**2. Fix the word, rename the seat for its scope, keep the provision.** Chosen. The rename ADR 0121
deferred as a delivering-seat migration is, since [ADR 0122](0122-a-seat-is-data-a-rename-is-a-database-update.md),
one update and one alias: the former name resolves forever, a held record follows by cascade, a claim
written with the old name still holds.
**On two implementations:** left as 0075 decided. Two provisions because two protocols; the OCI
registry the genesis installs because something must serve images before the mesh can build; the
forge may provide `artifact-store` too and a mesh may choose it. The bootstrap argument is weaker than
it reads, as 123 says, and the day the forge is raised at genesis and adopted in place is the day to
retire the second server — a migration a mesh performs, not a decision to take here.
## Decision
- **An artifact is anything a build produces** — an image, a mirrored upstream image, a bundle, an
archive — and the glossary says so. *Image* is one kind. A module is not an image; a module may
build several artifacts and install none.
- **The artifact store serves artifacts of every kind a machine fetches**, images and archives, by
digest, over the OCI registry protocol. The provision keeps its name.
- **The seat is `mesh-artifact-store`.** `the-artifact-store` is its alias. The catalogue's registry
module claims the new name; a definition elsewhere claiming the old one still holds.
- The two other deferred renames — `npm-package-registry` and `git` — stay deferred, and for the
same reason no longer. They are one migration each when wanted; nothing here needs them.
## Consequences
- The glossary stops contradicting the manifest vocabulary, and a reader of `artifact-store` reads
it as what it is: where the mesh's built things are kept.
- One migration on the seat table; no manifest but the registry's changes; no consumer of the
provision changes, because the provision did not.
- **What got harder:** nothing measurable. A record that says `the-artifact-store` is read through the
alias; design 26's table already carried the new name as intent.
## How this is checked
| Rule | Checked by |
|---|---|
| The former name resolves to the seat once the store's aliases are loaded | a catalogue test |
| The seat is in the compiled set under its new name, delivering `artifact-store` | the seat tests, updated |
| The registry module holds the seat under the new name on the live mesh | `seats` after the rollout |
| The glossary's *artifact* matches the build kinds a manifest may declare | design 18's table and the showcase module list the kinds; the glossary names the same four |
## References
- [issue 123](../04-ISSUES/123-the-image-registry-is-named-after-a-role/00-report.md)
- [ADR 0075](0075-two-stores-and-which-provides-what.md) — extended: the provision as defined stands, the word is corrected
- [ADR 0121](0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md), [ADR 0122](0122-a-seat-is-data-a-rename-is-a-database-update.md) — the rename, decided and made cheap
- mesh-controller migration 0048; mesh-catalog `modules/distribution`
@@ -0,0 +1,99 @@
---
topic: the mesh
status: accepted
date: 2026-10-01
deciders: jochen
reconstructed: false
extends: 02-DECISIONS/0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md
---
# 157. A build says what it does on the bus, as it happens
## Context
[ADR 0121](0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md) made a build work submitted to a role: the
build-machine seat accepts a build and emits its outcome, one publish that reaches whoever asked, the
controller that records it and the catalogue that places it. Everything **between** the request and
the outcome — which command is running, how long it has taken, where it hung, the compiler's error,
the clone's refusal — lived in one container's standard error on one machine.
The night of 2026-09-30 showed the cost three times over. A build that failed showed a person one
line, the first of its failure, in the controller's `builds`; the rest was read with `docker logs` over
ssh, which the mesh's own rule forbids. A build that ran for minutes could not be told from one that
had hung. And the builder has no tools and emits nothing but the outcome, so the console
([ADR 0152](0152-the-operators-surface-is-a-module-the-console.md)) had nothing to show while a
build ran, and no viewer could be built on top of it. The operator's ask was plain: the builder is to
be fully transparent, with its log on the bus, so that a log viewer can be built on the bus later.
## Considered Options
1. **Keep the log in the outcome.** The result carries the whole log when the build ends. Nothing new
on the bus; nothing while the build runs; a viewer sees a build only once it is over, which is
exactly when the log matters least.
2. **A log store.** The builder writes its log to a file or a table and a tool reads it. A second
place to keep something the bus already carries, with its own retention, access and failure modes,
and no live reading without inventing a subscription over it.
3. **The log is the role's own events.** Two more events on the build-machine seat beside `built`:
`started` when work is taken, and `log.<build id>` for every line, published as the build runs.
The events stream already retains every role's events for a week, so a reader follows a build
live by subscribing its subject, or reads it back afterwards from the stream, and a viewer is a
subscriber and nothing more.
## Decision
**Option 3.** A build machine says everything it does on the bus, as the role it holds, under the
build's id, and the mesh keeps no other copy.
- The build-machine seat's protocol gains `started` and `log.*`. A holder may therefore publish
`mesh.seat.mesh-build-machine.event.started` and `…event.log.<id>`, and no other subject, by the
same derivation every seat's grants follow ([ADR 0132](0132-a-seat-carries-the-tools-its-holder-must-serve.md)).
The event's tail token is the build's id, so one build is one subject: a reader filters by subject
alone, on the server, and a week of other builds does not travel to show one.
- **Every line goes two ways**: to the machine's own standard error as before, and onto the bus. That
includes every command the builder runs, its duration and its failure, and on failure the command's
own output line by line — the compiler's words, the clone's refusal. A build machine with nobody
listening still prints; a listener reads the same lines.
- A line is a core publish, unawaited. The stream that holds the role's events captures it on its way
through, and a build does not slow to the pace of an acknowledgement per line. Each line carries a
sequence number from one, so a reader who joined late, or reads two copies, sees order and gaps.
`started` and `built` are published into the stream and awaited, because they are the two facts a
later reader must never find missing.
- **The mesh reads it back from the stream**, never from a record of its own: `builds --log <id>`, and
the same verb on the controller's seat ([ADR 0154](0154-the-meshs-own-verbs-are-the-controller-seats-tools.md)),
reads one build's subject with a consumer that is gone when the reading is done. `builds` lists
each build's id beside it, and `build` says the id it asked with, so a person can follow.
- Nothing is declared by the builder module for this. The protocol is the seat's, seeded additively
into the store ([ADR 0132](0132-a-seat-carries-the-tools-its-holder-must-serve.md)), and the
holder's grant follows on the next composition of the broker node.
## Consequences
- A build is watchable while it runs, from anywhere on the mesh, with no access to the build machine.
The console's gap of 2026-10-01 — no live progress, no per-merge view — closes on the progress half;
the per-merge view is a reader over these subjects and the outcome, and is not built here.
- A log viewer on the bus is now a plain subscriber: live on `mesh.seat.mesh-build-machine.event.>`,
historical from the events stream filtered by a build's subject. NATS carries and retains; it does
not view. The `nats` command-line client can tail or replay a subject today; a viewer of our own is
later work and needs nothing more from the builder.
- The events stream grows by a build's log per build, for a week. A build is a few hundred lines; the
stream's limits are the bound, as for every other event, and a stream that fills drops the oldest.
- A line the bus did not take is lost, deliberately, and visible as a gap in the sequence. The outcome
is not affected: a build's result never depended on its narration.
## How this is checked
| Rule | Checked by |
|---|---|
| The seat's holder may publish `started` and `log.<id>` and nothing wider | `TestTheBuildMachineMaySayWhatItDoesUnderTheBuildsId` (broker) |
| A build's lines reach a reader of its subject in order, and the stream holds them afterwards | `TestNatsABuildIsTakenAndItsOutcomeReachesEverybody` against a real server (link) |
| The seat verb `builds` with a build's id reads that build's log | `TestBuildsWithAnIdReadsThatBuildsLog` |
| Every command the builder runs is said, with its output on failure | `Command` speaks through the hook every build sets; the builder's tests still see the lines on standard error when nothing listens |
| Live: a build triggered after the roll-out is readable line by line through the console | done by hand after the merge of mesh-controller PR — see the design's note |
## References
- [ADR 0121](0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md) — extended: the role now narrates as well as answers
- [ADR 0132](0132-a-seat-carries-the-tools-its-holder-must-serve.md), [ADR 0154](0154-the-meshs-own-verbs-are-the-controller-seats-tools.md) — the protocol and the verb
- [ADR 0152](0152-the-operators-surface-is-a-module-the-console.md) — the console this feeds
- [Design 25 — The bus on NATS](../03-DESIGN/01-to-be/25-the-bus-on-nats.md) §3, [Design 18 — Building a module](../03-DESIGN/01-to-be/18-building-a-module.md)
- [Issue 176](../04-ISSUES/176-the-consoles-build-tool-neither-waits-nor-registers/00-report.md) — the tool that starts a build and hears nothing; this gives it something to hear
+3
View File
@@ -169,6 +169,8 @@ python3 00-META/checks/index.py fail if stale
- **0134** — [The mesh says what it applied](0134-the-mesh-says-what-it-applied.md)
- **0142** — [The mesh delivers its own components as binaries, not as container images](0142-the-mesh-delivers-its-own-components-as-binaries.md)
- **0154** — [The mesh's own verbs are the mesh-controller seat's tools, and which verbs those are](0154-the-meshs-own-verbs-are-the-controller-seats-tools.md)
- **0156** — [An artifact is what a build produces, the artifact store serves every kind, and its seat is named for its scope](0156-an-artifact-is-what-a-build-produces-and-the-store-is-named-for-its-scope.md)
- **0157** — [A build says what it does on the bus, as it happens](0157-a-build-says-what-it-does-on-the-bus-as-it-happens.md)
### Its tiers, from the bottom up
@@ -258,6 +260,7 @@ python3 00-META/checks/index.py fail if stale
- **0147** — [A module anchors the mesh's authority on a machine, and takes it away again](0147-a-module-anchors-the-meshs-authority.md)
- **0150** — [A module's own code runs as supervised processes under the module's one account](0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md)
- **0152** — [The operator's surface is a module the mesh assigns: the console](0152-the-operators-surface-is-a-module-the-console.md)
- **0155** — [A definition names no installation: how that is checked, and the three ways a value that did gets out](0155-a-definition-names-no-installation-and-how-that-is-checked.md)
### How it is built
+2 -1
View File
@@ -38,7 +38,8 @@ The console asks the `mesh-controller` seat's `tools` verb beside the modules an
tools as `<seat>.<verb>` — `mesh-controller.status`, `mesh-controller.push` and the other ten. A call
to `<prefix>.<name>` reaches the seat when the prefix is a seat declaring that verb, and the module
otherwise; `seat:<seat>.<verb>` says so outright. When the control plane does not answer, the list
names `mesh-controller (seat)` as not answering and carries the modules' tools regardless.
names `mesh-controller (seat)` as not answering and carries the modules' tools regardless. The
`mesh-controller` *module* is always named as not answering: it serves no module tools, only its seat's.
## Around it
+32 -5
View File
@@ -5,8 +5,9 @@ code:
- mesh-controller cmd/mesh-builder
- mesh-controller internal/builder
- mesh-catalog modules/builder
updated: 2026-09-30
updated: 2026-10-01
decisions:
- 02-DECISIONS/0157-a-build-says-what-it-does-on-the-bus-as-it-happens.md
- 02-DECISIONS/0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md
- 02-DECISIONS/0142-the-mesh-delivers-its-own-components-as-binaries.md
- 02-DECISIONS/0111-a-build-source-is-on-the-git-seat-or-external.md
@@ -186,7 +187,8 @@ disagrees with it.
| `grants` | credentials it must create for its consumers |
| `filtering` | rules beyond its own ports |
| `computed` | marks a module the controller generates rather than an author writing |
| `build.artifacts` | what it produces |
| `build.artifacts` | what it produces; an artifact's `context` may be a URL or a path on the git seat (`seat: git`), composed by the mesh that builds it |
| `names-on-purpose` | on a resource: each name it means to name — the world's federation server, a registry that built an application the mesh does not — with its reason. A definition names no installation, and a test says so ([ADR 0155](../../02-DECISIONS/0155-a-definition-names-no-installation-and-how-that-is-checked.md)) |
**A container mounts only what the manifest declares**
([ADR 0091](../../02-DECISIONS/0091-a-mount-is-declared-three-ways.md)). A bind mount the module
@@ -233,10 +235,10 @@ counts as a copy and what as a base.
| resource | is | a module may |
|---|---|---|
| `directory` | a directory with a mode and an owner | ✅ |
| `file` | literal content, with `${bound:…}` and `${secret:…}` filled in | ✅ |
| `directory` | a directory with a mode and an owner, **placed by the mesh** under the node's root: `place: "."` is the assignment's own root, `place: "mesh"` the mesh's directory for the module, a pathless one sits beneath the root by its id; a stated path is the placement for data that must stay where it is, and may itself sit beneath a placed one (`${dir:<id>}/…`). Everything else names it as `${dir:<id>}` ([issue 119](../../04-ISSUES/119-a-module-definition-decides-where-its-files-live/00-report.md), [174](../../04-ISSUES/174-the-meshs-own-files-for-a-module-are-placed-by-the-definition/00-report.md)) | ✅ |
| `file` | literal content, with `${bound:…}`, `${secret:…}`, `${dir:…}`, `${port:…}`, `${machine:…}` and `${setting:…}` filled in — the last an operator's value from the assignment's settings, refused by name when unset ([ADR 0155](../../02-DECISIONS/0155-a-definition-names-no-installation-and-how-that-is-checked.md)) | ✅ |
| `user` | a login | ✅ |
| `access` | a pre-existing path it may use and must not own | ✅ |
| `access` | a pre-existing path it may use and must not own, **named by id** and placed by the assignment (`accesses: {<id>: <path>}` on its settings); mounts say `${access:<id>}`; a path in the definition is the default an assignment replaces, tolerated while the catalogue converts ([issue 153](../../04-ISSUES/153-an-adopted-machines-data-cannot-be-placed-where-it-is/00-report.md)) | ✅ |
| `archive` | files fetched by digest and unpacked | ✅ |
| `package` | a package that must be present | ✅ |
| `network` | a named container network | ✅ |
@@ -266,6 +268,31 @@ ships one and wrong for code the mesh built, which has no unit until the mesh wr
**Tools, hooks and consumers are not further modes**, which is the test of whether three is the
right number: they are loaded by a tool host, and a tool host is a process that stays up.
## A build says what it does, as it happens
*2026-10-01 — [ADR 0157](../../02-DECISIONS/0157-a-build-says-what-it-does-on-the-bus-as-it-happens.md).*
A build machine narrates every build on the bus as the role it holds: `started` when it takes the
work, one `log.<build id>` event per line — every command it runs with its duration, every step of
the recipe, and on failure the command's own output, line by line — and `built` for the outcome as
before. The same lines still go to the machine's standard error, so a build machine with nobody
listening is as readable as it was; a listener reads the same lines, live, from anywhere on the mesh.
One build is one subject. A reader follows it by subscribing that subject and nothing else, and the
events stream keeps it for a week, so `builds --log <id>` — on the command line and as the
controller's seat verb through the console — reads it back afterwards. `builds` lists every build's
id beside it, and `build` says the id it asked with. The mesh keeps no second copy: the stream is the
log. The console's `build` tool asks and answers at once with the id; the outcome is taken in — the
build recorded, the module registered with its source — by whoever hears it, the waiting command or
the daemon following the role's event, so a build nobody waited for still reaches the catalogue
([issue 176](../../04-ISSUES/176-the-consoles-build-tool-neither-waits-nor-registers/00-report.md)). A viewer of builds, when one is built, is a subscriber over these subjects and the outcome; the
builder needs nothing more for it.
*How it is checked:* the holder's grant is exactly `started`, `built` and `log.*` (broker test); a
build's lines reach a reader of its subject in order and the stream holds them afterwards (link test
against a real server); the seat verb with an id reads the log (controller test); and, live, a build
after the roll-out read line by line through the console.
## The builder compiles the languages the mesh is written in
*2026-09-29 —
+3 -2
View File
@@ -7,8 +7,9 @@ code:
- mesh-tools src/broker-amqp.ts (to be replaced)
- mesh-catalog modules/nats (to be written)
- mesh-sdk src (the protocol's NATS binding, step 3)
updated: 2026-09-27
updated: 2026-10-01
decisions:
- 02-DECISIONS/0157-a-build-says-what-it-does-on-the-bus-as-it-happens.md
- 02-DECISIONS/0106-the-bus-is-nats.md
- 02-DECISIONS/0131-everything-on-the-mesh-speaks-to-the-broker-seat.md
- 02-DECISIONS/0116-the-bus-is-built-in-five-steps.md
@@ -135,7 +136,7 @@ Core NATS is at-most-once. Everything the mesh must not lose lives in a JetStrea
|---|---|---|---|
| CONTROL | `mesh.control.>` except `alive` (a build's outcome moved to its seat, ADR 0121) | work queue, one consumer (the controller), explicit ack | the store-window guarantee ([ADR 0083](../../02-DECISIONS/0083-one-push-leaves-the-mesh-consistent.md)): the controller `nak`s with a delay while its store is away and the message is redelivered; nothing is dropped |
| NODES | `mesh.node.>` | last per subject | one declaration per node, always the newest |
| EVENTS | `mesh.mod.*.event.>` | limits (age, size), durable consumer per subscribing module | a subscriber that was down catches up; after `max-deliver` attempts the advisory feeds `mesh.events.dead` (its own small stream) |
| EVENTS | `mesh.mod.*.event.>` and `mesh.seat.*.event.>` | limits (age, size), durable consumer per subscribing module | a subscriber that was down catches up; after `max-deliver` attempts the advisory feeds `mesh.events.dead` (its own small stream). *2026-10-01:* a build's whole log is here too, as the build-machine seat's `log.<build id>` events ([ADR 0157](../../02-DECISIONS/0157-a-build-says-what-it-does-on-the-bus-as-it-happens.md)) — one subject per build, a week of retention, read back by `builds --log <id>` with a consumer that is gone when the reading is done |
Tool calls and heartbeats stay on core NATS: a lost heartbeat is the next heartbeat; a lost tool
call is a timeout the caller already handles.
+1 -1
View File
@@ -109,7 +109,7 @@ convention, which later seats departed from.
| `mesh-store` | — | mesh | — | the store the mesh's own records live in |
| `mesh-broker` | — | mesh | `mesh-bus` | the broker carrying the mesh's own bus |
| `mesh-vault` | — | mesh | `secret`, reserved | the vault |
| `mesh-artifact-store` | `the-artifact-store` | mesh | `artifact-store` | the artifact registry |
| `mesh-artifact-store` | `the-artifact-store` (renamed 2026-09-30, [ADR 0156](../../02-DECISIONS/0156-an-artifact-is-what-a-build-produces-and-the-store-is-named-for-its-scope.md)) | mesh | `artifact-store` | the artifact registry |
| `mesh-catalog` | `the-catalogue` | mesh | — | the catalogue |
| `mesh-npm-package-registry` | `npm-package-registry` | mesh | `npm-package-registry` | the forge |
| `mesh-git` | `git` | mesh | `git` | the forge |
@@ -1,10 +1,11 @@
---
layer: to-be
status: proposed
code: []
updated: 2026-09-26
status: in-progress
code: [mesh-controller internal/catalogue]
updated: 2026-09-30
decisions:
- 02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md
- 02-DECISIONS/0155-a-definition-names-no-installation-and-how-that-is-checked.md
- 02-DECISIONS/0115-one-assignment-of-a-module-per-node.md
- 02-DECISIONS/0113-the-vault-makes-every-secret.md
- 02-DECISIONS/0114-a-shared-credential-rotates-over-two-credentials.md
@@ -136,6 +137,12 @@ lost is a named volume, not a directory ([ADR 0030](../../02-DECISIONS/0030-data
the assignment says nothing;
- **a placement**, where the assignment puts one directory elsewhere: on a second disk, or where an
adopted machine's data already is ([ADR 0100](../../02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md)).
*Built 2026-10-01 ([issue 153](../../04-ISSUES/153-an-adopted-machines-data-cannot-be-placed-where-it-is/00-report.md)):*
`places` on the assignment's settings, by directory id, with an owner where the data already has
one; and `accesses`, by access id, for the operator's data — an access has an id and its mounts
name it as `${access:<id>}`. Both validated as `endpoints` is: an id the definition does not
declare is refused. *How it is checked:* the controller's placement tests, and the
path-preservation proof extended to accesses.
**An operator's shared data** is an access, as before ([ADR 0051](../../02-DECISIONS/0051-shared-data-is-the-operators.md)):
never created, owned or removed by the mesh. The module requires read or read-write access. Where
@@ -171,6 +178,34 @@ make a new external key, so rotating one means an operator handing over a new va
assignment, and the route provider answers. A public name already held by another assignment is
refused, like any other singular thing.
**Built so far, 2026-09-30** ([ADR 0155](../../02-DECISIONS/0155-a-definition-names-no-installation-and-how-that-is-checked.md)):
the operator's value in its first form — `${setting:<key>}` in a file's content, from the assignment's
settings layers, refused by name when nothing set it; a module told the name its route composes
(`${bound:<route>:name}`); a build context on the git seat; and the check that no definition names an
installation, with `names-on-purpose` for the names a definition means. The host's directory in its
first form is [`${dir:<id>}`](../../04-ISSUES/119-a-module-definition-decides-where-its-files-live/00-report.md),
a placed directory under the node's root. Each is this design's provider in the shape the existing
placeholders have, not yet the one requirement form below; they are phase 1's first cases.
*Phase 3, in part (2026-09-30):* every definition's **own** data directory is placed; the conversion
moved no data, proven by resolving both catalogues with the controller's rule and comparing
([issue 119](../../04-ISSUES/119-a-module-definition-decides-where-its-files-live/00-report.md)).
What the mesh writes *for* a module was still placed by the definition
([issue 174](../../04-ISSUES/174-the-meshs-own-files-for-a-module-are-placed-by-the-definition/00-report.md)),
the gap this design answered on 2026-09-26; built later the same day: a directory saying
`place: "mesh"` is `<root>/mesh/<module>`, a directory beneath a placed one states its path as
`${dir:<id>}/<rest>` and moves with it, and the same proof — both catalogues resolved and compared —
shows forty-eight definitions naming the paths they named before.
*The operator's value travels only where it is asked for (2026-09-30,
[issue 173](../../04-ISSUES/173-a-modules-settings-reach-every-fact-it-contributes/00-report.md)):*
a setting overrides a key a contribution or a served fact declares and adds none; a file keeps taking
any key. A provider that must tell its consumers an operator's value — a mail server's domain, an
identity provider's issuer — declares it in what it serves as `${setting:<key>}`, and it is refused by
name when nothing sets it. That is the contract half of this design's operator provider in the shape
the placeholder allows: the definition says which values reach which requirement, and nothing else
does. *How it is checked:* the unit tests named in issue 173, and the plan comparison that closed it.
## How a definition reads what was resolved
**One form, naming a requirement and a field of its contract.** A definition that needs the database's
@@ -379,7 +414,9 @@ writes (`/var/lib/mesh/<module>`: sealed credentials, composed bindings) need a
module-visible reservation. They do not: a module *requires* a `host-path` and receives a
location; what the mesh writes for the module is the mesh's plumbing, placed where the mesh
chooses and mounted in — never part of the module's contract. One reservation per
requirement, `<root>/<module>/<name>`.
requirement, `<root>/<module>/<name>`. *(Built 2026-09-30: the mesh's directory for a module is
`<root>/mesh/<module>`, named in the definition as a placed directory and nowhere as a path —
issue 174.)*
**Resolution happens in the controller, at declaration composition.** The node receives
concrete paths exactly as today — the wire format and the host's apply do not change for
@@ -1,6 +1,6 @@
---
layer: to-be
status: in-progress
status: implemented
code: [mesh-controller, mesh-tools]
updated: 2026-09-30
decisions:
@@ -139,6 +139,24 @@ verb answers every seat's tools from the records, because the console cannot rea
console lists a role's tools beside the modules' own and resolves `<seat>.<verb>` to the seat when the
seat declares that verb. Which verbs each *other* seat serves stays a decision per seat, still untaken.
## What shipped, 2026-09-30
mesh-controller PRs 166 and 167, mesh-tools PR 21. Verified on the live mesh the same evening: the
console on a workstation lists the twelve verbs as `mesh-controller.<verb>` beside 67 module tools, and
`mesh-controller.nodes` and `mesh-controller.status` answer through it with what the commands print.
Two things shipped bent. **The grant arrived after the holder started**: the controller composes the
bus's user list and a push delivers it, so the first controller to serve its seat subscribed before
the broker's list named the grant, the server refused all twelve subscriptions, and the client never
retried — a holder now rebinds a refused subscription every thirty seconds (PR 167), and a controller
roll-out that adds a grant is followed by a push to the broker node. **A JSON verb's answer was parsed
from both output streams**, so `status --json`'s warnings hid the document as data; the answer is now
parsed from standard output alone (mesh-controller PR 168, pending). The `output` field carried it
either way.
What stays as designed and not built: which verbs any *other* seat serves, and §3 for module-declared
seats' schemas beyond the names their manifests already list.
## What this does not settle
- Which verbs each seat should serve. That is a decision per seat, and the reason to do it slowly: a
+15 -1
View File
@@ -1,6 +1,6 @@
---
layer: to-be
status: in-progress
status: implemented
code: [mesh-catalog modules/records]
updated: 2026-09-30
decisions:
@@ -71,6 +71,20 @@ harmless. The mesh session of design 15, when it exists, calls this rather than
| a sync against an unreachable origin leaves the checkout standing and says why | silence and success never look alike |
| live: through the console, `records_search` for a phrase that appears only in a design document here returns it | issue 006's closing check |
## What shipped, 2026-09-30
mesh-catalog PR 183, then PR 185. Verified on the live mesh the same evening: `records` assigned to the
control node with `{"repository": …}` as its setting, its checkout at the repository's `main` with 478
documents, its five tools listed by the console beside every other tool, and — ADR 0025's check —
`records_search` for a phrase from this document's title returned it from where it is written, with
the commit. The first live search missed: the phrase chosen from ADR 0025 straddled a line break under
emphasis, and the reader matched single lines. PR 185 matches a line together with the next and
ignores emphasis marks, which the module's test now covers; until it rolls, a phrase that wraps is one
to shorten.
The reader's `consumes` names the forge module's event rather than the `git` seat, because the seat
declares none; a merge into the repository was seen and pulled within seconds.
## What this does not settle
- Ranking or meaning. A search that understands a question is the session's job, not the reader's.
@@ -1,9 +1,9 @@
---
status: located
status: resolved
opened: 2026-08-23
located-in: [mesh-catalog modules/records, mesh-catalog modules/mesh-console]
fixed-by:
amended-design: 02-DECISIONS/0025-the-design-record-is-read-not-copied.md
fixed-by: ADR 0153; mesh-catalog PR 183 (records), PR 185 (a phrase that wraps)
amended-design: 03-DESIGN/01-to-be/35-reading-the-record.md
---
# 006 — This repository is not indexed into the knowledge base, and the claim that it is holds up a decision
@@ -193,3 +193,16 @@ forge and answering `records_search`, `records_read`, `records_list`, `records_s
[35 — Reading the record](../../03-DESIGN/01-to-be/35-reading-the-record.md). The module's test runs
0025's check against a repository it makes; this record closes when the same check passes through the
console on the live mesh, and says so below.
## Resolved, 2026-09-30
The check ADR 0025 names passed on the live mesh: through the console on a workstation,
`records_search` for a phrase that appears in one design document here returned that document and the
commit it was read at, from a checkout the mesh keeps and nobody copied. What this record asked on
2026-08-23 — *does the searcher find it without already suspecting it exists?* — is answered by where
the tool sits: in the same list as the forge's and the mesh's own, described as the thing to search
before forming a hypothesis. Reachable became surfacing when the surface became a list.
Open beside it, and not this record's: the mesh has no symptom-indexed memory at all since the
cut-over ([as-is 07](../../03-DESIGN/00-as-is/07-knowledge.md) says so), and the lessons of these
days are in this repository by hand.
@@ -1,9 +1,9 @@
---
status: located
status: resolved
opened: 2026-09-25
located-in: [mesh-catalog modules, mesh-controller internal/catalogue]
fixed-by:
amended-design:
fixed-by: mesh-catalog PR 193 (the conversion), mesh-controller PR 170 (TestPlacedDirectoriesKeepTheirPaths, which proves it moved nothing); the placed-directory mechanism itself predates this in mesh-controller internal/catalogue/dir_into.go
amended-design: 03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md
---
# 119 — A module definition decides where its files live on the machine
@@ -133,3 +133,30 @@ not by any check.
- What identifies an assignment, if a module may be assigned to one node more than once?
- What would the contributions file carry instead of host paths, so a provider needs no
identical-path mount?
## Resolved, 2026-09-30 — the module's half; the mesh's half is issue 174
**A definition no longer decides where its own data lives.** Twenty-eight definitions that named their
data directories now place them: the module's root as `place: "."`, a sub-directory by its id, and every
host-side reference — bindings, secrets, own secrets, grants, receives, file paths, mounts, env-files —
as `${dir:<id>}`. Twenty-eight others had already been written that way. Five directories whose id is
not their last segment keep their path as a placement, which is the exception the design allows and
the reason nothing else has to move for them.
**Nothing moved, and a test says so.** The controller's `TestPlacedDirectoriesKeepTheirPaths` takes the
catalogue before and after, resolves every converted definition on the default root with the
controller's own rule, and compares it whole with the definition before it: identical for all
twenty-eight. So the retirement this record said was a data migration turned out not to be one, on
one condition — a node's default root is where the data already is, and every node's is — and the
machines see no change. A node that sets another root is the case this does not cover, and it does
not exist.
**What remains is not this record's.** The 232 host paths still in the catalogue are where the mesh
writes what it makes for a module, under `/var/lib/mesh/<module>`; design 27 says the mesh places
those itself, and it does not yet. That is [issue 174](../174-the-meshs-own-files-for-a-module-are-placed-by-the-definition/00-report.md). *Placed since later the same day: `place: "mesh"` — issue 174 is resolved.*
The defects this record listed under *where that has already gone wrong* are unchanged by this and
stay in 174's scope where they concern the mesh's files; the operator's shared data stays an access
([ADR 0051](../../02-DECISIONS/0051-shared-data-is-the-operators.md)).
The manifest change lands with the catalogue's next merge; the rollout is a rebuild that changes no
machine, checked by comparing each machine's plan before and after.
@@ -1,9 +1,9 @@
---
status: located
status: resolved
opened: 2026-09-26
located-in: [mesh-controller internal/catalogue/declaration.go, mesh-catalog modules/keycloak, mesh-catalog modules/minio, mesh-catalog modules/nextcloud]
fixed-by:
amended-design:
fixed-by: mesh-controller PR 149 (a module is told the name it is served under), PR 169 (an operator's value, a context on the seat); mesh-catalog PR 188; ADR 0155
amended-design: 03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md
---
# 122 — A module cannot ask for its own public name, so three manifests wrote this mesh's names into the catalogue
@@ -114,3 +114,27 @@ particular to one installation, and also has nowhere to live but the definition.
ADR 0112 would put it), and if so what reads it — the provisioner, or the module's own values?
- What check would notice the next one? A definition naming a public domain is detectable in the
shape of the value, which is more than nothing, and less than a rule.
## Resolved, 2026-09-30
The open questions, answered in order. **A module names what it will be reached at** through the
binding of the route it contributes: `${bound:route:name}`, or `:name-<local>` for several
contributions, is the composed public name; `:internal-name` the private one (controller PR 149). The
identity provider, the object store's console and the automation tool's webhook now read it there.
**The scheme is the module's**, because it is true of the route the proxy terminates and every instance
wrote `https://` in front of the name. **An adopted resource's name is a setting** on the assignment;
so is every other operator's value — a mail domain, a site name, the address a proxy forwards from —
as `${setting:<key>}` in the file the software reads
([ADR 0155](../../02-DECISIONS/0155-a-definition-names-no-installation-and-how-that-is-checked.md)).
**The check that notices the next one** is the shape of the value, as this record guessed it would be,
and it is more than nothing: it found forty-two, and the catalogue passes it now
([issue 134](../134-a-definition-may-still-name-the-mesh/00-report.md)).
**What the rollout cost, 2026-09-30 evening.** The site module's rename from its domain to `website`
was a new module to the mesh, and two things the old assignment carried by name were lost: the
container still named the old network, and a port setting on the old assignment had hidden that
`listens` said one port while the container published another. The site answered 502 for about
twenty minutes across two one-line fixes (mesh-catalog PRs 190, 191). A module's rename is an
unassign and an assign, and everything the assignment held — settings, ports, its directory — is the
new module's to get again; the mesh says nothing about that today. The mail module's settings turned
out to reach every fact it contributes, which is [issue 173](../173-a-modules-settings-reach-every-fact-it-contributes/00-report.md).
@@ -1,9 +1,9 @@
---
status: located
status: resolved
opened: 2026-09-26
located-in: [hq 00-META/glossary.md, hq 02-DECISIONS/0075-two-stores-and-which-provides-what.md, mesh-controller internal/catalogue/seats.go, mesh-catalog modules/distribution]
fixed-by:
amended-design:
fixed-by: ADR 0156; mesh-controller migration 0048 (feat/the-artifact-store-seat-is-named-for-its-scope); mesh-catalog modules/distribution
amended-design: 03-DESIGN/01-to-be/26-the-seats.md
---
# 123 — The image registry is named after a role, and *artifact* is defined as one format
@@ -72,3 +72,15 @@ adopted, rather than on protocols.
exercise that leaves the code disagreeing?
- What check would keep the glossary honest — a definition tested against the kinds a definition may
actually declare, rather than restated by hand?
## Resolved, 2026-09-30
Read from what the store serves rather than from what it was called: both shapes of a kept reference
— an image and an archive's blob — go to the same registry by digest, which is exactly the provision
ADR 0075 defined. So the word was wrong and the seat's name was odd, and the provision was right.
[ADR 0156](../../02-DECISIONS/0156-an-artifact-is-what-a-build-produces-and-the-store-is-named-for-its-scope.md):
*artifact* means what a build produces, of any of the four kinds; the seat is `mesh-artifact-store`
with the old name as its alias (one migration, ADR 0122's mechanism); the provision keeps its name.
The two-implementations question stays as 0075 answered it, with the day to retire the second server
named. The mechanical check the report asked for is the alias test and the glossary naming the same
four kinds as design 18's table.
@@ -1,9 +1,9 @@
---
status: open
status: resolved
opened: 2026-09-28
located-in: [mesh-catalog, mesh-controller internal/catalogue]
fixed-by:
amended-design:
fixed-by: mesh-controller PR 169 (the check, module check, the catalogue-wide test); mesh-catalog PR 188 (the catalogue that passes it); ADR 0155
amended-design: 03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md
---
# 134 — A definition may still name the mesh, and the check that would say so does not exist
@@ -64,3 +64,17 @@ that.
hostnames above are the first real cases.
- Should a build context name a repository on the git seat rather than by URL, and if so, what does
that mean for a context in *another* mesh's forge?
## Resolved, 2026-09-30
The check exists: `InstallationProblems`, run by `module check` and by a catalogue-wide test. Run over
the 77 definitions it found 42 values, not 15 — the by-hand count had missed a second name one
character after the first on the same line, which is the kind of thing a check is for. The three open
questions: **a domain in a `why` string does not break the rule**, prose is not judged, and the eight
were rewritten anyway because this catalogue is public; **a service's public name is the name the mesh
composes for its route**, read through the route's binding, and an operator's own value is a setting;
**a build context names a repository on the git seat**, `seat: git` with the path, and a context in
another mesh's forge stays a URL, which the check reports and `names-on-purpose` would declare. The
seven values that remain are declared with their reason — four applications built outside the mesh —
and are the list that shrinks ([ADR 0155](../../02-DECISIONS/0155-a-definition-names-no-installation-and-how-that-is-checked.md)).
Registration does not refuse yet; it will when the list has been empty for a release. *2026-09-30, later the same day:* it refuses — the list was empty the day the check landed, and the operator asked for it (mesh-controller PR 175); `module add` and a build's result are refused in the check's words, with the way out, and the build stays recorded.
@@ -1,11 +1,11 @@
---
status: open
status: resolved
opened: 2026-09-29
located-in:
- mesh-controller internal/catalogue/dir_into.go (dirsFor: a stated path or <data root>/<module>/<id>, nothing else)
- mesh-controller (accesses: the path is the manifest's literal)
fixed-by:
amended-design:
fixed-by: mesh-controller PR 176 (`places` and `accesses` on an assignment, `${access:<id>}`, an owner the data already has); mesh-catalog PR 198 (ten definitions name their accesses by id)
amended-design: [03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md, 03-DESIGN/01-to-be/18-building-a-module.md]
---
# 153 — An adopted machine's data cannot be placed where it is
@@ -55,3 +55,25 @@ The two assignment halves 0112 decided: a setting that places a declared directo
path on this node, and a setting that says where an access's data is — both validated like
`endpoints` (unknown ids refused), and an access placed by the assignment still never created,
chowned or removed.
## Resolved, 2026-10-01
The two assignment halves ADR 0112 decided exist. On an assignment's settings, `places` puts a
declared directory (by id) at a path on this node, with an owner where the data already has one —
`{"config": "/where/it/is", "data": {"path": "…", "owner": "1001:2000"}}` — and `accesses` says where
the operator's data is, by the access's id. Both are validated the way `endpoints` is: an id the
definition does not declare is refused, naming what it does declare; a relative path and a
non-numeric owner are refused; an access nothing places and whose definition carries no path is
refused with the setting to write, rather than mounted as nothing. A placed directory is still the
mesh's — created, owned as said, removed when empty and undeclared. A placed access is still the
operator's — mounted, never created, owned or removed.
An access now has an **id**, and the definition's mounts name it as `${access:<id>}`, so a placement
moves the mount with it. Ten catalogue definitions were given ids; each keeps its path as the default
an assignment may replace, so the machine that said nothing received exactly the paths it had before
(the path-preservation proof, extended to accesses). That default is still a host path in a
definition, tolerated as the transition: the media modules on the control node hold it until their
assignments say where the data is, and then the defaults go.
What the home server's assignments say next is the operator's: per media module, `places` for the
configuration on the second disk and `accesses` for the pool, with the owner the predecessor ran as.
@@ -0,0 +1,72 @@
---
status: resolved
opened: 2026-09-30
located-in: [mesh-controller internal/catalogue/settings.go (settle), mesh-controller internal/catalogue/declaration.go (composed, ownNames)]
fixed-by: mesh-controller PR 175 (a setting overrides a declared key and adds none; `${setting:…}` in a served or contributed value); mesh-catalog PR 196 (mail declares its domain, the identity provider its issuer)
amended-design: 03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md
---
# 173 — A module's settings reach every fact it contributes, not only the file that asked
## What was observed
Setting the mail module's operator values — `domain`, `sitename`, `website`, `proxy-address` — so that
its environment file could read them as `${setting:…}`
([ADR 0155](../../02-DECISIONS/0155-a-definition-names-no-installation-and-how-that-is-checked.md)),
and then planning the control node, showed the four keys in places nothing asked for them:
- in every **route** the module contributes to the proxy, beside `label`, `endpoint` and `port`;
- in the **database** it contributes to the store's provider, beside the database's `name`;
- in the `smtp` facts every **consumer** of its mail provision is bound to.
Nothing broke: a provider ignores a key it does not read. But a proxy now receives a mail server's
`proxy-address` and `website` as if they were route facts, a consumer of mail is told the site's name,
and a reader of `plan` cannot tell which of a contribution's keys the module meant and which leaked in.
## Why this is here
Settings are one flat map per module, laid over every mergeable file, every contribution and every
served fact alike (`settle`). That was the right generality when a setting *was* a contribution's
override — a route's label is the example the code gives. It stops being right the day a setting is
an operator's value for one file, which ADR 0155 made ordinary. The design permits a value to travel
where nobody sent it, silently, and every consumer of a provision reads a map that grows with the
provider's unrelated settings.
[ADR 0112](../../02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md) and design 27
already say where this ends: a requirement has a contract, and a value goes to the requirement that
asked for it. Until that form exists, this is the cost of the placeholder being the first case of it.
## Open questions
- Should a key a file asks for with `${setting:<key>}` be withheld from contributions and served
facts, or should a contribution's overrides live under their own key (`contributes`, `serves`)?
The second is the shape design 27 draws; the first is the smaller change and keeps the leak from
widening while it is drawn.
- What does a consumer do with a served key it did not expect? Today: nothing, silently. A served
map is not checked against what the provision's contract says it carries, because there is no such
contract yet.
## Resolved, 2026-09-30
**A setting overrides a key a contribution or a served fact declares, and adds none.** A file keeps
taking any key, because a configuration file is where an operator adds things; a contribution and a
served fact are a contract the other side reads, and a setting made for one of the module's files is
no part of it. A key that lands nowhere — no mergeable file, no `${setting:…}` asking for it, no
contribution or served fact declaring it — is named as stray when the node is planned, rather than
dropped.
The first open question is answered the smaller way, and it turned out to be the right one: the two
keys consumers actually read through the leak — the mail provider's `domain`, the identity provider's
`issuer` — are now **declared** by the provider in what it serves, as the operator's value
(`${setting:domain}`, `${setting:issuer}`), filled from the same setting that used to leak and refused
by name when nothing sets it. So the contract says what travels, which is the shape design 27 draws,
without a second key for overrides. The second question stands: a consumer still checks nothing
against a contract, because there is none yet; what it is told is now only what the provider
declared.
*How it was checked:* the plans of all four nodes, under the running controller and the one with the
rule, compared resource by resource — every key that disappears from a contribution is a leaked file
setting or one of the mesh's own words (`expose`, `endpoints`), and nothing a provider reads goes
away; the two served keys were declared before the controller rolled. Unit tests: a setting a route
never declared does not reach the proxy; a served value nothing sets is refused by name; the setting
a served fact asks for is not stray.
@@ -0,0 +1,66 @@
---
status: resolved
opened: 2026-09-30
located-in: [mesh-controller internal/catalogue/dir_into.go, mesh-controller internal/catalogue/declaration.go, mesh-catalog modules]
fixed-by: mesh-controller PR 175 (`place: "mesh"`, a directory beneath a placed one, the proof test resolving both sides); mesh-catalog PR 197 (48 definitions converted)
amended-design: [03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md, 03-DESIGN/01-to-be/18-building-a-module.md]
---
# 174 — The mesh's own files for a module are placed by the definition, not by the mesh
## What was observed
After every module's *own* data directory was placed by the mesh
([issue 119](../119-a-module-definition-decides-where-its-files-live/00-report.md)), the catalogue
still carries **232 host paths in 50 definitions**, all of one kind: where the mesh writes what it
makes *for* the module — its sealed bus credential (`own-secrets.broker`), its merged config file, its
bindings — under `/var/lib/mesh/<module>/…`, and the directory resource that creates that subtree.
Not one of those files is the module's. The mesh mints the credential, composes the binding, merges
the config; the definition only says where to put them, and says it the same way seventy times.
## Why this is here
Design 27's answer to *what sits beneath a node's root* (2026-09-26) is that the mesh's writes need no
module-visible reservation: **what the mesh writes for a module is the mesh's plumbing, placed where
the mesh chooses and mounted in, never part of the module's contract.** The definition today names
that place, so a definition is not yet free of host paths — and a node whose root is elsewhere would
place the module's data there and the mesh's files still under `/var/lib/mesh`.
The path-preserving test that let issue 119 close does not cover this: it proves a *placed* directory
resolves to what was named, and these are not placed.
## What it would take
A word for "the mesh's file for this module", or none: `own-secrets` values, a merged config file and
a binding could be named by key alone, with the mesh choosing `<root>/mesh/<module>/<key>` and
mounting it where the container says. The container side of the mount already exists in every
definition (`/run/secrets/broker`, `/run/config/config.json`); only the host side would go. The
change is in the controller, once, and then a mechanical edit of fifty definitions, which the same
test that proved 119 can prove again with the rule extended.
## Open questions
- Does `own-secrets` keep its map shape with the value becoming the *container* path rather than the
host path, or does the mount stay where it is and the host side become a placeholder the mesh
fills, `${mesh:<key>}`?
- A binding file today lands wherever `binds` says; a module's code reads it from an environment
variable naming the container path. If the host side is the mesh's, is the container side still the
definition's to choose? It should be: it is the software's contract.
## Resolved, 2026-09-30
The word is the one issue 119 introduced, with a second place: a directory saying `place: "mesh"` is
the mesh's directory for the module, `<root>/mesh/<module>`, beside the assignment's own root and
under the same node setting. The mesh's files keep their map shape and the container side of every
mount stays the definition's — only the host side changed, to `${dir:mesh-state}/…`. A directory that
sat beneath the mesh's (a forge's runtime state, a manager's output) states its path as
`${dir:mesh-state}/<rest>` and moves with it; one that sits elsewhere by adoption (the registry's data)
keeps its literal path as the exception it is.
Forty-eight catalogue definitions and the controller's own manifest were converted mechanically. The
proof is the same test that let issue 119 close, now resolving *both* checkouts before comparing,
because the earlier manifest already placed its own directories: resolved on the default root, every
converted definition names exactly the paths it named before. Nothing moved.
Both open questions are answered by keeping what exists: the map stays, the host side is placed; the
container side is the software's contract and stays where the definition says.
@@ -0,0 +1,62 @@
---
status: resolved
opened: 2026-09-30
located-in: [mesh-controller internal/broker/streams.go (the controller's EVENTS consumer), mesh-controller internal/link/receive_nats.go]
fixed-by: mesh-controller PR 173 (MaxAckPending 1 on the controller's events consumer); the packaging module's rebuild record and the status count remain open in the text below
amended-design:
---
# 175 — An announcement queued behind a long build comes back, and the build runs again
## What was observed
On the evening of 2026-09-30 five merges landed within minutes. The controller's log then showed the
same four announcements — two into this repository, one into the catalogue, one into the controller's
own — arriving again every couple of minutes, and each arrival of the controller's rebuilt the two
modules that package its source. The builder built `builder` and `route-proxy` five times over for one
merge, the control node was pushed after each, and every other message the controller handles waited
behind the builds. It looked like a slow mesh; it was a loop.
Earlier the same evening, at a lower rate, the log already carried duplicated lines — a merge seen
twice, a module "moved" twice — that nobody read as a symptom.
## Why
The controller acts on what it consumes in **one loop, one message at a time**, and a merge's handler
builds every module the merge changed before it returns — minutes of work. The bus's acknowledgement
window is thirty seconds. That contradiction was met once already: the message being worked on is kept
alive by a heartbeat while its handler runs
([issue 127](../127-a-module-event-derives-a-subject-nothing-publishes/00-report.md)'s stretch,
controller PR 122). **The heartbeat covers one message.** The consumer is a push consumer with no
bound on what it may have outstanding, so the client is handed everything that is waiting at once; the
messages queued behind the one being built time out unacknowledged, come back after thirty seconds,
and are handled again when the loop gets to them — including the merge whose builds are already done,
which builds them again. A module that only *packages* another repository's source has no record of
which commit it was last rebuilt for, so nothing says "already done".
## Why it matters beyond this instance
The design permits work to be done twice, silently, and the doubling scales with how busy the mesh
is: the busier the builder, the longer the queue, the more that comes back. A push to a machine is
idempotent and a rebuild produces the same digest, so nothing broke — but every merge cost several
builds, the control node was pushed after each, and a person watching saw a mesh that would not
settle. It was the redelivery storm of 2026-09-28 in a narrower form, one layer out.
## What would have prevented it
- **A consumer that is handled one at a time is delivered one at a time.** `MaxAckPending: 1` on the
controller's events consumer: the server holds the rest, nothing times out behind a build, and the
heartbeat that keeps one message alive is then keeping *the* message alive.
- **A packaging module records the commit it was last rebuilt for**, so a replayed announcement is
"already built from it", the answer the source-built modules already give.
- **A log line that appears twice with the same commit is a symptom**, and the check is cheap: the
same announcement acted on twice within its window is a count worth exposing in `status`.
## Resolved on the first remedy, 2026-09-30
`MaxAckPending: 1` on the controller's events consumer (mesh-controller PR 173): the server hands the
controller one announcement at a time and holds the rest, so nothing times out behind a build. The
existing consumer is brought to that configuration by the assertion the controller makes at start.
The second and third remedies — a packaging module recording the commit it was last rebuilt for, and
a doubled announcement counted in `status` — are not built; they would make the same fault visible
and cheaper should the first ever be undone, and they are left here as what to reach for then.
@@ -0,0 +1,74 @@
---
status: resolved
opened: 2026-10-01
located-in: [mesh-controller cmd/mesh-controller/seatverbs.go (argvFor, "build": `--wait 0`, no `--self`), mesh-controller cmd/mesh-controller/main.go (builds.Built records a build and registers nothing)]
fixed-by: mesh-controller PR 179 (one take-in for a build's outcome, called by the waiting command and by the daemon; `--wait 0` asks and returns the id; the seat verb says `--self` for a forge path); ADR 0157 (mesh-controller PR 178) gave the tool something to hear
amended-design: 03-DESIGN/01-to-be/18-building-a-module.md
---
# 176 — The console's `build` tool neither waits nor registers, and does not take a forge path
## What was observed
Eight media modules held by the mesh had not been rebuilt after their manifests changed, because a
merge rebuilds only the modules whose recorded source is the merged repository and theirs was another
one. Asked through the console — the controller's `build` tool, served on its seat
([ADR 0154](../../02-DECISIONS/0154-the-meshs-own-verbs-are-the-controller-seats-tools.md)) —
with the repository given as its path on the forge, the way the tool's own description invites:
- every call answered at once with *no build machine answered within 0s … the work is queued*;
- the builder, asked in the same breath, failed each one with *cannot clone novox/mesh-catalog*: the
path was handed to `git clone` as written, because the tool never says the repository is a path on
the forge holding the git seat (`--self`), which the command line requires for that form;
- given the repository's URL instead, the call still answered within zero seconds, the build ran on
the builder, its result was heard and recorded — and the module was **not registered**: what hears a
finished build records the build and stops; only the caller that waited would have parsed the
manifest and registered it, and the caller had gone.
So the console can start a build and never learn its outcome, and a build it starts cannot change
what the mesh holds. The tool's description — *have the build machine build a repository and record
what came out* — is true of the build record and false of the module.
## Why this is here
The seat verb was written as fire-and-forget, deliberately (`--wait 0`), so that a tool call over the
bus does not sit for the minutes a build takes. That reasoning moved the wait but not the work that
followed it: registration lives in the waiting caller, not in the path that hears the result. The two
halves of "build" — asking, and taking in what came back — are split across the command and the
event handler, and the tool reaches only the first.
The forge-path form is a second, smaller gap: the seat verb maps three arguments and forgets the flag
the same command needs to read one of them.
## What would be right
Registration belongs where the result is heard, once, so a build's outcome reaches the mesh whoever
asked and whether or not they waited — the same rule as an announcement's builds. The tool then
answers with what it can say at once (asked, queued, or refused) and `builds` says the rest. A
repository given without a scheme is a path on the git seat, and the verb says so.
## Open questions
- Should a tool call be able to wait at all? A builder answers in minutes; the console's transport
holds a call for a bounded time. If not, the tool needs a way to follow one build — which is the
builder's missing progress (no tools, no events) named in the console's review of 2026-10-01.
## Resolved, 2026-10-01
Registration moved to where the outcome is heard. One function takes a build's outcome in — records
the build, parses the manifest, refuses a definition that names an installation, registers the module
with its source as the seat and path the request carried and the outcome echoes — and both the
command that waited and the daemon that follows the role's `built` event call it. So a build asked
for by anything that could not wait reaches the catalogue the same as one asked for by hand, and the
same outcome heard twice writes one row twice with the same values.
The tool keeps not waiting, and says so: `build --wait 0` publishes the work and answers with the
build's id, and `builds --log <id>` follows the build line by line ([ADR 0157](../../02-DECISIONS/0157-a-build-says-what-it-does-on-the-bus-as-it-happens.md)),
which is what a tool call over the bus can do in the seconds it has. A repository given without a
scheme is said to be a path on the git seat, so the forge-path form the tool's description invites
now works.
The open question is answered by the shape: a tool call does not wait; it asks, gets the id, and
follows. *How it is checked:* the shared take-in against a raised store — registered with the seat
source, a definition naming an installation recorded and refused, a failure said in the builder's
words — and the tool's mapping of a forge path against a URL.
@@ -0,0 +1,51 @@
---
status: resolved
opened: 2026-10-01
located-in: [mesh-controller cmd/mesh-controller/sendable_test.go (the converged-declaration guard), mesh-controller cmd/mesh-controller/adopting_test.go (the adopted-anchor fixture), the build of the controller (runs no check)]
fixed-by: mesh-controller PR 180 (the two tests, for what they missed); the process half is open below
amended-design:
---
# 177 — The controller's check is run by nobody, and two of its tests failed for days unseen
## What was observed
`make check` on the controller's main failed two store-backed tests on 2026-10-01, both for
reasons older than that day:
- the guard that holds a converged declaration byte for byte to what an older host was sent still
expected a `hosts` list on every container, after the change of 2026-09-30 that took it off — a
machine's own resolver knows the mesh's names now ([issue 171](../171-a-modules-own-resolver-knows-no-mesh-name/00-report.md));
- the adopted machine in the converge test reported no outward link, after
[ADR 0140](../../02-DECISIONS/0140-the-filter-constrains-what-arrives-from-outside.md) (2026-09-28)
made a filter depend on one.
Neither commit touched the test it broke, and neither merge failed: the mesh builds the controller
from its repository and runs none of its tests. The tests that need a store — the ones that say what
a machine is actually sent — are exactly the ones a quick `go test ./...` skips, so a person running
the fast check sees green too. Every merge tonight, this one included, was checked that way.
## Why this is here
A guard that is not run is a comment. The byte-for-byte guard exists because an older host parses a
declaration strictly and a field it does not know is a machine that applies nothing
([ADR 0100](../../02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md)); it went
red on a change that happened to be safe — a field removed — and would have gone red the same way on
one that was not. The mesh has a rule that a test defends a decision
([ADR 0017](../../02-DECISIONS/0017-a-test-defends-a-decision.md)) and no rule that says when the
test is run.
## Resolved, 2026-10-01 — the tests
The guard is re-captured with the change named in its own comment: a field an older host never sees
is the one change the guard permits, a field it would refuse is the one it exists to catch. The
fixture reports an outward link as a real host does. `make check` is fully green on main again
(mesh-controller PR 180). No code changed.
## Open — the process
The build should run the check, or something should, before a merge lands. What that is — the
builder raising the store the tests need, a check the forge runs on a pull request, or the controller
refusing to record a build whose repository's own check fails — is a decision not taken here. Until
it is, `make check` before a controller merge is the operator's habit, written into the work order,
and this issue stays the record of why.
@@ -0,0 +1,59 @@
---
status: resolved
opened: 2026-10-01
located-in: [mesh-controller cmd/mesh-controller/plan.go (routeNamesInTheMesh), mesh-controller internal/catalogue (NamesServed)]
fixed-by: mesh-controller PR 181 (every node's resolution read first, names attributed across them to the terminus, in order; the many shape of contributions counted)
amended-design:
---
# 178 — A routed name resolves to a provider that was merely told it, and flips between plans
## What was observed
From the home server, the dashboard's public name resolved inside the mesh to the control node, where
no proxy serves it, and TLS failed; from outside it resolved to the home server and worked. Filed
first in the forge's tracker on the hq repository (its issue 227), on 2026-09-30. The same night,
two plans of the same machine taken a minute apart differed in exactly one resource — the mesh's
names region — with the dashboard's name on one node's address and then on the other's.
A second thing hid behind it: no module that is routed under several names — the photo service's
six, the invoicing service's two, the mail server's five — had any of them in the names region at
all.
## Why this is here
The names region is composed from every contribution the mesh gave a name to. A consumer that is
routed contributes its label to its route, and the proxy serves the composed name. A consumer that
also uses an identity provider contributes the same label there, because the provider must know the
consumer's public name to compose a redirect — and by the rule that two readers must agree
([issue 122](../122-a-module-cannot-ask-for-its-own-public-name/00-report.md)) it is
given the same composed name. So one name reaches two providers, and the code attributed it to the
node of whichever contribution a map yielded last. Map order is not stable between runs; the region
was not either.
The second fault is older: the single-value reading of a module's contributions is deliberately
empty when the module contributes several times to one requirement
([ADR 0094](../../02-DECISIONS/0094-a-module-may-hold-several-secrets-from-one-provider.md)'s
sibling), and the names region used that reading, so a module with several routes named none.
## Resolved, 2026-10-01
Every node's resolution is read first, and the names are attributed across them at once. **The
terminus serves the name**: among the providers a name reaches, the one that is not itself published
under a labelled name through another provider. An identity provider is routed through the proxy and
so is a consumer of names, not their end; the proxy contributes no label to anyone and is. The rule
knows nothing of what "route" means — it reads the graph the modules declared — and it walks nodes,
requirements and contributions in order, so one mesh yields one region. Every shape of contribution
is counted, so a module routed under several names has every one of them resolved.
*How it is checked:* the two-node mesh of the report, resolved and attributed twenty-five times — the
dashboard's name on the home server, the identity provider's own name on the control node, no name
leaked to the identity provider; a module with two routes yields two names; and, live, two plans of
the home server after the roll-out identical in the names region, with the dashboard's name at the
home server's address and the several-routed modules' names present.
## Note on where this was filed
The symptom was filed in the forge's issue tracker, which is not where hq's issues live: a tracker
issue carries no status the mesh's records read, and its numbers collide with hq's pull request
numbers in conversation. It is closed there pointing here.