From c3d47e47338accf99412c42c26e0a90e5a741a6a Mon Sep 17 00:00:00 2001 From: jochen Date: Fri, 6 Nov 2026 12:10:36 +0100 Subject: [PATCH] A composition at a node-engine head without its seat list fails (issue 1966) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the checker's seat list is not read, or a head's definitions are not taken, the composer at a node-engine member's head was handed no list, judged no machine, and the composition passed with warnings. It now fails, naming why (ADR 0635 §3), and no composer is asked. The check's readers of the members' heads are its manifest validator and that composer (ADR 0636 §4 insight 1). The composer's Request.Seats contract says both the judge and the composer read the list, and a composer handed none judges no machine (ADR 0636 §1). Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i --- .../internal/check/composition_test.go | 14 ++++++++++++++ .../mesh-delivery-checker/internal/check/judge.go | 14 +++++++++++--- sdk/composer/composer.go | 6 +++--- 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/modules/mesh-delivery-checker/internal/check/composition_test.go b/modules/mesh-delivery-checker/internal/check/composition_test.go index bafc264a..174f85be 100644 --- a/modules/mesh-delivery-checker/internal/check/composition_test.go +++ b/modules/mesh-delivery-checker/internal/check/composition_test.go @@ -79,6 +79,20 @@ func TestAPassingCheckTakesTheAssignersComposition(t *testing.T) { t.Errorf("with the list not read the composition was handed %s, want none", got) } } + // ADR 0635 §3: at a node-engine member's head a list not read fails the composition, naming why, and asks no composer + // (a composer handed none would judge no machine and the composition would pass); read, it composes. + for _, read := range []bool{false, true} { + b.ex.SeatList = fakeSeatList{read: read} + asked := len(f.asked) + hv := b.judge(t, caseOf(t, pullOf(repoA, 7, head1), pullOf(nodeEngineRepo, 3, head2))).Verdict + switch { + case !read && (hv.State != meshdeliverycheckerclient.VerdictFail || hv.Composition == nil || hv.Composition.State != meshdeliverycheckerclient.CompositionFail || + !strings.Contains(hv.Composition.Words, "seat list") || len(f.asked) != asked): + t.Errorf("at a node-engine head with the list not read: verdict %+v, composition %+v, asked %d more; want a failed composition naming the seat list, no ask", hv, hv.Composition, len(f.asked)-asked) + case read && (hv.Composition == nil || hv.Composition.State != meshdeliverycheckerclient.CompositionPass || len(f.asked) != asked+1): + t.Errorf("at a node-engine head with the list read: composition %+v, want the composer's pass", hv.Composition) + } + } b.ex.SeatList = nil f.asked, f.seats = f.asked[:1], f.seats[:1] // ADR 0548 §5: a carry whose fingerprints did not change keeps the composition it passed and asks the assigner nothing. diff --git a/modules/mesh-delivery-checker/internal/check/judge.go b/modules/mesh-delivery-checker/internal/check/judge.go index f138bddc..10680a69 100644 --- a/modules/mesh-delivery-checker/internal/check/judge.go +++ b/modules/mesh-delivery-checker/internal/check/judge.go @@ -406,9 +406,17 @@ func (st *state) composed(ctx context.Context, v *meshdeliverychecker.VerdictsBy // The composer of a node-engine member's head composes against the check's seat list, the judge's (ADR 0635 §1, ADR 0636 // §4); only a list the reader read is handed: the heads' alone are no list, and the composer then judges no machine. st.c.Seats = nil - if seats, err := st.checkSeats(ctx); err != nil { - st.c.note("the composition is given no seat list: " + err.Error()) - } else if seats.listed { + seats, serr := st.checkSeats(ctx) + switch head := composerHead(st.c); { + case serr != nil && head != "": + // A composition at a node-engine member's head that cannot read the seat list it needs fails, naming why, rather than + // passing with no machine judged (ADR 0635 §3, ADR 0636 §1). + withComposition(v, meshwants.CompositionsByGroupHeadsValue{Group: st.c.Group, Heads: st.c.Heads, State: meshdeliverycheckerclient.CompositionFail, + Sdk: head, Words: "the composer at the node-engine's head " + head + " cannot be given the seat list it composes against: " + serr.Error()}) + return + case serr != nil: + st.c.note("the composition is given no seat list: " + serr.Error()) + case seats.listed: st.c.Seats = seats.defs } comp, err := st.j.Compositions.Compose(ctx, st.c, st.tags.goPackages) diff --git a/sdk/composer/composer.go b/sdk/composer/composer.go index 685fa51d..5990fa29 100644 --- a/sdk/composer/composer.go +++ b/sdk/composer/composer.go @@ -63,9 +63,9 @@ type Request struct { // 0578 §5, ADR 0606 §4); the composer does not read them. Nil is no list at hand, and the judge judges no name; an // empty list is a list, and every name is unknown to it. Packages []string `json:"packages,omitzero"` - // Seats are the seat definitions the judge judges against, as the asker's seat list reader holds them (the definitions of the - // seat mesh-catalog, resolved), raw so that this package names no seat (ADR 0635 §4, ADR 0636 §1). The judge reads them; the - // composer does not. None is no list at hand: the judge then judges against the list its process was given. + // Seats are the seat definitions the judge judges and the composer composes against, as the asker's seat list reader holds + // them (the definitions of the seat mesh-catalog, resolved), raw so that this package names no seat (ADR 0635 §4, ADR 0636 + // §1). The judge and the composer read them. None is no list at hand: a composer handed none judges no machine (ADR 0636 §1). Seats []json.RawMessage `json:"seats,omitempty"` }