From f160b28a715fa465c0d2a7465d243fd8714b3585 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 26 Aug 2026 22:53:59 +0200 Subject: [PATCH] 011: postgres worked through, and "one kind of edge" was wrong MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../011-the-module-graph/00-overview.md | 7 ++ 01-RESEARCH/011-the-module-graph/proposal.md | 6 + .../011-the-module-graph/worked-postgres.md | 111 ++++++++++++++++++ 3 files changed, 124 insertions(+) create mode 100644 01-RESEARCH/011-the-module-graph/worked-postgres.md diff --git a/01-RESEARCH/011-the-module-graph/00-overview.md b/01-RESEARCH/011-the-module-graph/00-overview.md index aedff08..a4a4a3e 100644 --- a/01-RESEARCH/011-the-module-graph/00-overview.md +++ b/01-RESEARCH/011-the-module-graph/00-overview.md @@ -17,6 +17,13 @@ touches: Whether the catalogue's missing structure is a **graph** — modules declaring what they need, what they offer, and what they exclude — and what that replaces. +**[`worked-postgres.md`](worked-postgres.md) works one module through completely**, and breaks +the tidy version. A database is nine things, not one — and a small game asking the mesh for its +own database shows there are **two kinds of edge**: *presence*, where the thing must exist, and +*instantiation*, where a provider makes something for a consumer and hands back credentials. +Instantiation implies presence and not the reverse. The current system already had this split +and the design had collapsed it. + **[`features.md`](features.md) answers what happens to `feature`.** It is one word for four things spanning three tiers — artifacts built once per version, resources applied to a machine, actions run against something that is not this machine, and checks that are requirements in diff --git a/01-RESEARCH/011-the-module-graph/proposal.md b/01-RESEARCH/011-the-module-graph/proposal.md index c3eebcc..ee6e281 100644 --- a/01-RESEARCH/011-the-module-graph/proposal.md +++ b/01-RESEARCH/011-the-module-graph/proposal.md @@ -1,5 +1,11 @@ # 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. diff --git a/01-RESEARCH/011-the-module-graph/worked-postgres.md b/01-RESEARCH/011-the-module-graph/worked-postgres.md new file mode 100644 index 0000000..e79e6c2 --- /dev/null +++ b/01-RESEARCH/011-the-module-graph/worked-postgres.md @@ -0,0 +1,111 @@ +# Postgres, all the way through + +One module, worked out completely, because it is the case that breaks the tidy version. It +looks like *a container that runs a database* and it is at least nine things. + +## What it carries + +**1 — A supervised container.** An image, a version, a data volume, and configuration. Easy, +and the only part the phrase "a docker service" describes. + +**2 — Persistent state, and where it lives matters.** The volume is the database. Moving this +module between nodes is not rescheduling; it is a migration. Almost nothing else in the +catalogue has this property, and nothing in `provides` / `requires` expresses it. + +**3 — Configuration that is partly the machine's.** Tuning follows the hardware — memory, +storage. A declaration generated centrally cannot know those, and +[research 012](../012-the-minimum-viable-node/00-overview.md) says the machine's own values win +on conflict. So some of this module's configuration is *derived from the node it lands on*. + +**4 — A tool surface.** It exposes capabilities agents can call — query, list, provision. That +is not a resource on a machine and not an artifact; it is a contract the mesh publishes on the +module's behalf. + +**5 — A provisioner.** The part that matters, and the one below. + +**6 — Its own bookkeeping.** The provisioner must remember what it granted to whom, or it +cannot revoke, rotate or clean up. So a module that provides state to others *also* holds state +about its providing — and that state is not the database's data. + +**7 — An exposure decision, per node it runs on.** Reachable from the machine only, from the +local network, or publicly. That is a property of *this assignment*, not of the module: the +same module on two nodes may answer differently. + +**8 — Credentials it generates.** Per consumer, and they have to reach the consumer. Which +means a provisioning edge carries a payload, and the payload is a secret. + +**9 — Health that is not "the container is up".** A container running and a database accepting +connections are different facts, and the second is the one anything cares about. This is the +host's read-back rule, at a distance. + +## The provisioner is a second kind of edge + +The tidy version of this effort said: *a module provides names, a module requires names, that is +the only edge.* A small game wanting to store data shows it is not. + +``` +my-cool-game requires postgres # I speak its protocol, it must exist +my-cool-game requires a database FROM postgres, called my-cool-game +``` + +The first is **presence**: the thing exists and is reachable. Nothing is created; nothing flows +back. `vscode requires terminal` is this, and so is `requires container-runtime`. + +The second is **instantiation**: the provider is asked to make something *for this consumer*, +and hands back what the consumer needs to use it. A database, a user, a password, an address. + +They differ in every way that matters: + +| | presence | instantiation | +|---|---|---| +| creates something | no | yes, one per consumer | +| carries a payload back | no | credentials, an address | +| can be revoked | — | yes, and must be when the consumer goes | +| provider holds state about it | no | yes — who was granted what | +| satisfied by | anything providing the name | that provider, specifically | + +**This is the mesh's actual power**, in the operator's words: a small game declares it wants a +database and the mesh makes one. Nobody creates a user by hand, nobody pastes a connection +string. That is worth being precise about rather than folding into a single relation because +one relation is prettier. + +## What that costs the design + +**The proposal's "one kind of edge" is wrong**, and the current system already knew: it has +`dependencies` for presence and `requires: provision:` for instantiation, with the resolver +deriving a presence edge from every instantiation edge. [`analysis.md`](analysis.md) recorded +that derivation as a convenience. It is not — it is the correct relationship between two +genuinely different relations. + +So: **two kinds of edge, one graph.** Instantiation implies presence. Presence does not imply +instantiation. + +## What still has no answer + +**Which postgres?** A mesh with two of them, and a game that wants a database. Presence would be +satisfied by either. Instantiation cannot be — the data will live in exactly one, and choosing +wrongly is not a preference, it is the game's data in the wrong place, discovered later. + +This is the *who chooses between providers* question from [`proposal.md`](proposal.md), and the +worked example shows it is far sharper for instantiation than for presence. For `terminal` the +consumer genuinely does not care. For a database it cares permanently. + +**How many instances of postgres should exist?** One per mesh is wrong — a node that must work +while disconnected cannot depend on a database elsewhere. One per node is wrong — the mesh's own +registry is one thing, not one per node. So the answer is per-module, and nothing in the schema +says it. This is [`cases.md`](cases.md) axis *how many instances*, and postgres is the case that +proves it cannot be a global rule. + +**What happens to a grant when the consumer is removed?** The game is uninstalled. Its database +still exists, holding its data. Dropping it silently is data loss; keeping it forever is a leak. +[ADR 0043](../../02-DECISIONS/0043-a-declaration-is-an-ordered-list-of-owned-resources.md) says +the host removes what it applied and no longer declares — but this is not on the host, it is +inside another module's state, and the same reasoning does not obviously carry. + +**Where does node-derived configuration come from?** (3) The control plane composes a +declaration, and cannot know this machine's memory. Either the host fills in a blank the +declaration leaves — which makes the host decide something, against +[ADR 0037](../../02-DECISIONS/0037-the-host-applies-it-does-not-decide.md) — or the control +plane reads the node's inventory first and composes with it. The second is consistent and means +a declaration is composed *per node from what the node reported*, which is a stronger claim than +anything recorded so far.