From bfc3883af980d767ea3d889aa72e7a236686a25b Mon Sep 17 00:00:00 2001 From: jochen Date: Sat, 24 Oct 2026 14:57:10 +0200 Subject: [PATCH] =?UTF-8?q?Read=20a=20manifest's=20fallback=20to=20a=20pre?= =?UTF-8?q?defined=20key=20in=20the=20composer=20as=20the=20indexer=20read?= =?UTF-8?q?s=20it,=20or=20against=20the=20list=20a=20request=20or=20-catal?= =?UTF-8?q?ogue=20gives:=20a=20run=20never=20holds=20the=20catalogue's=20p?= =?UTF-8?q?redefined.json,=20and=20the=20composition=20refused=20pacman=20?= =?UTF-8?q?and=20xterm=20for=20it=20(ADR=200348=20=C2=A77)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go/cmd/mesh-compose/main.go | 10 +++++++++ go/cmd/mesh-compose/main_test.go | 38 +++++++++++++++++++++++++++++++- go/composer/composer.go | 23 +++++++++++++++---- go/composer/composer_test.go | 31 ++++++++++++++++++++++++++ 4 files changed, 97 insertions(+), 5 deletions(-) diff --git a/go/cmd/mesh-compose/main.go b/go/cmd/mesh-compose/main.go index cc0211d..3168bde 100644 --- a/go/cmd/mesh-compose/main.go +++ b/go/cmd/mesh-compose/main.go @@ -28,6 +28,7 @@ import ( "sort" "strings" + "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/composer" ) @@ -96,6 +97,15 @@ func run(args []string, stdin io.Reader, stdout, stderr io.Writer) int { req.Cases[0].Manifests[name] = m } } + // The catalogue's list of predefined keys is the one its manifests' fallbacks are judged against (ADR 0348 §7). + if req.Predefined == nil { + list, err := catalogue.ReadPredefined(*catalogueDir) + if err != nil { + fmt.Fprintln(stderr, "mesh-compose:", err) + return 1 + } + req.Predefined = list + } } req.Machines = append(req.Machines, machines...) if *depth != "" { diff --git a/go/cmd/mesh-compose/main_test.go b/go/cmd/mesh-compose/main_test.go index a878b74..c68ff93 100644 --- a/go/cmd/mesh-compose/main_test.go +++ b/go/cmd/mesh-compose/main_test.go @@ -114,7 +114,8 @@ func TestTheCommandAnswersWhatTheLinkedCompositionAnswers(t *testing.T) { in := meshOf() in.Catalogue = map[string]catalogue.Manifest{} for name, m := range r.Cases[0].Manifests { - parsed, err := catalogue.ParseManifest(m.Raw) + // A request without a list of predefined keys reads a fallback as the indexer does (Request.Predefined). + parsed, err := catalogue.ParseManifestForInputs(m.Raw) got := v.Cases[0].Manifests[name] if (err == nil) != got.Accepted || (err != nil && err.Error() != got.Refused) { t.Errorf("%s: the manifest of %s: linked %v, command %+v", depth, name, err, got) @@ -309,3 +310,38 @@ func TestTheCommandRunsWithNoNetwork(t *testing.T) { sort.Strings(names) t.Logf("composed with no network: %v", names) } + +// -catalogue judges a fallback to a predefined key against the predefined.json at the checkout's root (ADR 0348 §7): +// xterm (mesh-catalog at 835bf22b) falls back to "font-size", accepted where the root lists it and refused where the +// root's list does not. +func TestACatalogueCheckoutJudgesFallbacksAgainstItsPredefinedKeys(t *testing.T) { + raw, err := os.ReadFile("../../catalogue/testdata/catalogue-835bf22b-xterm.module.json") + if err != nil { + t.Fatal(err) + } + for _, c := range []struct { + list string + want bool + }{ + {`{"font-size": {"type": "number", "description": "the desktop's font size"}}`, true}, + {`{"font-family": {"type": "string", "description": "the desktop's font"}}`, false}, + } { + dir := t.TempDir() + if err := os.MkdirAll(filepath.Join(dir, "modules", "xterm"), 0o700); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(dir, "modules", "xterm", "module.json"), raw, 0o600); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(dir, catalogue.PredefinedFile), []byte(c.list), 0o600); err != nil { + t.Fatal(err) + } + code, v, stderr := runJSON(t, []string{"-catalogue", dir}, nil) + if code != 0 { + t.Fatalf("exit %d: %s", code, stderr) + } + if got := v.Cases[0].Manifests["xterm"]; got.Accepted != c.want { + t.Errorf("predefined.json %s: xterm %+v, want accepted %v", c.list, got, c.want) + } + } +} diff --git a/go/composer/composer.go b/go/composer/composer.go index 5ab962a..a456a1a 100644 --- a/go/composer/composer.go +++ b/go/composer/composer.go @@ -55,6 +55,12 @@ type Request struct { HoldingsFromClaims bool `json:"holdings_from_claims,omitempty"` // Declarations asks each composed machine's declared paths (DepthCompose only). Declarations bool `json:"declarations,omitempty"` + // Predefined is the list of predefined keys at the catalogue's root (predefined.json, novox/hq ADR 0348 §7) that + // every config value falling back to a key is judged against, when the asker has it (mesh-compose -catalogue + // reads it). Without it a fallback is read as catalogue.ParseManifestForInputs reads it: against the list the mesh + // published when this process holds one, else each key as its value declares it, for the merge gate judges the + // fallback in the catalogue and a manifest that only carries one is not refused here. + Predefined catalogue.PredefinedList `json:"predefined,omitempty"` } // Case is one catalogue composed: every module's manifest by name, and per node the manifests kept at the build it runs. @@ -209,13 +215,22 @@ func Run(r Request) (Verdict, error) { return v, nil } -func parse(m map[string]Manifest, reads map[string]ManifestRead) (map[string]catalogue.Manifest, error) { +// readManifest reads one manifest as this run reads every one: against the request's list of predefined keys when it +// has one, else as the indexer and the builder read it (Request.Predefined). +func readManifest(raw []byte, predefined catalogue.PredefinedList) (catalogue.Manifest, error) { + if predefined != nil { + return catalogue.ParseManifestWith(raw, predefined) + } + return catalogue.ParseManifestForInputs(raw) +} + +func parse(m map[string]Manifest, reads map[string]ManifestRead, predefined catalogue.PredefinedList) (map[string]catalogue.Manifest, error) { out := map[string]catalogue.Manifest{} for name, man := range m { if len(man.Raw) == 0 { return nil, fmt.Errorf("%w: %s (digest %s)", ErrUnfilled, name, man.Digest) } - parsed, err := catalogue.ParseManifest(man.Raw) + parsed, err := readManifest(man.Raw, predefined) if reads != nil { if err != nil { reads[name] = ManifestRead{Refused: err.Error()} @@ -232,7 +247,7 @@ func parse(m map[string]Manifest, reads map[string]ManifestRead) (map[string]cat func runCase(r Request, c Case, depth string) (CaseVerdict, error) { cv := CaseVerdict{Name: c.Name, Manifests: map[string]ManifestRead{}} - shelf, err := parse(c.Manifests, cv.Manifests) + shelf, err := parse(c.Manifests, cv.Manifests, r.Predefined) if err != nil { return CaseVerdict{}, err } @@ -243,7 +258,7 @@ func runCase(r Request, c Case, depth string) (CaseVerdict, error) { for i := range in.Nodes { in.Nodes[i].Kept = nil if kept := c.Kept[in.Nodes[i].Name]; len(kept) > 0 { - k, err := parse(kept, nil) + k, err := parse(kept, nil, r.Predefined) if err != nil { return CaseVerdict{}, err } diff --git a/go/composer/composer_test.go b/go/composer/composer_test.go index 175894a..5e26900 100644 --- a/go/composer/composer_test.go +++ b/go/composer/composer_test.go @@ -3,6 +3,7 @@ package composer import ( "encoding/json" "errors" + "os" "reflect" "strings" "testing" @@ -104,3 +105,33 @@ func TestARequestItCannotRunIsRefused(t *testing.T) { t.Fatalf("a request with no machines: %+v, %v", v, err) } } + +// A manifest whose config value falls back to a predefined key (novox/hq ADR 0348 §7) is read, not refused for the +// list of predefined keys at the catalogue's root that a run is never handed with its manifests: the merge gate judges +// the fallback in the catalogue. xterm's module.json from mesh-catalog at 835bf22b falls back to "font-size"; a group +// touching xterm or pacman failed its composition on this (group check 18df81380db3). A list in the request is the one +// judged against, so a fallback it does not name is still refused. +func TestAManifestFallingBackToAPredefinedKeyIsRead(t *testing.T) { + raw, err := os.ReadFile("../catalogue/testdata/catalogue-835bf22b-xterm.module.json") + if err != nil { + t.Fatal(err) + } + if !strings.Contains(string(raw), `"predefined"`) { + t.Fatal("the fixture names no predefined key") + } + r := Request{V: Version, Cases: []Case{{Name: "change", Manifests: map[string]Manifest{"xterm": {Raw: raw}}}}} + v, err := Run(r) + if err != nil { + t.Fatal(err) + } + if got := v.Cases[0].Manifests["xterm"]; !got.Accepted { + t.Fatalf("xterm: %+v, want accepted", got) + } + r.Predefined = catalogue.PredefinedList{"something-else": {Type: "string", Description: "x"}} + if v, err = Run(r); err != nil { + t.Fatal(err) + } + if got := v.Cases[0].Manifests["xterm"]; got.Accepted || !strings.Contains(got.Refused, "not on the list of predefined keys") { + t.Fatalf("xterm judged against a list without its key: %+v, want refused naming the list", got) + } +}