Bring F3 up to its release branch, with both sides' store tests (part 8 of 14)
mesh/delivery superseded: a newer head of the same pull request
mesh/merge-gate head 2d7ae1af8ffc merged into trunk fcdecb436117 — fail: head 2d7ae1af8ffc does not merge cleanly into trunk fcdecb436117: in conflict cmd/…
mesh/repo-check head 2d7ae1af8ffc merged into trunk fcdecb436117 — fail: head 2d7ae1af8ffc does not merge cleanly into trunk fcdecb436117: in conflict cmd/…

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-14 18:48:59 +00:00
co-authored by Claude Opus 5.5
parent 2e9b732914
commit 2d7ae1af8f
5 changed files with 511 additions and 9 deletions
+453
View File
@@ -0,0 +1,453 @@
package catalogue
import (
"bytes"
"encoding/json"
"errors"
"strings"
"testing"
)
// A module declares its config (novox/hq ADR 0348 §1, §4, §5, §6, §8, §11; design 57 package K1, issue 660):
// one registration refusal per rule of design 57's K1, the old names read as aliases, and a module rewritten
// from settings to config composing byte-identically.
// inConfigForm is a module in the config form that registers: every value typed, described, defaulted or
// required, placed in a file or read at runtime, and its mergeable file naming its keys.
func inConfigForm() map[string]any {
return map[string]any{
"module": "notifier",
"version": "1",
"config": map[string]any{
"font-size": map[string]any{"type": "number", "description": "the notification's font size, in points",
"minimum": 6, "maximum": 40, "default": 10},
"width": map[string]any{"type": "number", "description": "the notification's width, in pixels",
"default": 250},
"position": map[string]any{"type": "string", "description": "the corner it shows in, a word",
"enum": []any{"top-right", "top-left"}, "default": "top-right"},
"theme": map[string]any{"type": "string", "description": "the colour theme, a lower-case name",
"pattern": "[a-z]+", "default": "dark"},
"history": map[string]any{"type": "number", "description": "how many notifications it keeps, a count",
"default": 20, "runtime": true},
"sender": map[string]any{"type": "string", "description": "the address it mails a digest from",
"required": true},
},
"resources": []any{
map[string]any{"id": "configuration", "type": "file", "path": "/x/notifierrc",
"content": "font = Inter ${config:font-size}\nwidth = ${config:width}\nfrom = ${config:sender}\n"},
map[string]any{"id": "rules", "type": "file", "path": "/x/rules.json", "merge": "json",
"takes-config": []any{"position", "theme"}, "content": "{\"position\": \"top-right\"}\n"},
map[string]any{"id": "notifier", "type": "service", "unit": "notifier.service",
"reload-on": []any{"configuration"}, "restart-on": []any{"rules"}},
},
}
}
// declaration is one value of inConfigForm's config, to change in place.
func declaration(m map[string]any, key string) map[string]any {
return m["config"].(map[string]any)[key].(map[string]any)
}
func resourceOf(m map[string]any, id string) map[string]any {
for _, r := range m["resources"].([]any) {
if r.(map[string]any)["id"] == id {
return r.(map[string]any)
}
}
return nil
}
func parseRaw(t *testing.T, m map[string]any) (Manifest, error) {
t.Helper()
raw, err := json.Marshal(m)
if err != nil {
t.Fatal(err)
}
return ParseManifest(raw)
}
// refused is that the manifest is refused with every one of these words.
func refused(t *testing.T, m map[string]any, words ...string) {
t.Helper()
_, err := parseRaw(t, m)
if err == nil {
t.Fatalf("registered; wanted a refusal saying %q", words)
}
for _, w := range words {
if !strings.Contains(err.Error(), w) {
t.Fatalf("refused without %q:\n%v", w, err)
}
}
}
func TestAModuleInTheConfigFormRegisters(t *testing.T) {
m, err := parseRaw(t, inConfigForm())
if err != nil {
t.Fatal(err)
}
if !m.DeclaresConfig() {
t.Fatal("a module declaring config is not read as in the config form")
}
d := m.Settings["font-size"]
if d.Type != "number" || d.Description == "" || d.Minimum == nil || *d.Minimum != 6 || d.Default != float64(10) {
t.Fatalf("font-size read as %+v", d)
}
if s := m.Settings["sender"]; !s.Required || s.Default != nil {
t.Fatalf("sender read as %+v", s)
}
}
// Rule 1: a value without type or description.
func TestConfigRefusesAValueWithoutTypeOrDescription(t *testing.T) {
m := inConfigForm()
delete(declaration(m, "width"), "type")
refused(t, m, `notifier's config value "width"`, "no type")
m = inConfigForm()
declaration(m, "width")["type"] = "colour"
refused(t, m, `"width"`, `type "colour"`)
m = inConfigForm()
delete(declaration(m, "width"), "description")
refused(t, m, `"width"`, "no description")
}
// Rule 2: a value with neither a default nor required.
func TestConfigRefusesAValueWithNeitherDefaultNorRequired(t *testing.T) {
m := inConfigForm()
delete(declaration(m, "width"), "default")
refused(t, m, `"width"`, "neither a default nor required")
m = inConfigForm()
declaration(m, "sender")["default"] = "noreply@example.org"
refused(t, m, `"sender"`, "both a default and required")
}
// Rule 3: a default failing its validation.
func TestConfigRefusesADefaultFailingItsValidation(t *testing.T) {
for _, c := range []struct {
key string
value any
said string
}{
{"font-size", 4, "below its minimum"},
{"font-size", 41, "above its maximum"},
{"font-size", "10", "not a number"},
{"position", "bottom", "not one of"},
{"theme", "Dark", "does not match"},
} {
m := inConfigForm()
declaration(m, c.key)["default"] = c.value
refused(t, m, `"`+c.key+`"`, c.said)
}
}
// Rule 4: an object or a list not trusted.
func TestConfigRefusesAnObjectOrListNotTrusted(t *testing.T) {
for _, kind := range []string{"object", "list"} {
m := inConfigForm()
def := any(map[string]any{"a": 1})
if kind == "list" {
def = []any{"a"}
}
m["config"].(map[string]any)["rules"] = map[string]any{"type": kind, "description": "the rules, as " + kind,
"default": def, "runtime": true}
refused(t, m, `"rules"`, "is "+kind+" and not trusted")
m["config"].(map[string]any)["rules"].(map[string]any)["trusted"] = true
if _, err := parseRaw(t, m); err != nil {
t.Fatalf("a trusted %s is refused: %v", kind, err)
}
}
}
// withPredefined is the list of predefined keys as package K2 will give it, for the length of a test.
func withPredefined(t *testing.T, keys map[string]PredefinedKey) {
t.Helper()
was := predefinedKeys
predefinedKeys = keys
t.Cleanup(func() { predefinedKeys = was })
}
// Rule 5: a predefined key not on the list, or of another type. Until the list is present (package K2), a value
// naming one is refused saying so.
func TestConfigRefusesAPredefinedKeyNotOnTheListOrOfAnotherType(t *testing.T) {
m := inConfigForm()
declaration(m, "font-size")["predefined"] = "font-size"
refused(t, m, `"font-size"`, "the list of predefined keys", "not yet present")
withPredefined(t, map[string]PredefinedKey{"font-size": {Type: "number"}, "accent": {Type: "string"}})
if _, err := parseRaw(t, m); err != nil {
t.Fatalf("a value falling back to a listed key of its type is refused: %v", err)
}
m = inConfigForm()
declaration(m, "font-size")["predefined"] = "font-scale"
refused(t, m, `"font-size"`, `"font-scale"`, "not on the list")
m = inConfigForm()
declaration(m, "font-size")["predefined"] = "accent"
refused(t, m, `"font-size"`, `"accent"`, "is a string")
}
// Rule 6: an untrusted value falling back to a trusted predefined key.
func TestConfigRefusesAnUntrustedValueFallingBackToATrustedKey(t *testing.T) {
m := inConfigForm()
declaration(m, "theme")["predefined"] = "theme"
refused(t, m, `"theme"`, "not yet present")
withPredefined(t, map[string]PredefinedKey{"theme": {Type: "string", Trusted: true}})
refused(t, m, `"theme"`, "trusted", "is not")
declaration(m, "theme")["trusted"] = true
if _, err := parseRaw(t, m); err != nil {
t.Fatalf("a trusted value falling back to a trusted key is refused: %v", err)
}
}
// Rule 7: a value neither placed in a template nor read at runtime.
func TestConfigRefusesAValueNeitherPlacedNorReadAtRuntime(t *testing.T) {
m := inConfigForm()
delete(declaration(m, "history"), "runtime")
refused(t, m, `"history"`, "${config:history}", `"runtime": true`)
// Taken by a mergeable file is placed.
m = inConfigForm()
if _, err := parseRaw(t, m); err != nil {
t.Fatal(err)
}
}
// Rule 8: a placeholder naming an undeclared value.
func TestConfigRefusesAPlaceholderNamingAnUndeclaredValue(t *testing.T) {
m := inConfigForm()
resourceOf(m, "configuration")["content"] = "font = ${config:font-size}\nwidth = ${config:width}\n" +
"from = ${config:sender}\ncolour = ${config:colour}\n"
refused(t, m, "${config:colour}", "declares no such value")
}
// Rule 9: a reload-on or restart-on naming a file the module does not declare.
func TestConfigRefusesAReloadOrRestartOnNamingAnUndeclaredFile(t *testing.T) {
m := inConfigForm()
resourceOf(m, "notifier")["reload-on"] = []any{"configuration", "dunstrc"}
refused(t, m, "reload-on", `"dunstrc"`, "declares no such")
m = inConfigForm()
resourceOf(m, "notifier")["restart-on"] = []any{"rule"}
refused(t, m, "restart-on", `"rule"`)
// Another module's file, and one the mesh makes for the module, are not the module's to declare.
m = inConfigForm()
resourceOf(m, "notifier")["restart-on"] = []any{"rules", "resolver.fact-zones", "secret-mail", "composed-jails"}
if _, err := parseRaw(t, m); err != nil {
t.Fatalf("refused: %v", err)
}
}
// Rule 10: a moved module's mergeable file not naming its keys under takes-config.
func TestConfigRefusesAMergeableFileNotNamingItsKeys(t *testing.T) {
m := inConfigForm()
delete(resourceOf(m, "rules"), "takes-config")
// position and theme are then placed nowhere; say they are read at runtime so only the file's rule speaks.
declaration(m, "position")["runtime"] = true
declaration(m, "theme")["runtime"] = true
refused(t, m, "rules", "names no takes-config")
}
// Rule 11: a mesh word, puid or pgid in takes-config.
func TestConfigRefusesAMeshWordOrAnAccountFactInTakesConfig(t *testing.T) {
for _, word := range []string{"ports", "expose", "reach", "endpoints", "places", "accesses", "networks", "puid", "pgid"} {
m := inConfigForm()
resourceOf(m, "rules")["takes-config"] = []any{"position", "theme", word}
refused(t, m, `"`+word+`"`, "takes-config")
}
}
// A module uses one form or the other, never both.
func TestAManifestMixingTheConfigAndSettingsFormsIsRefused(t *testing.T) {
m := inConfigForm()
resourceOf(m, "configuration")["content"] = "font = ${config:font-size}\nwidth = ${setting:width}\nfrom = ${config:sender}\n"
refused(t, m, "mixes", "${setting:", "config")
m = inConfigForm()
delete(resourceOf(m, "rules"), "takes-config")
resourceOf(m, "rules")["takes-settings"] = []any{"position", "theme"}
refused(t, m, "mixes", "takes-settings")
m = inConfigForm()
m["settings"] = map[string]any{"colour": map[string]any{"kind": "preference", "default": "red", "why": "red"}}
refused(t, m, "mixes", "settings")
}
// The settings form keeps its own declaration; the config form's has no kind or why.
func TestEachFormKeepsItsOwnDeclaration(t *testing.T) {
m := inConfigForm()
declaration(m, "width")["why"] = "fits a title"
refused(t, m, `"width"`, "why")
err := parsed(t, Manifest{Module: "notifier",
Settings: map[string]SettingDeclaration{"width": {Kind: KindPreference, Default: float64(250),
Why: "fits a title", Type: "number"}},
Resources: []map[string]any{{"id": "c", "type": "file", "path": "/x", "content": "${setting:width}"}}})
if err == nil || !strings.Contains(err.Error(), "under config") {
t.Fatalf("a typed value under settings: %v", err)
}
}
// What the mesh holds is what was written: a module in the config form is written back in it, and read again
// the same.
func TestAConfigManifestIsWrittenBackInItsOwnForm(t *testing.T) {
m, err := parseRaw(t, inConfigForm())
if err != nil {
t.Fatal(err)
}
raw, err := json.Marshal(m)
if err != nil {
t.Fatal(err)
}
for _, said := range []string{`"config":`, `${config:font-size}`, `"takes-config":`, `"required":true`} {
if !strings.Contains(string(raw), said) {
t.Fatalf("written back without %s: %s", said, raw)
}
}
for _, unsaid := range []string{`"settings":`, `${setting:`, `"takes-settings"`, `"kind"`, `"why"`, `"default":null`} {
if strings.Contains(string(raw), unsaid) {
t.Fatalf("written back with %s: %s", unsaid, raw)
}
}
again, err := ParseManifest(raw)
if err != nil {
t.Fatal(err)
}
twice, _ := json.Marshal(again)
if !bytes.Equal(raw, twice) {
t.Fatalf("read again it is written\n%s\nwhere it was\n%s", twice, raw)
}
}
// A module in the settings form is written back byte for byte as before.
func TestASettingsManifestIsWrittenBackAsBefore(t *testing.T) {
d := SettingDeclaration{Kind: KindPreference, Default: float64(10), Why: "readable"}
raw, err := json.Marshal(d)
if err != nil {
t.Fatal(err)
}
if string(raw) != `{"kind":"preference","default":10,"why":"readable"}` {
t.Fatalf("written as %s", raw)
}
}
// settingsDunst is the dunst module as it is written today; configDunst the same module rewritten from settings to
// config.
const settingsDunst = `{"module": "dunst", "version": "1",
"settings": {
"font-size": {"kind": "preference", "default": 10, "why": "10 points of Inter reads well on a screen of about 100 DPI"},
"width": {"kind": "preference", "default": 250, "why": "250 pixels fits a title of about forty characters at 10 points"}
},
"resources": [
{"id": "package", "type": "package", "package": "dunst"},
{"id": "configuration", "type": "file", "path": "/home/me/.config/dunst/dunstrc", "mode": "0644",
"content": "[global]\n font = Inter ${setting:font-size}\n width = ${setting:width}\n"},
{"id": "rules", "type": "file", "path": "/home/me/.config/dunst/rules.json", "merge": "json",
"takes-settings": [], "content": "{\"urgency\": \"low\"}\n"},
{"id": "notifier", "type": "service", "unit": "dunst.service", "reload-on": ["configuration"]}
]}`
const configDunst = `{"module": "dunst", "version": "1",
"config": {
"font-size": {"type": "number", "description": "the font size of a notification, in points", "minimum": 6,
"maximum": 40, "default": 10},
"width": {"type": "number", "description": "the width of a notification, in pixels", "minimum": 100,
"default": 250}
},
"resources": [
{"id": "package", "type": "package", "package": "dunst"},
{"id": "configuration", "type": "file", "path": "/home/me/.config/dunst/dunstrc", "mode": "0644",
"content": "[global]\n font = Inter ${config:font-size}\n width = ${config:width}\n"},
{"id": "rules", "type": "file", "path": "/home/me/.config/dunst/rules.json", "merge": "json",
"takes-config": [], "content": "{\"urgency\": \"low\"}\n"},
{"id": "notifier", "type": "service", "unit": "dunst.service", "reload-on": ["configuration"]}
]}`
// ${config:<key>} fills exactly as ${setting:<key>} does: the module rewritten from settings to config composes
// to the same bytes, with no layer, with the node's, and with the mesh's under the node's.
func TestAModuleRewrittenFromSettingsToConfigComposesByteIdentically(t *testing.T) {
composed := func(manifest string, layers []Layer) []byte {
t.Helper()
m, err := ParseManifest([]byte(manifest))
if err != nil {
t.Fatal(err)
}
r := anAdoptedAnchor()
r.Modules = append(r.Modules, m)
with := anchorRendering(false)
if layers != nil {
with.Settings["dunst"] = layers
}
c, err := r.Compose(with)
if err != nil {
t.Fatal(err)
}
if why, left := c.LeftOut["dunst"]; left {
t.Fatalf("dunst was left out: %s", why)
}
raw, err := json.Marshal(c)
if err != nil {
t.Fatal(err)
}
return raw
}
for _, layers := range [][]Layer{
nil,
{{From: "anchor", Values: map[string]any{"font-size": float64(13)}}},
{{From: MeshWideLayer, Values: map[string]any{"width": float64(300)}},
{From: "anchor", Values: map[string]any{"font-size": float64(12), "width": float64(320)}}},
} {
was, is := composed(settingsDunst, layers), composed(configDunst, layers)
if !bytes.Equal(was, is) {
t.Fatalf("composed differently with %v:\nsettings: %s\nconfig: %s", layers, was, is)
}
if !strings.Contains(string(is), "font = Inter 1") || strings.Contains(string(is), "${config:") {
t.Fatalf("not filled: %s", is)
}
}
}
// A key a mergeable file takes is a config value like every other: one not declared under config is refused by
// name (ADR 0348 §4, review of mesh-controller#320).
func TestConfigRefusesATakesConfigKeyNotDeclared(t *testing.T) {
m := inConfigForm()
resourceOf(m, "rules")["takes-config"] = []any{"position", "theme", "urgency"}
refused(t, m, `"urgency"`, "takes-config", "declares no such value under config")
}
// A required value nothing sets is refused at composition in the config form's words: ${config:<key>} and the
// config verb, never `settings set` (review of mesh-controller#320). The settings form's words are unchanged.
func TestAnUnsetRequiredValueIsRefusedInTheConfigFormsWords(t *testing.T) {
m, err := parseRaw(t, inConfigForm())
if err != nil {
t.Fatal(err)
}
r := anAdoptedAnchor()
r.Modules = append(r.Modules, m)
composed, err := r.Compose(anchorRendering(false))
if err != nil {
t.Fatal(err)
}
why, left := composed.LeftOut["notifier"]
if !left {
t.Fatal("a module whose required value nothing sets was not left out")
}
for _, said := range []string{"${config:sender}", `required config value "sender"`, "config verb"} {
if !strings.Contains(why, said) {
t.Fatalf("left out without %q: %s", said, why)
}
}
for _, unsaid := range []string{"${setting:", "settings set"} {
if strings.Contains(why, unsaid) {
t.Fatalf("left out in the settings form's words (%q): %s", unsaid, why)
}
}
var unset *UnsetSettingError
if err := unsetInConfigWords(settingInto(map[string]any{"type": "file", "content": "${setting:sender}"}, nil,
"notifier"), m); !errors.As(err, &unset) || unset.Setting != "sender" {
t.Fatalf("not typed as a value nobody gave: %v", err)
}
settingsForm := notifier()
settingsForm.Resources[0]["content"] = "from = ${setting:domain}\n"
err = unsetInConfigWords(settingInto(settingsForm.Resources[0], nil, "notifier"), settingsForm)
if err == nil || !strings.Contains(err.Error(), "`settings set notifier <file>`") {
t.Fatalf("the settings form's refusal changed: %v", err)
}
}
+1 -1
View File
@@ -173,7 +173,7 @@ func TestTheBackupHoldersLinesAreDerivedFromTheData(t *testing.T) {
{"id":"meta","path":"${dir:meta}","class":"rebuildable"}]},
"resources":[{"id":"meta","type":"directory","mode":"0700"}]}`)
facts := map[string]string{"account-home": "/home/op"}
with := Rendering{Settings: SettingsBy{"media": {{From: "node", Values: map[string]any{AccessesSetting: map[string]any{"films": "/tank/films"}}}}}}
with := Rendering{Settings: SettingsBy{"media": {LayerOf("node", map[string]any{AccessesSetting: map[string]any{"films": "/tank/films"}})}}}
backup, _, err := seatContributions([]Manifest{pg, agent, media}, Manifest{}, BackupSeat+":"+BackupKindBackup, with, facts, nil)
if err != nil {
t.Fatal(err)
+1 -1
View File
@@ -1003,7 +1003,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, WithDefaults(m, with.Settings[m.Module]), m.Module); err != nil {
if err := unsetInConfigWords(settingInto(copied, WithDefaults(m, with.Settings[m.Module]), m.Module), m); err != nil {
return nil, err
}
// **Placed before anything reads a path.** A pathless directory receives the path
@@ -165,16 +165,19 @@ func TestTwoConsumersOfOneProviderGetTheirOwnDerivation(t *testing.T) {
}
}
// An operator may still set what the provider serves, and the mesh still derives the rest: the
// setting is laid on first, then the consumer's half is filled.
// An operator may still set what the provider serves where its definition asks (${setting:…}), and
// the mesh still derives the rest: the setting is filled first, then the consumer's half. (Until
// novox/hq issue 696 the layer replaced the served bucket whole; a layer replaces no served key now.)
func TestASettingComposesWithADerivedValue(t *testing.T) {
r, err := Resolve(shelf(store(), files()), []string{"store", "files"}, reachable(), World{})
prefixed := store()
prefixed.Serves["s3-bucket"]["bucket"] = "${setting:bucket-prefix}${consumer:as:dns}"
r, err := Resolve(shelf(prefixed, files()), []string{"store", "files"}, reachable(), World{})
if err != nil {
t.Fatal(err)
}
out, err := r.Declaration(Rendering{
Settings: SettingsBy{"store": {{From: "the operator",
Values: map[string]any{"bucket": "team-${consumer:as:dns}"}}}},
Values: map[string]any{"bucket-prefix": "team-"}}}},
Grants: []Grant{{Provision: "s3-bucket", Consumer: "workstation", From: "files", Slug: "files",
Values: map[string]any{}, Sealed: "c2VhbGVk"}},
})
+49 -3
View File
@@ -530,6 +530,11 @@ type Manifest struct {
// 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"`
// forms is which of the two forms its config is written in, `config` or its alias `settings` (config.go,
// novox/hq ADR 0348 §1): the config form is read into Settings, a mergeable file's takes-settings and
// `${setting:<key>}`, and written back as it was read.
forms configForms
// unknown is the first key this manifest has that this controller does not know, at any depth, when
// it was read from the store (novox/hq ADR 0262): a manifest a newer controller registered.
// ParseManifest refuses it; the store's catalogue still loads, and the module is left out of every
@@ -1321,10 +1326,25 @@ type manifestFields Manifest
// strict read there failed the whole catalogue, and with it every plan and every send, the moment a
// controller was rolled back. UnknownField says what was set aside.
func (m *Manifest) UnmarshalJSON(raw []byte) error {
// The config form read into the settings form's fields, so both compose by one path (config.go).
raw, forms := toSettingsForm(raw)
var keys map[string]json.RawMessage
if err := json.Unmarshal(raw, &keys); err != nil {
return err
}
if settings, has := keys[SettingsField]; has && string(settings) != "null" {
forms.settings = append(forms.settings, SettingsField)
}
if config, has := keys[ConfigField]; has {
if string(config) != "null" {
forms.config = append(forms.config, ConfigField)
}
if _, both := keys[SettingsField]; !both {
keys[SettingsField] = config
}
// Beside `settings` it is refused by name (MixedFormProblems), never merged into it.
delete(keys, ConfigField)
}
plain := map[string]string{}
many := map[string]map[string]string{}
if secrets, ok := keys["secrets"]; ok && string(secrets) != "null" {
@@ -1453,6 +1473,7 @@ func (m *Manifest) UnmarshalJSON(raw []byte) error {
}
*m = Manifest(fields)
m.unknown = unknown
m.forms = forms
m.OptionalRequires = optionalRequires
if len(plain) > 0 {
m.Secrets = plain
@@ -1470,8 +1491,16 @@ func (m *Manifest) UnmarshalJSON(raw []byte) error {
}
// MarshalJSON writes `secrets` and `contributes` back in the shape they were read: single values,
// and objects of local names.
// and objects of local names; and its config in the form it was read in (config.go).
func (m Manifest) MarshalJSON() ([]byte, error) {
raw, err := m.marshalFields()
if err != nil || !m.DeclaresConfig() {
return raw, err
}
return toConfigForm(raw)
}
func (m Manifest) marshalFields() ([]byte, error) {
raw, err := json.Marshal(manifestFields(m))
if err != nil {
return nil, err
@@ -1783,8 +1812,14 @@ func ParseManifest(raw []byte) (Manifest, error) {
problems = append(problems, StateProblems(m)...)
// And what it may call its logs, and how large it may ask them to grow (logs.go, novox/hq ADR 0297).
problems = append(problems, LogProblems(m)...)
// And the defaults it gives its settings (setting_defaults.go, novox/hq ADR 0262).
problems = append(problems, SettingProblems(m)...)
// And the defaults it gives its settings (setting_defaults.go, novox/hq ADR 0262), or, written in the
// config form, the whole declaration of each value (config.go, novox/hq ADR 0348).
if m.DeclaresConfig() {
problems = append(problems, ConfigProblems(m)...)
} else {
problems = append(problems, SettingProblems(m)...)
problems = append(problems, TypedSettingProblems(m)...)
}
// And what it requires optionally, and the files it delivers only while bound (novox/hq ADR 0330).
problems = append(problems, OptionalProblems(m)...)
// And which setting keys each mergeable file takes (setting_takes.go, novox/hq ADR 0326).
@@ -1853,6 +1888,9 @@ func ParseManifest(raw []byte) (Manifest, error) {
}
}
problems = append(problems, m.Build.problems(m.Module)...)
// A file's text in a template file of the module's repository (novox/hq ADR 0349 §2): only a file, never beside
// content, only under files/, and packed by the module's build.
problems = append(problems, templateProblems(m)...)
problems = append(problems, undeliveredBundles(m)...)
// **What provides the artifact store cannot be delivered through it** (novox/hq 04-ISSUES/029).
//
@@ -2338,6 +2376,14 @@ func ParseManifest(raw []byte) (Manifest, error) {
problems = append(problems, healthProblems(m)...)
// And whether a container is busy (novox/hq ADR 0335), likewise.
problems = append(problems, busyProblems(m)...)
if m.DeclaresConfig() {
// Said in the words it was written in: the rules shared with the settings form say ${setting:<key>}
// and takes-settings.
for i, p := range problems {
problems[i] = inConfigWords(p)
}
problems = append(problems, MixedFormProblems(m)...)
}
if len(problems) > 0 {
sort.Strings(problems)
return Manifest{}, fmt.Errorf("this manifest cannot be used:\n - %s",