Fill a preference's ${setting:} from its manifest default (hq ADR 0262)

Without a default, a running module could never gain a setting: the file asking for it
failed to compose until set, and the key was refused as stray until a file asked for it.
Defaults sit under the mesh's and the node's settings, never merge into a JSON file, are
refused for the operator's own values, and settings shows each value's source.
This commit is contained in:
jochen
2026-10-08 17:24:32 +02:00
parent 3f68a495f1
commit e41b78cd77
11 changed files with 470 additions and 13 deletions
+1 -1
View File
@@ -238,7 +238,7 @@ func (r Resolution) derivedFor(provision, as, consumer, local string, settings S
"different things and nothing would compare them (novox/hq ADR 0202)",
consumer, local, provision, m.Module, orNothing(sortedAnyKeys(names)))
}
settled, err := Settle(names, settings[m.Module])
settled, err := Settle(names, WithDefaults(m, settings[m.Module]))
if err != nil {
return nil, fmt.Errorf("%s serving %s: %w", m.Module, provision, err)
}
+3 -3
View File
@@ -907,7 +907,7 @@ func (r Resolution) compose(with Rendering, owner map[string]string,
// **An operator's value, from the assignment** (novox/hq ADR 0112, ADR 0155): what a
// definition may not carry because it is true of one installation only. Filled from
// the same layers a mergeable file takes, and refused when no layer set it.
if err := settingInto(copied, with.Settings[m.Module], m.Module); err != nil {
if err := settingInto(copied, WithDefaults(m, with.Settings[m.Module]), m.Module); err != nil {
return nil, err
}
// **Placed before anything reads a path.** A pathless directory receives the path
@@ -1495,7 +1495,7 @@ func (r Resolution) composed(m Manifest, to string, raw map[string]any, layers [
// Overridden, not merged: a setting changes a key the contribution declares and adds none.
// The provider reads the contribution as a contract, and a setting made for one of this
// module's files is no part of it (novox/hq 04-ISSUES/173).
values, err := overridden(raw, layers, what)
values, err := overridden(raw, WithDefaults(m, layers), what)
if err != nil {
return nil, fmt.Errorf("%s: %w", what, err)
}
@@ -1926,7 +1926,7 @@ func (r Resolution) servedOnThisMachine(provision string, with Rendering) (map[s
// one, so keep looking rather than concluding from the first.
continue
}
settled, err := Settle(serves, with.Settings[m.Module])
settled, err := Settle(serves, WithDefaults(m, with.Settings[m.Module]))
if err != nil {
return nil, false, fmt.Errorf("%s serving %s: %w", m.Module, provision, err)
}
+9
View File
@@ -458,6 +458,13 @@ type Manifest struct {
// (novox/hq ADR 0201). Not history — that is an event — and never a secret, sealed or not.
State []StateDeclaration `json:"state,omitempty"`
// Settings are the defaults this module gives its settings (novox/hq ADR 0262): each key a file,
// a contribution or a served fact asks for as `${setting:<key>}`, its default, and why that
// default. Only a preference has one — a font size, a width, a number of workers — and a value
// that is inherently the operator's (a domain, an identity, a secret) is declared nowhere here and
// stays refused by name until a layer sets it. The mesh's layer, then the node's, override it.
Settings map[string]SettingDeclaration `json:"settings,omitempty"`
// Data is every kind of data this module keeps — its own, by directory, and what it keeps for
// its consumers, by provision — each with a class the mesh protects and watches it by (novox/hq
// ADR 0233). One list: the backup holder's lines, the bindings that do not move, what an
@@ -1541,6 +1548,8 @@ func ParseManifest(raw []byte) (Manifest, error) {
problems = append(problems, EventProblems(m)...)
// And what it may call its state, and whose it may read (state.go, novox/hq ADR 0201).
problems = append(problems, StateProblems(m)...)
// And the defaults it gives its settings (setting_defaults.go, novox/hq ADR 0262).
problems = append(problems, SettingProblems(m)...)
wellFormed := true
for _, c := range m.Claims {
if !name.MatchString(c.Name) {
+172
View File
@@ -0,0 +1,172 @@
package catalogue
import (
"fmt"
"sort"
"strings"
)
// A preference has a default; the operator's own value has none (novox/hq ADR 0262, extending ADR
// 0112 and taking the default half of ADR 0164).
//
// `${setting:<key>}` was refused whenever no layer set the key, and `settings set` refused a key no
// file asked for yet. Together they meant a running module could never gain a setting: the file that
// asks for it fails to compose until somebody sets it, and nobody can set it until the file asks.
// A definition may now give a key a default, with why, when the value is a **preference** — a font
// size, a width, a number of workers: something true of the software that a machine may tune. A
// value that is inherently the operator's — a domain, a public name, an identity, a secret — gets no
// default and stays refused by name until a layer sets it, which is what ADR 0112 and ADR 0155 were
// written for.
//
// The default is the lowest layer. The mesh's layer, then the node's, override it. It fills only
// `${setting:<key>}`: a mergeable file's content already is its defaults, and laying a default over
// it would reach every mergeable file of the module (novox/hq issue 168).
// SettingDeclaration is one key's default, as the definition gives it.
type SettingDeclaration struct {
// Kind says what sort of value this is. `preference` is the only kind with a default; it is
// stated rather than assumed so a reviewer sees the claim being made.
Kind string `json:"kind"`
// Default is the value when no layer sets the key: a string, a number or a boolean.
Default any `json:"default"`
// Why this default: one sentence for whoever wonders whether to change it.
Why string `json:"why"`
}
// KindPreference is the kind of a setting that may have a default.
const KindPreference = "preference"
// DefaultLayer is what the layer of a module's own defaults is called where a value's source is said.
const DefaultLayer = "default"
// meshWords are the settings keys the mesh reads itself; a module declares none of them.
var meshWords = map[string]bool{
PortsSetting: true, ExposeSetting: true, ReachSetting: true, EndpointsSetting: true,
PlacesSetting: true, AccessesSetting: true, NetworksSetting: true,
}
// operatorsOwn are words that, in a key's name, say its value is the operator's and never a
// preference: a default for one would be the literal ADR 0112 removed from definitions.
var operatorsOwn = []string{
"domain", "host", "issuer", "url", "email", "mail", "address", "identity", "login", "user",
"account", "password", "secret", "token", "credential",
}
// SettingProblems is every way a definition's setting defaults are wrong, in its own words.
func SettingProblems(m Manifest) []string {
if len(m.Settings) == 0 {
return nil
}
used := settingKeysUsedBy(m)
var problems []string
for _, key := range sortedSettingKeys(m.Settings) {
d := m.Settings[key]
say := func(format string, args ...any) {
problems = append(problems, fmt.Sprintf("%s's setting %q: ", m.Module, key)+fmt.Sprintf(format, args...))
}
if !settingRef.MatchString("${setting:" + key + "}") {
say("not a usable key: lower-case letters, digits, dots, dashes and underscores")
continue
}
if meshWords[key] {
say("the mesh reads %q itself, and a module gives it no default", key)
continue
}
if d.Kind != KindPreference {
say("kind is %q, and only a %q has a default (novox/hq ADR 0262)", d.Kind, KindPreference)
}
for _, word := range operatorsOwn {
if strings.Contains(key, word) {
say("a key naming %q is the operator's value, and has no default — it is the "+
"assignment's, never the definition's (novox/hq ADR 0112, ADR 0262)", word)
break
}
}
switch v := d.Default.(type) {
case string:
if strings.TrimSpace(v) == "" {
say("an empty default is no default; give the value, or declare nothing")
}
case float64, bool:
case nil:
say("no default: a key without one is the operator's, and is not declared here")
default:
say("a default is a string, a number or a boolean, and this is %T", d.Default)
}
if strings.TrimSpace(d.Why) == "" {
say("no why: say in one sentence why this default")
}
if !used[key] {
say("nothing asks for ${setting:%s}, so the default reaches nothing", key)
}
}
return problems
}
// Defaults is the layer a module's own defaults make, or nothing when it gives none.
func Defaults(m Manifest) (Layer, bool) {
if len(m.Settings) == 0 {
return Layer{}, false
}
values := map[string]any{}
for key, d := range m.Settings {
if d.Default != nil {
values[key] = d.Default
}
}
return Layer{From: DefaultLayer, Values: values}, len(values) > 0
}
// WithDefaults is a module's layers with its defaults under them, for filling `${setting:<key>}`.
func WithDefaults(m Manifest, layers []Layer) []Layer {
d, has := Defaults(m)
if !has {
return layers
}
return append([]Layer{d}, layers...)
}
// SettingSource is one key's effective value and the layer it came from.
type SettingSource struct {
Key string
Value any
// From is DefaultLayer, MeshWideLayer or the node's name.
From string
// Default is the module's default, when it gives one.
Default any
HasDefault bool
}
// Effective is every key a module gives a default or a layer sets, with its value and where it came
// from: the default, then the mesh's layer, then the node's — later wins.
func Effective(m Manifest, layers []Layer) []SettingSource {
byKey := map[string]*SettingSource{}
for key, d := range m.Settings {
byKey[key] = &SettingSource{Key: key, Value: d.Default, From: DefaultLayer, Default: d.Default, HasDefault: true}
}
for _, layer := range layers {
for key, v := range layer.Values {
s, ok := byKey[key]
if !ok {
s = &SettingSource{Key: key}
byKey[key] = s
}
s.Value, s.From = v, layer.From
}
}
out := make([]SettingSource, 0, len(byKey))
for _, s := range byKey {
out = append(out, *s)
}
sort.Slice(out, func(i, j int) bool { return out[i].Key < out[j].Key })
return out
}
func sortedSettingKeys(in map[string]SettingDeclaration) []string {
keys := make([]string, 0, len(in))
for k := range in {
keys = append(keys, k)
}
sort.Strings(keys)
return keys
}
+195
View File
@@ -0,0 +1,195 @@
package catalogue
import (
"encoding/json"
"strings"
"testing"
)
// A preference has a default in the definition; the mesh's layer, then the node's, override it; a key
// with a default is not stray; and a value that is the operator's still has none (novox/hq ADR 0262).
func notifier() Manifest {
return Manifest{Module: "notifier",
Settings: map[string]SettingDeclaration{
"font-size": {Kind: KindPreference, Default: float64(10), Why: "readable at a scale of one"},
"width": {Kind: KindPreference, Default: float64(250), Why: "fits a title of forty characters"},
},
Resources: []map[string]any{{"id": "configuration", "type": "file", "path": "/x/notifierrc",
"content": "font = Inter ${setting:font-size}\nwidth = ${setting:width}\n"}},
}
}
func parsed(t *testing.T, m Manifest) error {
t.Helper()
raw, err := json.Marshal(m)
if err != nil {
t.Fatal(err)
}
_, err = ParseManifest(raw)
return err
}
func TestAnUnsetPreferenceTakesItsDefault(t *testing.T) {
m := notifier()
for _, layers := range [][]Layer{nil, {{From: MeshWideLayer, Values: map[string]any{}}}} {
file := map[string]any{}
for k, v := range m.Resources[0] {
file[k] = v
}
if err := settingInto(file, WithDefaults(m, layers), m.Module); err != nil {
t.Fatal(err)
}
if file["content"] != "font = Inter 10\nwidth = 250\n" {
t.Fatalf("filled as %q", file["content"])
}
}
if err := JudgeSettings(m, nil, false); err != nil {
t.Fatalf("a module whose every key has a default does not compose with no layer: %v", err)
}
}
func TestTheNodeOverTheMeshOverTheDefault(t *testing.T) {
m := notifier()
layers := []Layer{
{From: MeshWideLayer, Values: map[string]any{"width": float64(300)}},
{From: "laptop", Values: map[string]any{"font-size": float64(13), "width": float64(340)}},
}
file := map[string]any{"type": "file", "content": m.Resources[0]["content"]}
if err := settingInto(file, WithDefaults(m, layers), m.Module); err != nil {
t.Fatal(err)
}
if file["content"] != "font = Inter 13\nwidth = 340\n" {
t.Fatalf("filled as %q", file["content"])
}
file = map[string]any{"type": "file", "content": m.Resources[0]["content"]}
if err := settingInto(file, WithDefaults(m, layers[:1]), m.Module); err != nil {
t.Fatal(err)
}
if file["content"] != "font = Inter 10\nwidth = 300\n" {
t.Fatalf("the mesh's layer over the default filled as %q", file["content"])
}
}
// Composed for a machine, the way a push writes it: the default reaches the file, and the node's
// layer overrides it.
func TestAComposedMachineGetsTheDefaultAndTheNodesValue(t *testing.T) {
r := anAdoptedAnchor()
r.Modules = append(r.Modules, notifier())
with := anchorRendering(false)
composed, err := r.Compose(with)
if err != nil {
t.Fatal(err)
}
if why, left := composed.LeftOut["notifier"]; left {
t.Fatalf("the notifier was left out: %s", why)
}
if c := byID(composed.Resources)["notifier.configuration"]["content"]; c != "font = Inter 10\nwidth = 250\n" {
t.Fatalf("composed with no layer as %q", c)
}
with.Settings["notifier"] = []Layer{{From: "anchor", Values: map[string]any{"font-size": float64(13)}}}
if composed, err = r.Compose(with); err != nil {
t.Fatal(err)
}
if c := byID(composed.Resources)["notifier.configuration"]["content"]; c != "font = Inter 13\nwidth = 250\n" {
t.Fatalf("composed with the node's font size as %q", c)
}
}
// Setting a key the module gives a default is not refused as reaching nothing: it overrides the
// default, which is how a running module gains a setting with no gap between.
func TestAKeyWithADefaultIsNotStray(t *testing.T) {
m := notifier()
m.Resources[0]["content"] = "font = Inter ${setting:font-size}\nwidth = ${setting:width}\n"
stray := UnusedSettings(m, []Layer{{From: "laptop", Values: map[string]any{"font-size": float64(13), "colour": "red"}}})
joined := strings.Join(stray, "; ")
if strings.Contains(joined, `"font-size"`) || !strings.Contains(joined, `"colour"`) {
t.Fatalf("stray: %s", joined)
}
}
// A default fills ${setting:…} only: it never becomes a key of a mergeable file (issue 168).
func TestADefaultIsNotMergedIntoAJSONFile(t *testing.T) {
m := notifier()
m.Resources = append(m.Resources, map[string]any{"id": "other", "type": "file", "path": "/x/other.json",
"merge": MergeJSON, "content": `{"keep": 1}`})
out, err := ApplySettings(m.Resources[1], WithDefaults(m, nil))
if err != nil {
t.Fatal(err)
}
if strings.Contains(out["content"].(string), "font-size") {
t.Fatalf("a default reached a mergeable file: %s", out["content"])
}
}
// A value that is the operator's has no default: no layer setting it is refused by name, as before.
func TestAnOperatorsValueWithoutADefaultIsStillRefused(t *testing.T) {
m := notifier()
m.Resources[0]["content"] = "font = Inter ${setting:font-size}\nwidth = ${setting:width}\nfrom = ${setting:domain}\n"
err := JudgeSettings(m, nil, false)
if err == nil || !strings.Contains(err.Error(), "${setting:domain}") {
t.Fatalf("judged %v", err)
}
if strings.Contains(err.Error(), "font-size") {
t.Fatalf("a default was named as set today: %v", err)
}
}
func TestTheParserTakesAPreferenceAndRefusesTheRest(t *testing.T) {
if err := parsed(t, notifier()); err != nil {
t.Fatalf("a preference with a default was refused: %v", err)
}
for _, c := range []struct {
name string
key string
d SettingDeclaration
refuse string
}{
{"no kind", "font-size", SettingDeclaration{Default: float64(10), Why: "x"}, `only a "preference" has a default`},
{"another kind", "font-size", SettingDeclaration{Kind: "operator", Default: float64(10), Why: "x"}, `only a "preference"`},
{"no default", "font-size", SettingDeclaration{Kind: KindPreference, Why: "x"}, "no default"},
{"an empty default", "font-size", SettingDeclaration{Kind: KindPreference, Default: " ", Why: "x"}, "an empty default"},
{"an object", "font-size", SettingDeclaration{Kind: KindPreference, Default: map[string]any{"a": 1.0}, Why: "x"}, "a string, a number or a boolean"},
{"no why", "font-size", SettingDeclaration{Kind: KindPreference, Default: float64(10)}, "no why"},
{"the operator's", "mail-domain", SettingDeclaration{Kind: KindPreference, Default: "example.tld", Why: "x"}, "is the operator's value"},
{"a secret", "api-token", SettingDeclaration{Kind: KindPreference, Default: "x", Why: "x"}, "is the operator's value"},
{"the mesh's word", PortsSetting, SettingDeclaration{Kind: KindPreference, Default: float64(1), Why: "x"}, "the mesh reads"},
{"read by nothing", "height", SettingDeclaration{Kind: KindPreference, Default: float64(300), Why: "x"}, "reaches nothing"},
} {
m := notifier()
m.Resources[0]["content"] = m.Resources[0]["content"].(string) + "x = ${setting:" + c.key + "}\n"
if c.name == "read by nothing" {
m.Resources[0]["content"] = "font = Inter ${setting:font-size}\nwidth = ${setting:width}\n"
}
m.Settings[c.key] = c.d
err := parsed(t, m)
if err == nil || !strings.Contains(err.Error(), c.refuse) {
t.Errorf("%s: parsed %v, want %q", c.name, err, c.refuse)
}
}
}
func TestEveryValueSaysWhereItCameFrom(t *testing.T) {
m := notifier()
m.Resources[0]["content"] = m.Resources[0]["content"].(string) + "x = ${setting:position}\n"
got := Effective(m, []Layer{
{From: MeshWideLayer, Values: map[string]any{"width": float64(300), "position": "top-right"}},
{From: "laptop", Values: map[string]any{"font-size": float64(13)}},
})
want := map[string]string{"font-size": "laptop", "position": MeshWideLayer, "width": MeshWideLayer}
if len(got) != 3 {
t.Fatalf("effective: %+v", got)
}
for _, s := range got {
if s.From != want[s.Key] {
t.Errorf("%s from %q, want %q", s.Key, s.From, want[s.Key])
}
}
if got[0].Key != "font-size" || got[0].Value != float64(13) || got[0].Default != float64(10) {
t.Fatalf("font-size: %+v", got[0])
}
only := Effective(m, nil)
if only[0].From != DefaultLayer || only[0].Value != float64(10) {
t.Fatalf("with no layer: %+v", only)
}
}
+8 -3
View File
@@ -40,8 +40,9 @@ func settingsUsed(content string) []string {
// settingInto fills a file's ${setting:…} placeholders from the layers over a module.
//
// The last layer setting a key wins, which is the node's over the mesh's — the same order settle
// applies to a mergeable file. A value that is not a string is written the way a program would read
// The last layer setting a key wins, which is the node's over the mesh's over the module's own
// default (novox/hq ADR 0262) — the same order settle applies to a mergeable file. The caller lays
// the defaults under the layers with WithDefaults. A value that is not a string is written the way a program would read
// it (a number without a trailing .000000, a boolean as true/false).
func settingInto(resource map[string]any, layers []Layer, module string) error {
if fmt.Sprint(resource["type"]) != "file" {
@@ -56,7 +57,8 @@ func settingInto(resource map[string]any, layers []Layer, module string) error {
if !set {
return fmt.Errorf(
"%s has a file that says ${setting:%s}, and nothing sets %q for it — an operator's "+
"value is the assignment's, never the definition's (novox/hq ADR 0112): "+
"value is the assignment's, never the definition's (novox/hq ADR 0112), and only a "+
"preference has a default in the definition (ADR 0262): "+
"`settings set %s <file>` with {%q: …}%s",
module, key, key, module, key, orNoSettings(layers))
}
@@ -80,6 +82,9 @@ func settingValue(layers []Layer, key string) (any, bool) {
func orNoSettings(layers []Layer) string {
var keys []string
for _, l := range layers {
if l.From == DefaultLayer {
continue
}
for k := range l.Values {
keys = append(keys, k)
}
+12 -1
View File
@@ -151,6 +151,12 @@ func settle(base map[string]any, layers []Layer, protected map[string]bool, what
map[string]any, error) {
merged := deepCopy(base)
for _, layer := range layers {
if layer.From == DefaultLayer {
// A module's own defaults fill ${setting:<key>} only (novox/hq ADR 0262). A mergeable
// file's content already is its defaults, and a contribution or served fact declares its
// own; laid on here, a default would reach every mergeable file of its module (issue 168).
continue
}
for key, value := range layer.Values {
if key == PortsSetting {
// Where the machine puts a port is the mesh's to apply, not a value for a file or
@@ -224,6 +230,11 @@ func UnusedSettings(m Manifest, layers []Layer) []string {
}
lands := settingKeysUsedBy(m)
// A key the module gives a default reaches what asks for it (novox/hq ADR 0262): setting it
// overrides the default, which is how a running module gains a setting without a gap between.
for key := range m.Settings {
lands[key] = true
}
for _, values := range m.Contributes {
for key := range values {
lands[key] = true
@@ -420,7 +431,7 @@ func JudgeSettings(m Manifest, layers []Layer, adopted bool) error {
for k, v := range settled {
copied[k] = v
}
if err := settingInto(copied, layers, m.Module); err != nil {
if err := settingInto(copied, WithDefaults(m, layers), m.Module); err != nil {
return err
}
}
+3 -1
View File
@@ -249,7 +249,9 @@ var ControllerVerbs = []Verb{
}, nil, "adopted"),
Replaces: []string{"mesh-controller token issue", "wg set"}},
{Name: "settings", Description: "Read or set what an assignment is configured with: a module's settings for the whole " +
"mesh, or for one machine. Without values or clear, answers the layer as it stands — read it before setting it; " +
"mesh, or for one machine. Without values or clear, answers the layer as it stands — read it before setting it — " +
"then every value the module gives a default or a layer sets, with where it comes from: the module's default, " +
"the mesh, or the machine (novox/hq ADR 0262); " +
"with history, the layers it replaced. Setting replaces that layer whole and answers each key it adds (+), " +
"changes (~) and removes (-); a set that would remove a key is refused unless replace says it is meant " +
"(novox/hq ADR 0217). Takes effect at the next push. With clear, removes the layer and the module is back to " +