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:
+6
-1
@@ -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
|
||||
|
||||
+5
@@ -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
|
||||
|
||||
+194
@@ -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`
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user