A composition at a node-engine head without its seat list fails (issue 1966)

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
This commit is contained in:
2026-11-06 12:10:36 +01:00
parent a92dc95f94
commit c3d47e4733
3 changed files with 28 additions and 6 deletions
@@ -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.
@@ -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)
+3 -3
View File
@@ -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"`
}