011: an abstract name needs providers that are actually substitutable

Two corrections from the operator, and the first improves the design rather than
narrowing it.

`database` is not an edge. The test it fails, and the test the proposal was
missing: can a consumer be switched from one provider to another WITHOUT
CHANGING? A module speaking Postgres does not speak MongoDB or SQL Server —
different wire protocol, dialect, driver — so a consumer declaring `requires:
database` and handed any of them breaks. The name promises what no provider can
deliver, and the resolver would report a requirement satisfied that is not.

`terminal` passes: anything that runs a command in a terminal works and the
consumer never learns which it got.

So the ADAPTER is what creates an interface. `ai-assistant` is legitimate exactly
because adapters normalise what is behind it. Without one there is no interface,
there is a category — and a category is a TAG. Tags describe, edges bind, and
keeping them apart is what stops the catalogue acquiring a second kind of
relationship that looks like a dependency and is not, which is what a folder
named after a domain already was.

And the domain module goes. A `networking` module gathering a firewall, a
resolver and a proxy under one name came from an older shape and does not fit —
there is no such thing to install. There is core infrastructure: concrete
modules named individually, not flavourable, with no grouping module standing in
front of them.

Fixed three places where the revision left the old rule standing, including an
example manifest still requiring `database` — the kind of contradiction that
would have been read as the design rather than as a leftover.
This commit is contained in:
2026-08-26 22:44:37 +02:00
parent e20a09ae80
commit f55ecc1a47
2 changed files with 76 additions and 26 deletions
@@ -19,7 +19,10 @@ what they offer, and what they exclude — and what that replaces.
**The design is in [`proposal.md`](proposal.md): one kind of edge.** A module provides names
and requires names, and that single relation absorbs requiring a module, requiring a resource,
and the interface-and-adapter idea — an interface is simply a name with more than one provider.
and the interface-and-adapter idea. An abstract name is legitimate **only where providers are
genuinely substitutable** — `terminal` passes, `database` does not, because a consumer speaking
Postgres does not speak MongoDB. The adapter is what creates an interface; without one there is
a **tag**, which describes and does not bind.
A **node provides names too**, which makes capability checking stop being a separate mechanism
and makes the host's own capability report an input to resolution rather than something a
person reads.
@@ -132,7 +135,8 @@ integration being wrong looks like from the outside.
|---|---|
| ~~What does the graph **delete**?~~ | **Answered for the design** in [`proposal.md`](proposal.md): the distinction between requiring a module and requiring a resource, the interface as a kind of thing, capability checking as a separate mechanism, domain grouping, and possibly tiers as a separate concept. *(For the existing system the answer was nothing, because it is already there — [`analysis.md`](analysis.md).)* |
| When two modules provide one name, who chooses? | The substance of the interface idea, and the proposal does not settle it: a node setting, a mesh setting, or an explicit pin. |
| Does an abstract name need a type? | A consumer of `database` needs credentials; a consumer of `terminal` needs a command to run. Whether that difference lives in the name, beside it, or in what the provider hands back is undecided. |
| What does a provider hand back? | A consumer requiring `postgres` needs credentials and an address; one requiring `terminal` needs a command. Same relation, different shape flowing across it. |
| ~~Where do domain modules fit?~~ | **Answered: they do not.** A `networking` module gathering a firewall, resolver and proxy under one name came from an older shape. There is no such thing to install — there is core infrastructure, a set of concrete modules named individually, not flavourable, with no grouping module in front of them. |
| Would the missing declarations be used? | Zero manifests declare exclusions or capabilities, which is equally consistent with *nobody needs them* and *nobody can express them*. Nothing measured separates those. |
| Should `provider:` become a real edge, or should the relationship be declared twice? | It names a module and means *depends on*. Reading it as an edge fixes the closure; the alternative is requiring the consumer to also list it under `dependencies:`, which is duplication a resolver already avoids elsewhere. |
| Should placement leave the catalogue? | A provision pins itself to a named node, in the manifest. Which node runs what is an inventory decision — tier 2 by the skeleton's own test — and having it in tier 4 means a second node cannot provide the mesh's database without editing the module that consumes it. |
+70 -24
View File
@@ -12,29 +12,61 @@ Everything the effort listed as separate declarations turns out to be one relati
different kinds of name on either end.
```
postgres provides postgres, database
lavinmq provides lavinmq, message-bus
postgres provides postgres
kitty provides kitty, terminal
xterm provides xterm, terminal
anthropic provides anthropic, ai-assistant
meshboard requires database, message-bus
meshboard requires postgres, lavinmq
vscode requires terminal, display-server
```
A name is either **concrete** — a module's own name, so `requires: postgres` means that module
and no other — or **abstract**, so `requires: database` means whatever provides it. The
difference is not in the schema; it is whether more than one thing offers the name.
and no other — or **abstract**, so `requires: terminal` means whatever provides it.
That single relation absorbs three things the effort had listed separately:
| Was going to be | Is |
|---|---|
| requires another module | requires a concrete name |
| requires a resource — a database, a bucket | requires an abstract name |
| an interface, with adapters providing it | an abstract name several modules provide |
| requires a resource | requires the module that provides it, by name |
| an interface, with adapters providing it | an abstract name, legitimate only where an adapter makes providers substitutable |
The interface stops being a kind of module. It is a name with more than one provider, and
nothing has to declare that it is one.
The interface stops being a kind of module. It is a name with more than one provider *and a
contract they all satisfy*, and nothing has to declare that it is one.
### An abstract name is only legitimate when providers are actually substitutable
The test, and it is a strict one: **can a consumer be switched from one provider to another
without changing?**
`terminal` passes. Anything that runs a command in a terminal works, and a consumer never
learns which one it got.
**`database` fails, and it is the example worth keeping.** A module connecting to Postgres does
not connect to MongoDB, or to SQL Server, or to MariaDB. Different wire protocol, different
dialect, different driver. A consumer that declared `requires: database` and was handed any of
them would break — so the name promises something no provider can deliver, and the resolver
would satisfy a requirement that is not satisfied.
That failure has a shape this repository already knows: something declared, accepted, and not
true. It is worse here than usual because the graph would report success.
**The adapter is what creates an interface.** `ai-assistant` is a legitimate abstract name
exactly when adapters exist to normalise the providers behind it. Without an adapter there is
no interface — there is a category, and a category is not an edge.
So `postgres` is required by name, and a module that could genuinely work with several stores
requires whichever it actually speaks to.
### Categories are catalogue metadata, not structure
*Database* is still a useful word — for finding things, for a person browsing what the mesh can
host, for grouping in an interface. It is a **tag**.
Tags describe. Edges bind. Keeping them apart is what stops the catalogue acquiring a second
kind of relationship that looks like a dependency and is not — which is what a folder named
after a domain already was.
## The node provides names too
@@ -42,7 +74,7 @@ The move that makes capabilities stop being a separate system.
A node's profile is a set of provided names. `display-server`. `container-runtime`.
`amd64`. A module requiring `display-server` is satisfied by **the node**, exactly as a module
requiring `database` is satisfied by another module.
requiring `postgres` is satisfied by another module.
So there is one resolution rather than two: *is this name provided by anything available here?*
A graphical application cannot be installed on a node with no display server for the same
@@ -58,18 +90,23 @@ be read by a person; it turns out to be an input to resolution.
name: meshboard
provides: [meshboard]
requires: [database, message-bus, container-runtime]
requires: [postgres, lavinmq, container-runtime] # by name: it speaks their protocols
excludes: []
tags: [observability] # for finding it, never for resolving it
runs: # what applying it means
runs: # what applying it means
- container: ...
constraints: # what must be true of a node that runs it
constraints: # what must be true of a node, never which node
- architecture: amd64
```
`postgres` and `lavinmq` are concrete because this module speaks their wire protocols and would
break against anything else. `container-runtime` is abstract and satisfied by the **node**.
**`excludes`** is the one genuinely new relation: naming something that cannot coexist with
this. It is not derivable from requires and provides, and without it two modules that both
provide `message-bus` look interchangeable when installing both would break the machine.
provide `terminal` — or two that both want port 443 — look independent right up until
installing the second breaks the first.
**`constraints` are not placement.** They say what must be true of a node, never which node.
Which node runs what is an inventory decision, and the measurement found the current catalogue
@@ -84,9 +121,15 @@ The question the effort opened with, answered for the design rather than for wha
- **The distinction between requiring a module and requiring a resource.** One relation.
- **The interface as a kind of thing.** A name with several providers.
- **Capability checking as a separate mechanism.** The node is a provider.
- **Domain grouping** ([ADR 0017](../../02-DECISIONS/0017-modules-outside-the-core-are-grouped-by-domain.md)).
Folders assert relationships; edges record them. A domain is then a *query* over the graph —
what changes together — rather than a directory somebody has to keep true.
- **The domain module** — a `networking` module that exists to gather a firewall, a resolver and
a proxy under one name. It came from an older shape and does not fit: there is no such thing
to install. There is **core infrastructure**, which is a set of concrete modules named
individually — a firewall, a store, a resolver — with no flavour and no grouping module
standing in front of them.
- **Domain grouping as structure** ([ADR 0017](../../02-DECISIONS/0017-modules-outside-the-core-are-grouped-by-domain.md)).
Folders assert relationships; edges record them. What grouping was for — finding things,
seeing what belongs together — is a **tag** and a *query* over the graph, neither of which
anybody has to keep true by hand.
- **Tiers as a separate concept**, possibly. A tier is a level in the graph, and levels are
computed. Whether the coarse boundary is still worth naming is below.
@@ -99,17 +142,20 @@ platform's package manager rather than reimplementing.
## Open
- **When two modules provide one name, who chooses?** The consumer names `database` and gets
something. Whether the choice is a node setting, a mesh setting, or an explicit pin is the
substance of the interface idea, and this proposal does not settle it.
- **When two modules provide one abstract name, who chooses?** A node with both `kitty` and
`xterm` satisfies `terminal` twice. Whether the choice is a node setting, a mesh setting, or
an explicit pin is the substance of the interface idea, and this proposal does not settle it.
It is a smaller question than it was: the strict substitutability test means the consumer
genuinely does not care which it gets, so the choice is about preference rather than
correctness.
- **Are tiers a view of the graph, or a boundary that survives it?** If tiers are levels, the
concept is derived and the word is a convenience. If the tier rule — *a tier may depend only
on tiers below it* — is meant to be enforceable, it is a constraint on the graph rather than
a description of it, and it has to be stated as one.
- **Does a name need a type?** `database` and `terminal` are both abstract names, and a consumer
of the first needs credentials while a consumer of the second needs a command to run. Whether
that difference lives in the name, in a type beside it, or in what the provider hands back is
undecided.
- **What does a provider hand back?** A consumer requiring `postgres` needs credentials and an
address; one requiring `terminal` needs a command. The requirement is satisfied by the same
relation in both cases, and what flows across it is not the same shape. Whether that lives in
the name, beside it, or in what the provider returns is undecided.
- **What does a node provide that is not a capability?** Its architecture is a provided name
under this scheme, and so is its operating system. That may be elegant or may be one
abstraction too far; nothing here tests it.