From 7530c2aeaccd793342524776bfee4b3fa2e54bbf Mon Sep 17 00:00:00 2001 From: mesh-admin Date: Wed, 14 Oct 2026 00:41:06 +0000 Subject: [PATCH] Read placement from a layer's Placement, never from its config values (issue 651) ADR 0348 clause 11: places, accesses and kept networks are placement, not config. Today's mergeable files still take them as said, so nothing composes differently. Co-Authored-By: Claude Opus 5.5 --- internal/catalogue/placement.go | 67 +++++++++++++++++++++++++- internal/catalogue/setting_defaults.go | 3 +- internal/catalogue/settings.go | 16 ++++-- 3 files changed, 80 insertions(+), 6 deletions(-) diff --git a/internal/catalogue/placement.go b/internal/catalogue/placement.go index 400c5b3f..bc8bf03d 100644 --- a/internal/catalogue/placement.go +++ b/internal/catalogue/placement.go @@ -34,6 +34,69 @@ const PlacesSetting = "places" // AccessesSetting is the settings key that says where a module's accesses are on this node, by id. const AccessesSetting = "accesses" +// **Placement is not config** (novox/hq ADR 0348 clause 11, issue 651). `places`, `accesses` and +// `networks` are facts of an assignment the controller decides, held beside the assignment and apart +// from its settings: a Layer carries them in Placement, never in Values, and only Places, AccessPlaces +// and KeptNetworks read them. A person still says them as keys of a layer through `settings`; LayerOf +// splits such a layer, so what they type is routed to the placement and nothing they do changes. + +// placementKeys are the keys of a layer that are placement, not config. +var placementKeys = map[string]bool{PlacesSetting: true, AccessesSetting: true, NetworksSetting: true} + +// SplitPlacement divides a layer as a person says it into its config values and its placement. Each is +// nil when it holds nothing; the values themselves are not copied. +func SplitPlacement(values map[string]any) (config, placement map[string]any) { + for key, value := range values { + if placementKeys[key] { + if placement == nil { + placement = map[string]any{} + } + placement[key] = value + continue + } + if config == nil { + config = map[string]any{} + } + config[key] = value + } + return config, placement +} + +// LayerOf is a layer as a person says it — config values and placement in one map — split into the two. +func LayerOf(from string, values map[string]any) Layer { + config, placement := SplitPlacement(values) + if config == nil { + config = map[string]any{} + } + return Layer{From: from, Values: config, Placement: placement} +} + +// Whole is the layer as a person says it: its values and its placement in one map, a new map. +func (l Layer) Whole() map[string]any { + out := make(map[string]any, len(l.Values)+len(l.Placement)) + for k, v := range l.Values { + out[k] = v + } + for k, v := range l.Placement { + out[k] = v + } + return out +} + +// asSaid is the layers with placement folded back into the values, for what reads a layer whole as it +// did while placement was kept in it: today's mergeable file, which takes every key (ADR 0326 §3), and +// the judgement that no value holds a line end. +func asSaid(layers []Layer) []Layer { + out := make([]Layer, len(layers)) + for i, l := range layers { + out[i] = l + if len(l.Placement) > 0 { + out[i] = Layer{From: l.From, Values: l.Whole(), Default: l.Default} + } + } + return out +} + // Placement is what an assignment says about one of a module's directories. type Placement struct { // Path is where the directory is on this machine. Absolute. @@ -208,7 +271,7 @@ func Places(m Manifest, layers []Layer) (map[string]Placement, error) { } out := map[string]Placement{} for _, layer := range layers { - raw, ok := layer.Values[PlacesSetting] + raw, ok := layer.Placement[PlacesSetting] if !ok { continue } @@ -278,7 +341,7 @@ func AccessPlaces(m Manifest, layers []Layer) (map[string]string, error) { } out := map[string]string{} for _, layer := range layers { - raw, ok := layer.Values[AccessesSetting] + raw, ok := layer.Placement[AccessesSetting] if !ok { continue } diff --git a/internal/catalogue/setting_defaults.go b/internal/catalogue/setting_defaults.go index 1bc72f67..3f158e02 100644 --- a/internal/catalogue/setting_defaults.go +++ b/internal/catalogue/setting_defaults.go @@ -207,7 +207,8 @@ func Effective(m Manifest, layers []Layer) []SettingSource { Default: d.Default, HasDefault: true} } for _, layer := range layers { - for key, v := range layer.Values { + // As the layer is said, placement too: `settings` answers it as a person set it. + for key, v := range layer.Whole() { s, ok := byKey[key] if !ok { s = &SettingSource{Key: key} diff --git a/internal/catalogue/settings.go b/internal/catalogue/settings.go index e20b61a2..45c992e8 100644 --- a/internal/catalogue/settings.go +++ b/internal/catalogue/settings.go @@ -37,6 +37,11 @@ type Layer struct { // Default marks the layer a module's own defaults make (novox/hq ADR 0262). Marked rather than // recognised by From, which is a node's name for a node's layer, and a node may be called anything. Default bool + // Placement is the assignment's placement at this layer's scope — `places`, `accesses`, `networks` + // — which the controller holds apart from settings (novox/hq ADR 0348 clause 11, issue 651): read + // by Places, AccessPlaces and KeptNetworks, and never from Values. LayerOf splits a layer as a person + // says it into the two. + Placement map[string]any } // ApplySettings produces a resource's final content from the module's own and the layers over it. @@ -45,6 +50,10 @@ type Layer struct { // module first, then this node's. A node that differs from the rest is expressed by differing, // rather than by repeating everything the rest already say. func ApplySettings(resource map[string]any, layers []Layer) (map[string]any, error) { + // Today's form reaches a mergeable file with every key of the layer as it was said, placement too + // (novox/hq ADR 0326 §3, ADR 0348 clause 11: a module not yet moved keeps it), so a file composes + // byte for byte as it did while placement was kept in the layer. + layers = asSaid(layers) how, _ := resource["merge"].(string) taken, names := takenKeys(resource) if how == "" { @@ -395,7 +404,7 @@ func UnusedSettings(m Manifest, layers []Layer) []string { } var unused []string for _, layer := range layers { - for key := range layer.Values { + for key := range layer.Whole() { if lands[key] { continue } @@ -483,7 +492,7 @@ func KeptNetworks(m Manifest, layers []Layer, adopted bool) (map[string][]string } out := map[string][]string{} for _, layer := range layers { - raw, ok := layer.Values[NetworksSetting] + raw, ok := layer.Placement[NetworksSetting] if !ok { continue } @@ -543,7 +552,8 @@ var networkName = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9_.-]*$`) // refused where it is stored (SetSettings, with UnusedSettings) and said where a plan is read, // and never costs a module its place. func JudgeSettings(m Manifest, layers []Layer, adopted bool) error { - if err := settingsHoldOneLine(m.Module, layers); err != nil { + // Placement is judged for line ends as it always was: a path is written into files as well. + if err := settingsHoldOneLine(m.Module, asSaid(layers)); err != nil { return err } // Which keys each mergeable file takes is the definition's to say right (novox/hq ADR 0326);