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.**