Let the composer take each manifest's verdict from its input instead of judging (ADR 0578 §5)

Issue 1585, S11 (mesh-sdk half): Case.Verdicts carries the indexer's verdicts; a refused module is held as its
stub and named; an unknown field is set aside and named, never refused.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
This commit is contained in:
2026-11-01 01:57:36 +01:00
parent 7631a7d5de
commit 7b09de1e55
4 changed files with 67 additions and 91 deletions
+1 -10
View File
@@ -28,7 +28,6 @@ import (
"sort"
"strings"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/composer"
)
@@ -97,15 +96,7 @@ 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
}
// No list of predefined keys is read: the composer judges no manifest (novox/hq ADR 0578 §5).
}
req.Machines = append(req.Machines, machines...)
if *depth != "" {
+11 -38
View File
@@ -16,6 +16,7 @@ import (
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/compose"
"git.novox.be/novox/mesh-sdk/go/composer"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/internal/beside"
)
@@ -77,7 +78,9 @@ func encode(t *testing.T, r composer.Request) []byte {
// refused.
func TestTheCommandWritesOneVerdictPerManifestAndMachine(t *testing.T) {
r := request(t, composer.DepthResolve)
r.Cases[0].Manifests["broken"] = composer.Manifest{Raw: json.RawMessage(`{"module":"broken","version":"1","shares":["x"]}`)}
r.Cases[0].Manifests["later"] = composer.Manifest{Raw: json.RawMessage(`{"module":"later","version":"1","shares":["x"]}`)}
r.Cases[0].Manifests["refused"] = composer.Manifest{Raw: json.RawMessage(`{"module":"refused","version":"1"}`)}
r.Cases[0].Verdicts = map[string]delivery.ManifestVerdict{"refused": {State: delivery.VerdictRefused, Problems: []string{"it is wrong"}}}
code, v, stderr := runJSON(t, nil, encode(t, r))
if code != 0 {
t.Fatalf("exit %d: %s", code, stderr)
@@ -86,8 +89,13 @@ func TestTheCommandWritesOneVerdictPerManifestAndMachine(t *testing.T) {
t.Fatalf("verdict = %+v", v)
}
c := v.Cases[0]
if got := c.Manifests["broken"]; got.Accepted || !strings.Contains(got.Refused, `"shares"`) {
t.Errorf("the manifest with an unknown field reads %+v, want refused naming the field", got)
// ADR 0578 §5, §6: the composer judges nothing; it sets an unknown field aside and names it, and holds a manifest
// its verdict refuses, named in the indexer's words.
if got := c.Manifests["later"]; !got.Accepted || !strings.Contains(got.SetAside, `"shares"`) {
t.Errorf("the manifest with an unknown field reads %+v, want accepted naming the field set aside", got)
}
if got := c.Manifests["refused"]; got.Accepted || !strings.Contains(got.Refused, "refused the manifest of refused: it is wrong") {
t.Errorf("the refused manifest reads %+v, want refused in the verdict's words", got)
}
if got := c.Manifests["nats"]; !got.Accepted {
t.Errorf("nats reads %+v, want accepted", got)
@@ -310,38 +318,3 @@ 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)
}
}
}
+39 -30
View File
@@ -1,4 +1,4 @@
// Package composer is mesh-sdk's composition and validator as one function over a plain request (novox/hq ADR 0482
// Package composer is mesh-sdk's composition as one function over a plain request (novox/hq ADR 0482
// clause 4, issue 1621): what the command mesh-compose runs, so a program that links any one mesh-sdk can have the
// composition of another commit judged by building that commit's command and reading its verdict.
//
@@ -57,11 +57,9 @@ 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 is the list of predefined keys at the catalogue's root (predefined.json, novox/hq ADR 0348 §7). No
// longer read: the composer judges no manifest (novox/hq ADR 0578 §5), the indexer's judge does, and its verdicts
// come in each case's Verdicts. Kept so an asker that still sends it is read.
Predefined catalogue.PredefinedList `json:"predefined,omitempty"`
}
@@ -70,11 +68,16 @@ type Request struct {
// Placements are, by node, the placement the case composes that node with in place of what Input says of it: a change's
// own placement files, so a check composes a catalogue pull request's placement (issue 1126, frombus.Placed); a null
// placement leaves nothing placed on the node. A case without them composes Input's.
//
// Verdicts are, by module, the indexer's judge's verdict on its manifest in Manifests (novox/hq ADR 0578 §5), put in by
// the assigner or the checker that runs the composer: a refused one is held, composed as its stub so what it offers
// still shapes the rest, and named in the case's verdict. A module without one is accepted (delivery.TakeManifestVerdict).
type Case struct {
Name string `json:"name"`
Manifests map[string]Manifest `json:"manifests"`
Kept map[string]map[string]Manifest `json:"kept,omitempty"`
Placements map[string]*delivery.Placement `json:"placements,omitempty"`
Name string `json:"name"`
Manifests map[string]Manifest `json:"manifests"`
Verdicts map[string]delivery.ManifestVerdict `json:"verdicts,omitempty"`
Kept map[string]map[string]Manifest `json:"kept,omitempty"`
Placements map[string]*delivery.Placement `json:"placements,omitempty"`
}
// Manifest is a module's manifest as its repository holds it (Raw, the bytes), or the digest the delivery indexer keeps
@@ -124,10 +127,13 @@ type CaseVerdict struct {
Machines map[string]Machine `json:"machines,omitempty"`
}
// ManifestRead is one manifest as this mesh-sdk reads it: accepted, or refused with the reader's words.
// ManifestRead is one manifest as this mesh-sdk reads it: accepted, or refused with why: the indexer's verdict
// (Case.Verdicts) or bytes that do not decode. A field the decoder does not know is set aside, named in SetAside, and
// the manifest still accepted (novox/hq ADR 0578 §6).
type ManifestRead struct {
Accepted bool `json:"accepted"`
Refused string `json:"refused,omitempty"`
SetAside string `json:"set_aside,omitempty"`
}
// Machine is one machine composed: whether it composes (resolves, at DepthResolve), why not, the node-engine
@@ -222,31 +228,34 @@ func Run(r Request) (Verdict, error) {
return v, nil
}
// 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) {
// parse decodes every manifest leniently and takes its verdict (novox/hq ADR 0578 §3, §5): the composer judges none. A
// manifest refused by its verdict is held, composed as its stub without its items (as the bus reader holds one); bytes
// that do not decode are left out. Both are named in reads.
func parse(m map[string]Manifest, reads map[string]ManifestRead, verdicts map[string]delivery.ManifestVerdict) (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 := readManifest(man.Raw, predefined)
if reads != nil {
if err != nil {
reads[name] = ManifestRead{Refused: err.Error()}
} else {
reads[name] = ManifestRead{Accepted: true}
decoded, err := catalogue.Decode(man.Raw)
read := ManifestRead{Accepted: err == nil, SetAside: decoded.SetAside()}
if err != nil {
read.Refused = fmt.Sprintf("the manifest of %s does not decode: %v", name, err)
} else if v, ok := verdicts[name]; ok {
if refused, _ := delivery.TakeManifestVerdict(&v); refused {
if v.Module == "" {
v.Module = name
}
read = ManifestRead{Refused: v.Why()}
decoded.Items = nil
decoded = compose.Stub(decoded)
}
}
if reads != nil {
reads[name] = read
}
if err == nil {
out[name] = parsed
out[name] = decoded
}
}
return out, nil
@@ -254,7 +263,7 @@ func parse(m map[string]Manifest, reads map[string]ManifestRead, predefined cata
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, r.Predefined)
shelf, err := parse(c.Manifests, cv.Manifests, c.Verdicts)
if err != nil {
return CaseVerdict{}, err
}
@@ -268,7 +277,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, r.Predefined)
k, err := parse(kept, nil, nil)
if err != nil {
return CaseVerdict{}, err
}
+16 -13
View File
@@ -120,20 +120,17 @@ func TestARequestItCannotRunIsRefused(t *testing.T) {
}
}
// 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) {
// The composer judges no manifest (novox/hq ADR 0578 §5): xterm's module.json from mesh-catalog at 835bf22b, whose
// config value falls back to "font-size", is read whatever list of predefined keys a request carries (a group touching
// xterm failed its composition on this, group check 18df81380db3); a module whose verdict in the input refuses it is
// held, composed as its stub, and named in the verdict's words.
func TestTheComposerTakesTheVerdictsOfItsInput(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}}}}}
r := Request{V: Version, Predefined: catalogue.PredefinedList{"something-else": {Type: "string", Description: "x"}},
Cases: []Case{{Name: "change", Manifests: map[string]Manifest{"xterm": {Raw: raw}}}}}
v, err := Run(r)
if err != nil {
t.Fatal(err)
@@ -141,11 +138,17 @@ func TestAManifestFallingBackToAPredefinedKeyIsRead(t *testing.T) {
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"}}
r.Cases[0].Verdicts = map[string]delivery.ManifestVerdict{"xterm": {Module: "xterm", State: delivery.VerdictRefused,
Problems: []string{"falls back to a key not listed"}, Judge: "src2:j"}}
shelf, err := parse(r.Cases[0].Manifests, map[string]ManifestRead{}, r.Cases[0].Verdicts)
if err != nil || !shelf["xterm"].Stubbed {
t.Fatalf("the refused module is not held as its stub: %+v %v", shelf["xterm"], err)
}
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)
if got := v.Cases[0].Manifests["xterm"]; got.Accepted ||
!strings.Contains(got.Refused, "the indexer's judge (src2:j) refused the manifest of xterm: falls back to a key not listed") {
t.Fatalf("xterm refused by its verdict: %+v, want refused in the verdict's words", got)
}
}