ADR 0262: let a preference carry a default so a running module can gain a setting

A ${setting:} nothing set failed composition, and settings refused a key no file asked
for yet, so no order of changes could add a setting to a running module. A manifest may
now default a preference; the operator's own value still has none.
This commit is contained in:
jochen
2026-10-08 17:55:04 +02:00
parent acc82aa5e4
commit 417db4d837
6 changed files with 166 additions and 3 deletions
+4
View File
@@ -85,6 +85,10 @@ 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.
- **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.
@@ -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,135 @@
---
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>}` wherever it
appears: a file's content, a contribution, a served fact. It 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.
**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.
## 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.
- **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 whose name says it is the operator's (domain, host, issuer, url, mail, address, identity, login, user, account, password, secret, token, credential), 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` |
| 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
- 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