Merge pull request 'ADR 0262: a preference has a default in its manifest, and the operator's own value has none' (#216) from feat/setting-defaults into main

This commit was merged in pull request #216.
This commit is contained in:
2026-10-08 16:18:53 +00:00
8 changed files with 381 additions and 4 deletions
+6 -1
View File
@@ -66,7 +66,7 @@ is listed under [Homonyms](#homonyms), with the qualified form each domain uses.
**Purpose.** To say what one module is, in the one document every other domain reads.
**Recorded in** the catalogue, one manifest per module. **Upstream of** every other domain; its words
are a published vocabulary, fixed by the manifest's schema. **Decided by** ADR 0040, 0126, 0174, 0188,
0207, 0236.
0207, 0236, 0262.
**Uses:** module, seat, provision (Provisioning), data class (Data), health (Health and repair).
- **manifest** — the file a module is defined by (`module.json`): what it is, what it provides and
@@ -85,6 +85,11 @@ are a published vocabulary, fixed by the manifest's schema. **Decided by** ADR 0
a module ([ADR 0174](../02-DECISIONS/0174-a-node-varies-a-module-through-settings-and-kept-regions-never-an-edit.md)).
What a "flavor" once varied is a setting or a separate module.
*Not:* ~~flavor~~
- **preference** — a setting whose manifest gives it a default, because its value is true of the
software and only tuned per node: a font size, a width
([ADR 0262](../02-DECISIONS/0262-a-preference-has-a-default-in-its-manifest-and-the-operators-own-value-has-none.md)).
A setting with no default is the operator's own value: a domain, an identity.
*Not:* ~~tunable~~ (hq)
- **kept region** — a marked block in a managed file the mesh writes *into*, where the operator's own
lines survive every send and are given back when the module goes (ADR 0174). The other way a node
varies a module.
@@ -0,0 +1,39 @@
---
status: active
initiated: 2026-10-08
touches:
- 02-DECISIONS/0262-a-preference-has-a-default-in-its-manifest-and-the-operators-own-value-has-none.md
- 03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md
- the catalogue's module manifests
- the media catalogue's module manifests
became: []
---
# 037 — The catalogue's hard-coded preferences
## What is investigated
[ADR 0262](../../02-DECISIONS/0262-a-preference-has-a-default-in-its-manifest-and-the-operators-own-value-has-none.md)
lets a manifest give a **preference** a default, which the mesh-wide settings and a node's settings
override. Before it, a value a person might want to tune per machine could only be written into a
module's file as a literal. This effort lists those literals in both catalogues: the value, where it
is, the key and default it would take as a preference, and why a person would change it.
## Why
The first case was the notifier's font size, which is less than half as tall on a laptop panel of
about 240 DPI as on a desktop monitor of about 110 DPI, while X is told 96 on both. The same is true of
every size, font and gap on the desktop, and of retention counts, timeouts and thresholds in the
services. A list says which modules to convert, in what order, and which values are shared between
modules and should keep one default.
## What it touches
The manifests of both catalogues, and through ADR 0262 the controller's `settings` verb, which lists
every declared preference. Nothing is changed here: each conversion is a change to its module, made on
the operator's yes.
## Documents
- [01 — the survey](01-the-survey.md): every candidate, by module, strongest first, with the modules
that already take settings and those that have nothing to convert.
@@ -0,0 +1,115 @@
# 01 — The survey
Both catalogues were read on 2026-10-08: every manifest's file contents and the sources they are
copied from, the shell code modules contribute to the login session, and the contributions to the
display session. Nothing was changed. Each row is a literal that is a preference in the sense of
[ADR 0262](../../02-DECISIONS/0262-a-preference-has-a-default-in-its-manifest-and-the-operators-own-value-has-none.md):
true of the software, and only tuned per machine or per mesh. The proposed default is always today's
value, so converting a module changes no machine until a value is set.
Values that are the operator's own (names, addresses, accounts, secrets) are left out, as are values the
mesh assigns (`${port:…}`, `${dir:…}`, `${machine:…}`, `${bound:…}`, `${secret:…}`) and structure nobody
tunes. Every proposed key passes ADR 0262's whole-word check.
**Where a literal sits decides what converting it needs.** A row marked *(shell)* is in code a module
contributes to the login shell, and one marked *(contribution)* is in a contribution to the display
session. ADR 0262 fills `${setting:…}` in files, contributions and served facts, but not in shell code,
so the *(shell)* rows need the controller to fill shell code too before they can convert. A row marked
*(compiled)* is in a module's own program and needs the program to read the value from a file first.
## The desktop, strongest first
| Module | Where | Today | Key | Why a person changes it |
|---|---|---|---|---|
| xorg | the X resources file | `Xft.dpi: 96` | `dpi` | A laptop panel of about 240 DPI and a desktop monitor of about 110 DPI want different values; the file's own comment already expects a setting |
| xterm | X resources *(shell)* | `faceSize: 12`, face JetBrainsMono Nerd Font, `saveLines: 10000` | `font-size`, `font`, `scrollback-lines` | Terminal text size per screen; the face and history are taste |
| i3 | its configuration | `font pango:JetBrainsMono Nerd Font 11` | `font-size` | Title text size follows the screen's density |
| i3 | its configuration | `gaps inner 0`, `gaps outer 0`, `default_border pixel 1`, resize step 10 px | `inner-gap`, `outer-gap`, `border-width`, `resize-step` | Taste and density; a 1-pixel border is hard to see on a dense panel |
| i3 | its configuration | focused colour `#de5200` | `accent-colour` | Taste; the same colour is in the bar and the launcher (see below) |
| i3status-rust | the display-session contribution *(contribution)* | bar font size 11 | `font-size` | The bar's height follows it |
| i3status-rust | the top bar | window title cut at 50 characters | `title-width` | How much title fits depends on the screen's width |
| i3status-rust | the bottom bar | disk warning at 850 GB, critical at 920 GB | `disk-warning`, `disk-critical` | Thresholds written for one disk; wrong on a smaller laptop drive |
| i3status-rust | the bottom bar | updates warn at 10, polled every 1800 s; clock format with seconds, every 1 s; container block every 2 s, disk every 20 s | `updates-threshold`, `updates-interval`, `clock-format`, `refresh-interval` | Polling costs battery; the clock is taste |
| rofi | its theme | font size 11, width 600 px, 8 lines, icon 24 px, padding 10 px, message font size 11 | `font-size`, `width`, `lines`, `icon-size`, `padding`, `message-font-size` | The launcher's size per screen |
| rofi | the power menu theme | width 800 px, font size 32 | `power-menu-width`, `power-menu-font-size` | Too large on a small panel. The power menu script also passes 350 px, which overrides 800: one of the two is dead |
| rofi | its configuration | history of 25 | `history-size` | History length |
| screen-lock | the session's X settings *(shell)* | lock after 1800 s; display standby 1800 s, off 3600 s | `lock-after`, `display-standby-after`, `display-off-after` | The clearest laptop and desktop difference |
| screen-lock | its wrapper script | blur 5, ring width 7.0, accent `#ca4a00`, time and date formats | `blur`, `ring-width`, `accent-colour`, `clock-format`, `date-format` | Look, density and locale |
| picom | its configuration | terminal opacity 0.95 focused and 0.75 not; tooltip 0.95; fade step 0.05; corner radius 0 | `terminal-opacity`, `inactive-terminal-opacity`, `tooltip-opacity`, `fade-step`, `corner-radius` | Taste |
| picom | its configuration | `backend = "glx"` with vsync | `backend` | Depends on each machine's graphics driver |
| adwaita | GTK 3 and 4 settings, Qt settings, and the GNOME settings *(shell)* | interface font Inter 11, monospace 11; cursor size 24 (also in X resources and GNOME settings); double-click 400 ms, 3 scroll lines | `font-size`, `cursor-size`, `double-click-interval`, `scroll-lines` | Interface size on a dense panel; the cursor size must change in all four places together |
| fonts | the fontconfig defaults | monospace JetBrainsMono Nerd Font, sans Inter, serif Noto Serif | `monospace-face`, `sans-face`, `serif-face` | Taste; every desktop module names these faces again |
| the laptop's hardware module | touchpad configuration and its script | acceleration 0.15, tapping on, natural scrolling on | `touchpad-speed`, `tap-to-click`, `natural-scrolling` | Pointer feel; the script repeats 0.15 |
| the laptop's hardware module | the power daemon's battery levels | low 15 %, critical 10 %, act at 7 %, action suspend | `battery-low`, `battery-critical`, `battery-action-at`, `battery-action` | Thresholds move as a battery wears |
| the laptop's hardware module | its scripts | brightness step 5; on-screen display for 1500 ms | `brightness-step`, `notification-timeout` | Feel |
| feh | the wallpaper command | `--bg-fill` | `wallpaper-mode` | Fill, scale or centre is taste |
| zsh | its rc file | `HISTSIZE` and `SAVEHIST` 100000 | `history-size` | History length |
| lemurs | its configuration | background black, border white | `background-colour` | Low value; only the start of the file was read |
## Services
| Module | Where | Today | Key | Why a person changes it |
|---|---|---|---|---|
| restic | its program *(compiled)* | keep 14 daily, 8 weekly, 6 monthly | `keep-daily`, `keep-weekly`, `keep-monthly` | Retention is the clearest service preference, and today it cannot be changed without a new build |
| logrotate | its configuration | weekly, keep 4; the login record monthly, keep 1 | `rotate-period`, `rotate-count`, `wtmp-rotate-count` | Retention per machine and disk |
| docker | the prune unit and timer | prune what is older than 168 h, weekly | `prune-age`, `prune-schedule` | How long images and build cache stay |
| docker | the daemon's configuration | no log rotation at all | `log-size`, `log-files` | **A new value, not a conversion:** container logs grow without bound today. A default such as 10 MB and 3 files changes every machine, so it is a decision of its own |
| memory-pressure | the zram generator | `min(ram / 2, 16384)` | `zram-size` | RAM and workload |
| memory-pressure | the out-of-memory daemon and the session's limit | swap used 90 %, pressure 60 % for 20 s; the session 80 % | `swap-used-limit`, `pressure-limit`, `pressure-duration`, `session-pressure-limit` | How early processes are killed |
| time-sync | its servers | a regional pool | `pool-region` | The region is the mesh's |
| pacman | its configuration and the mirror list's options | 5 parallel downloads; a country list, the latest 20, sorted by rate | `parallel-downloads`, `mirror-countries`, `mirror-count` | Bandwidth and region |
| fail2ban | its jails | ban 1 day after 3 failures in 1 day; repeat offenders 4 weeks after 2 in 2 weeks | `ban-time`, `find-time`, `max-retries`, `repeat-ban-time`, `repeat-find-time`, `repeat-max-retries` | Strictness |
| sshd | its configuration | port 22 | `listen-port` | A port people move. The filter and the jail name the port too, and must follow it |
| dnsmasq | its configuration | two public resolvers | `upstream-resolvers` | The choice of resolver |
| icecast | its configuration | client timeout 30 s, source 10 s, burst 65535, queue 524288, log level 3, log size 10000 | `client-timeout`, `source-timeout`, `burst-size`, `log-size` | Streaming tuning |
| mailu | its environment file | message size 50 MB, 200 messages a day, sign-in rate limits, fetch every 600 s, log level | `message-size-limit`, `message-rate-limit`, `sign-in-rate-limit`, `fetch-delay`, `log-level` | Real limits a person sets |
| matrix | the homeserver's configuration | request size 20 MB | `upload-size-limit` | The upload cap |
| supabase | three files | a sign-in lifetime of 3600 s, written three times | `session-lifetime` | One value in three places, which a setting keeps equal |
| supabase | the functions' main | 150 MB, 60 s | `function-memory-limit`, `function-time-limit` | Function limits |
| power | its stop unit and script | 120 s, 30 s | (weak) | Bounds on shutdown work, rarely changed |
## The media catalogue
| Module | Where | Today | Key | Why |
|---|---|---|---|---|
| qbittorrent | its web login script | ban for 60 s | `ban-duration` | How long a failed login is locked out |
| tautulli | its start script | wait 60 s for the media server | `media-server-wait` | Low value |
The other media modules carry only identities from settings, key-provisioning scripts and mergeable
files. Nothing to convert.
## Already taking settings
- **`${setting:…}`:** the notifier (font size and width, the first preferences), power, hostname, xdg,
keycloak, mailu, lab, and every media module.
- **A mergeable file:** baserow, claude-code, the licence manager, confluence, gitea, gitlab, grafana,
home-assistant, icecast, influxdb, jira, keycloak, letta, mailu, matrix, mesh-watcher, messenger,
nextcloud, nodered, records, route-adapter, searxng, step-ca, supabase, unifi; in the media
catalogue bazarr, bookshelf, jackett, kometa, lidarr, nzbget, ombi, qbittorrent, radarr, sonarr and
tautulli. A mergeable file's content already is its defaults; ADR 0164's declaration would list its
keys.
## Nothing to convert
avahi, blueman, bluetooth, build-agent, ca-trust, clipmenu (its 1-second timeout is a safety bound),
cups, dbus, de-spiegel, disk-load, distribution, dmenu, gnome-keyring, hello-web, invoicing, letta,
local-model-consumer, localization (the language and keymap are the operator's, not a preference),
mesh-catalog, minio, model-usage, mongodb, mosquitto, mssql, n8n, nats, networkmanager, nftables,
node-env, only-office, redis, route-proxy, ssh-client, sudo, systemd, systemd-networkd,
systemd-resolved, triggerhappy, umami, xclip. A container's internal port is structure, not a
preference.
## What follows from it
- **Shared values want one default each.** The accent colour is `#de5200` in the window manager, the
bar and the launcher, and `#ca4a00` in the lock screen. The interface font size is 11 in the window
manager, the bar, the launcher and the GTK, Qt and GNOME settings, and 12 in the terminal. As one
setting per module they would drift. Either each module declares the same default and a person sets
them together, or the mesh grows a preference several modules read. That is a decision, not a
conversion.
- **Three prerequisites in the controller and the modules:** filling `${setting:…}` in shell code, for
the *(shell)* rows; the restic program reading its retention from a file; and, for the container
runtime's log rotation, a decision on the new default.
- **Order:** the density rows first (xorg's DPI, then the font sizes of the window manager, the bar,
the launcher, the terminal and the interface, and the cursor size), because the laptop panel is the
case that started this. Then the lock and display timeouts, then retention and thresholds.
@@ -9,6 +9,11 @@ extends: 02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md
# 155. A definition names no installation: how that is checked, and the three ways a value that did gets out
> **The mechanism changed — 2026-10-08, by [ADR 0262](0262-a-preference-has-a-default-in-its-manifest-and-the-operators-own-value-has-none.md).** A `${setting:<key>}` nothing sets is
> still refused by name when the value is the operator's own, as decided here. A key the manifest declares
> a preference, with a default and why, takes that default instead; the mesh-wide and the node's settings
> override it.
## Context
[ADR 0112](0112-a-module-definition-names-no-node-mesh-or-path.md) decided that a module definition
@@ -9,6 +9,11 @@ extends: 02-DECISIONS/0046-a-module-configuration-is-its-assignments-not-its-man
# 164. A setting is declared with its default, its meaning and what changing it costs
> **The mechanism changed — 2026-10-08, by [ADR 0262](0262-a-preference-has-a-default-in-its-manifest-and-the-operators-own-value-has-none.md).** The default half of this record is
> built, in its own spelling: a manifest's `settings` gives a preference its `default` and `why`, and fills
> `${setting:<key>}` with it. The rest stays proposed here: the type, the meaning, the target file, the cost
> of a change, and the refusal of an undeclared key when it is set.
## Context
The operator asked for one thing for every module, with the container runtime as the first case: **one
@@ -0,0 +1,194 @@
---
topic: what runs on it
status: accepted
date: 2026-10-08
deciders: jochen
reconstructed: false
extends: 02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md
---
# 262. A preference has a default in its manifest, and the operator's own value has none
## Context
A file a module declares may ask for an operator's value as `${setting:<key>}`
([ADR 0155](0155-a-definition-names-no-installation-and-how-that-is-checked.md)). The controller fills it
from the module's mesh-wide settings, then the node's, and **refuses to compose the module when neither
sets the key**. [ADR 0112](0112-a-module-definition-names-no-node-mesh-or-path.md) had allowed an
operator's value a default ("the assignment's, or the requirement's default, or unresolved"); ADR 0155
built the placeholder without one, for a reason that stands: "a default for a mail domain would be the
literal this removes". The controller's refusal says it as "an operator's value is the assignment's,
never the definition's".
A second rule closes the other side. `settings` refuses to store a key that no file, contribution or
served fact of the module asks for: "these settings reach nothing". That refusal exists so a misspelt key
is caught when it is set rather than found when a machine does not change.
Together the two rules leave a module that is already running with no way to gain a setting:
- if the file asks for `${setting:font-size}` first, every machine running the module fails to compose
until somebody sets the key on each of them;
- if somebody sets the key first, it is refused, because no file asks for it yet.
The case that found it is the notifier on the operator's machines. It runs on the laptop and on a
workstation, with one file whose font is `Inter 10` and whose width is 250 pixels. The laptop's panel is
measured at 242 DPI while X is told 96, so the same 10 points is less than half as tall there as on the
workstation's 110 DPI monitors. Making the size a setting would have broken the notifier on both
machines for as long as the change took to reach them.
[ADR 0164](0164-a-setting-is-declared-with-its-default-its-meaning-and-what-changing-it-costs.md),
still proposed, already decides that "a setting with a default is a tunable; a setting without one is the
operator's", within a larger declaration of every setting with its type, meaning, cost and target
file. None of it is built, and its spelling was left to design 27. This record takes the default half
now, in the smallest form that removes the gap, and leaves the rest of 0164 standing as proposed.
## Considered Options
1. **Set the key on every machine before the file asks for it.** Rejected: `settings` refuses the key
until the file asks for it, so there is no order that works. Loosening that refusal would bring
back the misspelt key nobody notices.
2. **Let `${setting:<key>|<default>}` carry a default inline in the file.** Rejected: the default
would be hidden in file content, invisible to `settings` and to a reviewer of the manifest, and
nothing could tell a preference's default from a mail domain written the same way.
3. **Build all of ADR 0164 first.** Rejected for now: it is the right end state, but it changes how
every mergeable file takes settings and needs design 27's requirement form. The notifier's font size
does not need to wait for that.
4. **A manifest declares a default for a preference, with why, and nothing for the operator's own
value.** Chosen.
## Decision
**A manifest may give a setting a default when the value is a preference.** A preference is true of the
software and only tuned per machine: a font size, a width, a number of workers. Each one is declared
under `settings` in the manifest with three fields: `kind` (which is `preference`), `default` (a
string, a number or a boolean) and `why` (one sentence on why this default).
**The operator's own value has no default.** A domain, a public name, an identity, an address or a
secret is true of one installation, and declaring a default for it would be the literal ADR 0112
removed. It is declared nowhere in the manifest, and a file asking for it is still refused by name
when nothing sets it, as ADR 0155 decided. This record narrows that refusal to exactly this case; it
does not change it.
**The settings override the default in a fixed order:** the manifest's default, then the mesh-wide
settings, then the node's, and the last one wins. A default fills `${setting:<key>}` in a file's
content, a contribution and a served fact. A module's zone name is the one served value it does not
fill: that name is the operator's own and has no default. A default is never merged into a mergeable
JSON file as a key, because that file's content already holds its own defaults, and a key laid over every
such file is the leak [issue 168](../04-ISSUES/168-a-setting-reaches-every-file-and-contribution/00-report.md)
describes.
**A key with a default is not stray.** `settings` accepts it on the mesh or on one node. That is how a
running module gains a setting with no gap: the module's next version asks for the key and gives it
today's value as its default, so nothing changes on any machine, and a machine that should differ
sets its own value afterwards.
**`settings` shows every value and where it came from.** After the stored settings, which a caller
reads before replacing them ([ADR 0217](0217-no-change-to-a-machine-takes-effect-unseen.md)), it lists
each key the module gives a default or that is set, its effective value, and its source: `default`,
the mesh, or the node. Where the mesh or the node overrides a default, the default is shown beside it.
**One listing of every preference, in the controller's `settings` verb.** Asked with `list:
"preferences"`, or with no module, `settings` lists every module that declares a preference: each key,
its default and why, and, for every machine the module is assigned to, the value there and where it
comes from (the default, the mesh, or the node). A module and a node narrow it: with a node, only the
modules on that machine are listed, and a machine the mesh does not know is refused. Values or a clear
without a module are refused: a module is needed to set values. No module builds a
settings tool of its own; this verb is the interface for all of them, so a person or an agent finds every
preference in one place.
**A default must be read by something.** A declared key that no file, contribution or served fact asks
for is refused, because a default that reaches nothing is the same mistake as a stray setting.
**The defaults are a layer of their own, marked as one.** A node's settings are named after the node,
and a node may be called anything, `default` included. The defaults are told apart by a mark on the
layer, never by its name.
**A stored manifest with a key the controller does not know costs that module, never the catalogue,
and never silently.** Registration and the module check refuse such a key, as before. A manifest the
store already holds was registered by a newer controller and is read by this one after a rollback.
Read strictly, it failed the whole catalogue, and with it every plan and every send on every node.
Read with the key dropped, the module would run without something its manifest says, which is worse.
So the controller reads the rest of the catalogue as before, and for that module:
- it is left out of every machine's declaration, by name, with the key as the reason, the way a module
whose stored settings no longer compose is left out
([ADR 0163](0163-taking-a-module-over-is-a-comparison.md), rule 6). Its files and data on the
machine stay where they are and its containers are not removed, but everything the declaration
carries for it stops while it is left out: the backup holder's lines for its data, its contributions
to a seat's holder, and the ports the filter opens for it;
- the controller raises a condition naming the module, saying it uses a field this controller does not
know and is left out until the controller is updated, and clears it once the module reads again;
- the controller's log says it once.
A key at the top of the manifest is set aside and the rest of the manifest is read. A key inside an
entry (a state, a provided name, an offer's identity, a data item's backup, an own secret, a seat's verb)
is found as well, and the module is left out and raised the same way, but the controller that merged
this record then keeps only the module's name and version: it no longer provides what it provides, so a
module that requires it is refused on every machine, by name. The rule holds for every field a later
record adds, not only `settings`.
**What a left-out module keeps is to be widened.** A follow-up to the controller reads an entry with an
unknown key in a second pass, so a left-out module still provides what it provides and no consumer is
refused for it, and keeps its backup lines, so its data is still copied while it is left out; the
condition then says exactly what stopped. Until that follow-up is live, the condition and this record
say what stops as above.
## Consequences
- The notifier's font size and width become preferences with today's values as defaults, and the
laptop sets larger ones for itself. Every other machine composes the file it has today, byte for byte.
- A manifest that declares `settings` is refused by a controller older than this record, which reads
manifests strictly. The controller change is sent first, and a catalogue change that uses the field
merges only after it is live.
- **This protects rollbacks to this controller and later ones, not to earlier ones.** Once a manifest
in the store uses `settings`, a controller from before this record still fails the whole catalogue
read. **Before rolling the controller back below this record, register the notifier again at its
manifest from before this record**, which has no `settings`; then roll back. Rolled back to this
version or a later one, nothing needs doing first: the notifier is left out of its machines by name,
a condition says so, and every other module is composed.
- **What got harder:** a module author now decides, for every `${setting:…}`, whether the value is a
preference or the operator's own, and says so. The check below catches the clear mistakes by the
key's name and by the value naming an installation. It cannot catch a preference default that is
wrong for a machine; that remains a review question.
- ADR 0164 still stands as proposed. When it is built, this record's `settings` block is the start of
its declaration: the type, meaning, cost and target file are added to the same entry, and a setting
with no default is declared there too.
- **Not decided here:** whether a change to a preference reloads the software that reads it. The
notifier reads its file when it starts, and nothing tells a running notifier that the file changed.
That is ADR 0164's "what changing it costs".
## How it is checked
| Rule | Checked by |
|---|---|
| A default is declared only for a preference, with why | The controller's manifest parser, run on every catalogue pull request as the module check: a `settings` entry is refused without `kind: preference`, without a default, with an empty or non-scalar default, or without `why` (`TestTheParserTakesAPreferenceAndRefusesTheRest`) |
| The operator's own value has no default | The same parser refuses a default for a key about the operator's own value: its last word, read as singular, is one such name (among them domain, host, hostname, zone, site, timezone, url, ip, ipv4, ipv6, mail, address, user, owner, uid, password, secret, token, key, apikey, bearer, cert), or its last two are a compound such as client-id, site-name, server-name, public-name or smtp-relay. A key that starts with max, min, num, count, show, hide, enable, disable, use or allow is a number or a switch, not an identity; the controller follow-up narrows that to the first eight, since `allow-from` or `use-host` names whom, and adds host-name, user-name, domain-name, nameserver, dns-server, gateway, subnet, sender, recipient, contact and certificate; and the installation check of ADR 0155 already refuses a default naming a domain or a public address. `TestAnOperatorsValueWithoutADefaultIsStillRefused`: an unset key with no default is still refused by name |
| The node over the mesh over the default | `TestTheNodeOverTheMeshOverTheDefault`, and `TestAComposedMachineGetsTheDefaultAndTheNodesValue` through a machine's whole composition |
| A default never becomes a key of a mergeable file | `TestADefaultIsNotMergedIntoAJSONFile` |
| A key with a default is accepted by `settings` | `TestAKeyWithADefaultIsNotStray` |
| A default reaches something | The parser refuses a declared key that nothing asks for (`TestTheParserTakesAPreferenceAndRefusesTheRest`, "read by nothing") |
| Every value says where it came from | `TestEveryValueSaysWhereItCameFrom` and `TestSettingsSayWhereEachValueComesFrom` |
| A key is the operator's by what it is about | `TestAKeyIsTheOperatorsByWhatItIsAbout`: `max-tokens`, `show-hostname`, `users-per-page`, `mailbox-size`, `user-agent`, `url-timeout` are preferences; `api-key`, `oauth-client-id`, `public-ip`, `dns-zone`, `allowed-hosts`, `site-name`, `smtp-relay` are not |
| A default is read by the installation check | `TestTheInstallationCheckReadsADefault`, including a setting called `description` |
| A default fills a contribution and a served fact, and never replaces a contribution's own value | `TestADefaultFillsAContributionAndAServedFactButNotALiteral` |
| A node called `default` is a node | `TestANodeCalledDefaultIsANodesLayer` |
| One listing of every preference, per machine with its source | `TestSettingsListPreferences` (the verb's arguments; a listing other than preferences, and values or a clear without a module, refused), `TestPreferencesSayEachMachinesValueAndItsSource`, and `TestPreferencesListedFromTheStores` (a machine names only the modules on it; a machine the mesh does not know is refused); live, `mesh-controller.settings {list: "preferences"}` lists the notifier on both machines |
| A stored manifest with an unknown key, at the top or in any block, leaves its module out by name, and registration refuses it | `TestAStoredManifestWithAnUnknownKeyIsLeftOutAndRegistrationRefusesIt` (one case per block); `TestTheCatalogueLoadsAroundAManifestWithAnUnknownKey` over the store |
| It is a condition until it reads again | `TestAModuleWithAnUnknownFieldIsACondition`: raised in plain words, cleared once the module reads |
| Nothing changes where no machine sets a value | The notifier's manifest test fills the file with the defaults and finds today's lines; live, `mesh-controller.plan` for the workstation shows no change to the notifier's file after the catalogue change is sent |
## References
- [ADR 0112](0112-a-module-definition-names-no-node-mesh-or-path.md), [ADR 0155](0155-a-definition-names-no-installation-and-how-that-is-checked.md):
the operator's value, and its refusal
- [ADR 0164](0164-a-setting-is-declared-with-its-default-its-meaning-and-what-changing-it-costs.md): the
whole declaration this record starts
- [ADR 0174](0174-a-node-varies-a-module-through-settings-and-kept-regions-never-an-edit.md): a node
varies a module through settings
- [ADR 0217](0217-no-change-to-a-machine-takes-effect-unseen.md): a caller reads the stored settings
before replacing them
- [Design 27](../03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md): the operator as a
provider
- [Research 037](../01-RESEARCH/037-the-catalogues-hard-coded-preferences/00-overview.md): the literals in
both catalogues that would become preferences
- mesh-controller `internal/catalogue/setting_defaults.go`; mesh-catalog `modules/dunst`
+3 -2
View File
@@ -5,8 +5,9 @@ code:
- mesh-controller cmd/mesh-builder
- mesh-controller internal/builder
- mesh-catalog modules/build-agent
updated: 2026-10-06
updated: 2026-10-08
decisions:
- 02-DECISIONS/0262-a-preference-has-a-default-in-its-manifest-and-the-operators-own-value-has-none.md
- 02-DECISIONS/0233-a-module-declares-the-data-it-holds-and-the-mesh-protects-and-watches-it-from-that.md
- 02-DECISIONS/0219-the-build-queue-is-controlled-through-the-controller-and-the-build-seat.md
- 02-DECISIONS/0190-a-seats-work-is-shared-by-its-holders-and-building-is-the-first-such-role.md
@@ -241,7 +242,7 @@ counts as a copy and what as a base.
| resource | is | a module may |
|---|---|---|
| `directory` | a directory with a mode and an owner, **placed by the mesh** under the node's root: `place: "."` is the assignment's own root, `place: "mesh"` the mesh's directory for the module, a pathless one sits beneath the root by its id; a stated path is the placement for data that must stay where it is, and may itself sit beneath a placed one (`${dir:<id>}/…`). Everything else names it as `${dir:<id>}` ([issue 119](../../04-ISSUES/119-a-module-definition-decides-where-its-files-live/00-report.md), [174](../../04-ISSUES/174-the-meshs-own-files-for-a-module-are-placed-by-the-definition/00-report.md)) | ✅ |
| `file` | literal content, with `${bound:…}`, `${secret:…}`, `${dir:…}`, `${port:…}`, `${machine:…}` and `${setting:…}` filled in — the last an operator's value from the assignment's settings, refused by name when unset ([ADR 0155](../../02-DECISIONS/0155-a-definition-names-no-installation-and-how-that-is-checked.md)) | ✅ |
| `file` | literal content, with `${bound:…}`, `${secret:…}`, `${dir:…}`, `${port:…}`, `${machine:…}` and `${setting:…}` filled in — the last an operator's value from the assignment's settings, refused by name when unset ([ADR 0155](../../02-DECISIONS/0155-a-definition-names-no-installation-and-how-that-is-checked.md)) unless the manifest gives a preference a default ([ADR 0262](../../02-DECISIONS/0262-a-preference-has-a-default-in-its-manifest-and-the-operators-own-value-has-none.md)) | ✅ |
| `user` | a login | ✅ |
| `access` | a pre-existing path it may use and must not own, **named by id** and placed by the assignment (`accesses: {<id>: <path>}` on its settings); mounts say `${access:<id>}`; a path in the definition is the default an assignment replaces, tolerated while the catalogue converts ([issue 153](../../04-ISSUES/153-an-adopted-machines-data-cannot-be-placed-where-it-is/00-report.md)) | ✅ |
| `archive` | files fetched by digest and unpacked | ✅ |
@@ -2,8 +2,9 @@
layer: to-be
status: in-progress
code: [mesh-controller internal/catalogue]
updated: 2026-10-06
updated: 2026-10-08
decisions:
- 02-DECISIONS/0262-a-preference-has-a-default-in-its-manifest-and-the-operators-own-value-has-none.md
- 02-DECISIONS/0225-a-consumers-identity-is-bounded-by-the-provision-it-requires.md
- 02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md
- 02-DECISIONS/0155-a-definition-names-no-installation-and-how-that-is-checked.md
@@ -199,6 +200,18 @@ the gap this design answered on 2026-09-26; built later the same day: a director
`${dir:<id>}/<rest>` and moves with it, and the same proof — both catalogues resolved and compared —
shows forty-eight definitions naming the paths they named before.
*A preference has a default; the operator's own value has none (2026-10-08,
[ADR 0262](../../02-DECISIONS/0262-a-preference-has-a-default-in-its-manifest-and-the-operators-own-value-has-none.md)):* the operator provider's optional default, which this design always had,
in its first form. A manifest's `settings` gives a key a `default` and a `why` when its `kind` is
`preference`: a font size, a width, a number of workers. The default is the lowest of the values: the
mesh-wide settings override it, and the node's override both. It fills `${setting:<key>}` in a file, a
contribution or a served fact, and is never merged into a mergeable file as a key. A value that is the
operator's own (a domain, an identity, a secret) is declared nowhere and stays refused by name when
unset. A key with a default is accepted by `settings` even before anything is set, so a running module
gains a setting by shipping its current value as the default, and `settings` lists each value with its
source: the default, the mesh, or the node. The first module to use it is the notifier, whose font size
and width are preferences. *How it is checked:* the table in ADR 0262.
*The operator's value travels only where it is asked for (2026-09-30,
[issue 173](../../04-ISSUES/173-a-modules-settings-reach-every-fact-it-contributes/00-report.md)):*
a setting overrides a key a contribution or a served fact declares and adds none; a file keeps taking