From f14030325714966d3d5a0a23d8edaf3d5da304a6 Mon Sep 17 00:00:00 2001 From: jochen Date: Sat, 29 Aug 2026 21:00:13 +0200 Subject: [PATCH] A module claims; it does not list its rivals. And flavor is retired. Three decisions, all Jochen's, and the first is the one that unlocked it. Exclusivity is not a property of a module. It is a property of a singular resource the module takes over. Two shells compete for nothing and any number may be installed; two display servers both want the seat. So a module declares what it CLAIMS, and two modules claiming the same thing cannot both be assigned within that claim's scope. Not "xorg conflicts with wayland". Pairwise exclusion has a property that only shows up later: adding a third display server means editing xorg and wayland to know about it. Every new module requires changing modules nobody who wrote it owns, and the edits grow as the square of the count. With a claim the third one says what it claims and nothing else changes anywhere. Claims have a scope -- node, site, mesh -- which is not new. The mesh already enforces exactly one hub with a unique index. Scope is that idea said once rather than hard-coded per case. And some conflicts need no claim at all: two modules declaring the same file or binding the same port are visible from what they declare. A claim is only written for the abstract ones. A requirement with several answers is refused, never guessed. One candidate is assigned silently because there was no choice to make; none is refused naming what is missing; several is refused naming them. That is what makes a solver unnecessary -- counting candidates has no surprising behaviour, and a solver can be added later without changing a single manifest. Flavor is retired. It was carrying three unrelated meanings: variants of a thing, a subset of a module a node installs, and whatever the current system does, which earned two knowledge-base entries about going wrong. A word with three meanings cannot be reasoned about. What it reached for is two ordinary things -- different modules providing the same thing, and one module with a setting. --- 02-DECISIONS/0009-modules-and-the-graph.md | 90 +++++++++++++++++++++- 03-DESIGN/01-to-be/00-work-breakdown.md | 4 +- 2 files changed, 90 insertions(+), 4 deletions(-) diff --git a/02-DECISIONS/0009-modules-and-the-graph.md b/02-DECISIONS/0009-modules-and-the-graph.md index d127ba9..d4eae6a 100644 --- a/02-DECISIONS/0009-modules-and-the-graph.md +++ b/02-DECISIONS/0009-modules-and-the-graph.md @@ -53,8 +53,8 @@ and receiving credentials. Same edge. **Provider stops being a category.** Any hosted thing can be a factory — an identity provider grants clients, a mail server grants mailboxes. It is a facet, not a kind. -**A module may also declare exclusion**, because some things cannot coexist on one machine and -that is a fact about the module rather than about a particular node. +**A module may also declare what it claims**, because some things cannot coexist and that is a +fact about the module rather than about a particular node. What that means precisely is below. ### Why the build edge is a different kind @@ -82,6 +82,92 @@ and the values are derived onto the consumer. **Neither the credential nor the t written by hand.** A requirement may name a provider on another node, so cross-node wiring is the same declaration. +## What a module claims, and why it is not a list of rivals + +*Written 2026-08-29, replacing pairwise exclusion.* + +**Exclusivity is not a property of a module. It is a property of a singular resource the module +takes over.** Two shells do not compete for anything and any number may be installed. Two display +servers both want the seat, and only one may have it. + +> **A module declares what it *claims*. Two modules claiming the same thing cannot both be +> assigned within that claim's scope.** + +**Not "xorg conflicts with wayland".** Pairwise exclusion has a property that only shows up later: +adding a third display server means **editing xorg and wayland to know about it**. Every new +module requires changing modules nobody who wrote it owns, and the edits grow as the square of +the count. With a claim, the third one says `claims: the seat` and nothing else changes anywhere. +**The new module is the only thing that has to know anything** — which is the difference between +a catalogue that grows and one that calcifies. + +The pattern is common enough to be worth listing, because seeing it is most of understanding it: + +| these coexist | these claim one thing | +|---|---| +| shells — bash, zsh, fish | display servers — xorg, wayland (*the seat*) | +| editors — vim, emacs, helix | init — systemd, openrc (*pid 1*) | +| language runtimes | container runtime — docker, podman | +| terminal emulators | reverse proxies — nginx, caddy, traefik (*ports 80/443*) | +| browsers | time — chrony, timesyncd, ntpd (*the clock*) | +| | resolvers — resolved, dnsmasq, unbound (*`/etc/resolv.conf`*) | +| | network management — NetworkManager, networkd, netctl | +| | mail — postfix, exim, msmtp (*port 25*) | +| | audio — pipewire, pulseaudio (*the device*) | + +**A claim has a scope**, because not everything singular is singular per machine: + +| scope | example | +|---|---| +| **node** | the seat, pid 1, port 443 | +| **site** | a DHCP server on a segment | +| **mesh** | the hub, the control plane | + +The last is not new — the mesh already enforces exactly one hub with a unique index +([ADR 0007](0007-connectivity.md)). Scope is that idea, said once rather than hard-coded per case. + +**Some conflicts need no claim at all.** Two modules declaring the same file, or binding the same +port, are visible from *what they declare* — the mesh already holds every resource of every +declaration. So a claim is only written for the abstract ones, where nothing in the declaration +reveals the clash. That keeps the manifest small, which is worth protecting. + +## A requirement with several answers is refused, never guessed + +A module requiring *a shell* may be satisfied by three. The mesh does not pick. + +| candidates | what happens | +|---|---| +| exactly one | assigned, silently — there was no choice to make | +| none | refused, naming what is missing | +| several | **refused, naming them**, and a person chooses | + +**This is what makes a solver unnecessary.** Counting candidates is a few lines and has no +surprising behaviour; a solver that picks has to be understood before its answer can be trusted, +and it is understood by whoever is debugging it at the time. Nothing here is lost by waiting — +a solver can be added later without changing a single manifest, and the reverse is not true. + +**Requiring a module and requiring a capability are different fields**, because the remedies +differ and the message should say which: + +- *i3 needs xorg, which is not assigned here* — assign it. +- *this machine has no seat* — wrong machine; nothing can be installed to fix it. + +## "Flavor" is retired + +It was carrying three unrelated meanings — variants of a thing, a subset of one module a node +installs, and whatever the current system does, which earned two knowledge-base entries about +going wrong. **A word with three meanings cannot be reasoned about**, and every attempt to design +around it produced a rule that was right for one meaning and wrong for the others. + +What it was reaching for is two ordinary things: + +- **Different modules that provide the same thing.** `zsh` and `fish` both provide *a shell*. They + are two modules, not one module with a switch: they share a name and nothing else — different + packages, different configuration, different everything. +- **One module with a setting.** A monitoring module that is an agent here and a server there is + one module, configured. Nothing varies but a value. + +If something is neither, it is probably two modules. + ## The core library is the mesh's domain One module everything may depend on. It holds **what is true of the mesh regardless of which diff --git a/03-DESIGN/01-to-be/00-work-breakdown.md b/03-DESIGN/01-to-be/00-work-breakdown.md index 8424dbc..243517a 100644 --- a/03-DESIGN/01-to-be/00-work-breakdown.md +++ b/03-DESIGN/01-to-be/00-work-breakdown.md @@ -84,7 +84,7 @@ The decomposition is impossible while a feature is a singleton per module. |---|---|---| | 1.1 | Decision record — named features, per-node opt-in (next free number) | accepted | | 1.2 | Manifest: declared `features:` with type + directory | a module declares two of one kind and both build | -| 1.3 | Selection: `always` / flavor-selected / `optional` | a node installs a subset; artifacts stay flavor-blind | +| 1.3 | Selection: `always` / `optional` | a node installs a subset; artifacts stay selection-blind | | 1.4 | `requires:` moves onto the feature | a schema feature's database is not provisioned where the feature is not installed | | 1.5 | Assignment carries the opted-in feature set | opting a node in requires no rebuild | @@ -103,7 +103,7 @@ Cheapest first, and each one proves the extraction pattern before the expensive | 2.2 | `hal/stream` — the record; notifications and messaging as views | axon, synapse, notifications, meetings, conversations | medium | | 2.3 | `hal/agents` — identity, licence, runs, memory, thoughts | noxflow agents, `hal/thoughts` | **high** — touches credentials | | 2.4 | `hal/work` — what remains of noxflow | noxflow tasks | medium | -| 2.5 | `hal/ai` — provider integration, flavored | `hal/claude*` | medium | +| 2.5 | `hal/ai` — one module per provider, each providing *a model provider* | `hal/claude*` | medium | Each extraction is expand-then-contract: new context alongside, dual-write, verify, cut over, remove. **Never a move commit.**