diff --git a/00-META/glossary.md b/00-META/glossary.md index e177d8f2..af651753 100644 --- a/00-META/glossary.md +++ b/00-META/glossary.md @@ -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. diff --git a/02-DECISIONS/0155-a-definition-names-no-installation-and-how-that-is-checked.md b/02-DECISIONS/0155-a-definition-names-no-installation-and-how-that-is-checked.md index 9deb8e54..8f49618d 100644 --- a/02-DECISIONS/0155-a-definition-names-no-installation-and-how-that-is-checked.md +++ b/02-DECISIONS/0155-a-definition-names-no-installation-and-how-that-is-checked.md @@ -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:}` 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 diff --git a/02-DECISIONS/0164-a-setting-is-declared-with-its-default-its-meaning-and-what-changing-it-costs.md b/02-DECISIONS/0164-a-setting-is-declared-with-its-default-its-meaning-and-what-changing-it-costs.md index f1caeda0..9eff4ff5 100644 --- a/02-DECISIONS/0164-a-setting-is-declared-with-its-default-its-meaning-and-what-changing-it-costs.md +++ b/02-DECISIONS/0164-a-setting-is-declared-with-its-default-its-meaning-and-what-changing-it-costs.md @@ -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:}` 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 diff --git a/02-DECISIONS/0262-a-preference-has-a-default-in-its-manifest-and-the-operators-own-value-has-none.md b/02-DECISIONS/0262-a-preference-has-a-default-in-its-manifest-and-the-operators-own-value-has-none.md new file mode 100644 index 00000000..15393562 --- /dev/null +++ b/02-DECISIONS/0262-a-preference-has-a-default-in-its-manifest-and-the-operators-own-value-has-none.md @@ -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:}` +([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:|}` 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:}` 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` diff --git a/03-DESIGN/01-to-be/18-building-a-module.md b/03-DESIGN/01-to-be/18-building-a-module.md index 36c361d4..d89794ae 100644 --- a/03-DESIGN/01-to-be/18-building-a-module.md +++ b/03-DESIGN/01-to-be/18-building-a-module.md @@ -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:}/…`). Everything else names it as `${dir:}` ([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: {: }` on its settings); mounts say `${access:}`; 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 | ✅ | diff --git a/03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md b/03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md index 33db43e6..d5a173d2 100644 --- a/03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md +++ b/03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md @@ -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:}/` 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:}` 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