The tidy version said a module provides names and requires names and that is the only edge. Working postgres through completely disproves it. A small game wanting to store data does not require postgres to EXIST. It requires postgres to MAKE IT A DATABASE and hand back credentials. Those are different relations in every way that matters: one creates something per consumer, carries a payload back, can be revoked, and leaves the provider holding state about who was granted what. The other creates nothing. So: two kinds of edge, one graph. Instantiation implies presence; presence does not imply instantiation. The current system already had exactly this split — `dependencies` for presence, `requires: provision:` for instantiation, with the resolver deriving one from the other. analysis.md called that derivation a convenience. It is not: it is the correct relationship between two genuinely different relations, and the design had collapsed them. Postgres also turns out to be nine things, not one. A container. Persistent state where moving nodes is a migration rather than a reschedule. Configuration partly derived from the machine's hardware. A tool surface. A provisioner. Its own bookkeeping about what it granted, which is not the data it stores. An exposure decision per node it runs on. Credentials it generates, which means a provisioning edge carries a secret. And health that is not "the container is up". Four questions the worked example makes concrete rather than abstract. WHICH postgres, when there are two — a consumer of `terminal` does not care and a consumer of a database cares permanently. How many instances a module should have, which cannot be a global rule because one-per-mesh is wrong for a store a disconnected node needs and one-per-node is wrong for the mesh's own registry. What happens to a grant when its consumer is removed, where dropping is data loss and keeping is a leak. And whether a declaration is composed PER NODE from what that node reported — because tuning follows hardware the control plane cannot know, and the alternative is the host deciding, which ADR 0037 forbids.
168 lines
8.7 KiB
Markdown
168 lines
8.7 KiB
Markdown
# One kind of edge
|
|
|
|
> **Superseded in part by [`worked-postgres.md`](worked-postgres.md).** Working postgres through
|
|
> completely shows there are **two** kinds of edge, not one: *presence* — the thing exists and is
|
|
> reachable, nothing created — and *instantiation* — the provider is asked to make something for
|
|
> this consumer and hands back credentials. Instantiation implies presence; presence does not
|
|
> imply instantiation. Everything else below stands; the claim in the title does not.
|
|
|
|
A design, not an account of what exists. [`analysis.md`](analysis.md) measured the current
|
|
catalogue and its value here is two lessons rather than its machinery: a field that means
|
|
*depends on* should say so, and placement does not belong in a manifest.
|
|
|
|
## The idea
|
|
|
|
**A module provides names. A module requires names. That is the only edge.**
|
|
|
|
Everything the effort listed as separate declarations turns out to be one relation with
|
|
different kinds of name on either end.
|
|
|
|
```
|
|
postgres provides postgres
|
|
kitty provides kitty, terminal
|
|
xterm provides xterm, terminal
|
|
anthropic provides anthropic, ai-assistant
|
|
|
|
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: 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 | 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 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
|
|
|
|
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 `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
|
|
reason, and through the same code, that it cannot be installed without its libraries.
|
|
|
|
And it means the host's capability detection — which already reports what a machine can be
|
|
asked to do, with a reason for each verdict — **is the node's provides-list**. It was built to
|
|
be read by a person; it turns out to be an input to resolution.
|
|
|
|
## What a module declares
|
|
|
|
```yaml
|
|
name: meshboard
|
|
|
|
provides: [meshboard]
|
|
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
|
|
- container: ...
|
|
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 `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
|
|
deciding it in the manifest — a module pinning its database to a named node, so that a second
|
|
node cannot provide it without editing the module that consumes it. That is the mistake this
|
|
separation exists to avoid.
|
|
|
|
## What it deletes
|
|
|
|
The question the effort opened with, answered for the design rather than for what exists.
|
|
|
|
- **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.
|
|
- **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.
|
|
|
|
## What it does not delete, and should not
|
|
|
|
**A resolver still has to exist**, and it will need version constraints, conflict handling and
|
|
a story for when two modules provide one name and nothing says which to use. That is the
|
|
long-solved and easily-botched part, and the design should say what it **delegates** to the
|
|
platform's package manager rather than reimplementing.
|
|
|
|
## Open
|
|
|
|
- **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.
|
|
- **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.
|