From 56a7b115564bfffaf33232db324edcb7020f7e06 Mon Sep 17 00:00:00 2001 From: jochen Date: Sun, 8 Nov 2026 15:28:03 +0100 Subject: [PATCH 1/2] Move SeatsToPassOn into the SDK and widen it: a holder whose manifest no longer claims its seat has left it (issue 1966, ADR 0660) Claude-Session: https://claude.ai/code/session_01NcbgfFfQ4Z9UE5jK32PmE5 --- go/catalogue/seat_pass.go | 87 ++++++++++++++++++++++++++++++- go/catalogue/seat_pass_test.go | 94 ++++++++++++++++++++++++++++++++++ 2 files changed, 179 insertions(+), 2 deletions(-) create mode 100644 go/catalogue/seat_pass_test.go diff --git a/go/catalogue/seat_pass.go b/go/catalogue/seat_pass.go index 58ff9e8..472eb08 100644 --- a/go/catalogue/seat_pass.go +++ b/go/catalogue/seat_pass.go @@ -1,14 +1,97 @@ package catalogue -// An assignment as the passing of a seat reads it: a module on a machine, with its manifest. +import ( + "fmt" + "sort" +) + +// An assignment as the passing of a seat reads it: a module on a machine, with the manifest of the build that machine +// runs, else the trunk's (novox/hq ADR 0660 §2). type Assignment struct { Node string Manifest Manifest + // Unread is an assignment whose manifest could not be read at all: it is still assigned, so its holdings stand, + // and it claims nothing. + Unread bool } -// SeatPass is one seat whose holder on record is assigned nowhere, and the only assignment that can take it. +// SeatPass is one seat whose holder on record has left it, and the only assignment that can take it. type SeatPass struct { Seat Seat From Held To Held } + +// SeatsToPassOn reads the mesh for seats held once whose holder on record has left (novox/hq ADR 0449 §4, ADR 0660). +// +// **A holder on record that is assigned on no machine, or whose every assignment's manifest no longer claims the seat +// (or claims it and cannot hold it, by CanHold), holds nothing.** Of such a seat, exactly one assignment in the mesh +// able to hold it takes it over: that is a pass, and the caller applies it and says it. An assignment is a module on +// one node, so one module placed on two nodes is two claimants. With no such assignment the record stays; with two or +// more nothing is passed and the answer says so, naming two and the `seat --to` that settles it. A holder with an +// assignment whose manifest still claims the seat changes nothing (ADR 0131), and so does one whose manifest could not +// be read. Only mesh seats held once are read: a replicated seat's holders are added by an act (ADR 0223), a kinded +// bench is held by its claims, and a seat with several holders on record is a store that disagrees with the mesh. +// Nothing here records a pass (ADR 0660 §4). +func SeatsToPassOn(holdings []Held, assignments []Assignment) ([]SeatPass, []string) { + bySeat := map[string][]Held{} + var order []string + for _, h := range holdings { + s, ok := SeatNamed(h.Claim) + if !ok || s.Replicated || s.Scope != ScopeMesh || h.Scope != ScopeMesh || KindedBenches[s.Name] { + continue + } + if _, seen := bySeat[s.Name]; !seen { + order = append(order, s.Name) + } + bySeat[s.Name] = append(bySeat[s.Name], h) + } + sort.Strings(order) + + var passes []SeatPass + var said []string + for _, name := range order { + recs := bySeat[name] + if len(recs) != 1 { + continue + } + seat, _ := SeatNamed(name) + holds := false + for _, a := range assignments { + if a.Manifest.Module != recs[0].Module { + continue + } + if a.Unread || CanHold(a.Manifest, seat) == nil { + holds = true + } + } + if holds { + continue + } + var claimants []Held + for _, a := range assignments { + if a.Unread || CanHold(a.Manifest, seat) != nil { + continue + } + claimants = append(claimants, Held{Claim: seat.Name, Scope: seat.Scope, Node: a.Node, Module: a.Manifest.Module}) + } + sort.Slice(claimants, func(i, j int) bool { + if claimants[i].Node != claimants[j].Node { + return claimants[i].Node < claimants[j].Node + } + return claimants[i].Module < claimants[j].Module + }) + switch len(claimants) { + case 0: + case 1: + passes = append(passes, SeatPass{Seat: seat, From: recs[0], To: claimants[0]}) + default: + said = append(said, fmt.Sprintf( + "%s on %s is on record as the holder of %q and holds it nowhere, and %s on %s and %s on %s both claim it "+ + "— nothing is passed; `seat %s --to /` records the one that holds it", + recs[0].Module, recs[0].Node, seat.Name, claimants[0].Module, claimants[0].Node, + claimants[1].Module, claimants[1].Node, seat.Name)) + } + } + return passes, said +} diff --git a/go/catalogue/seat_pass_test.go b/go/catalogue/seat_pass_test.go new file mode 100644 index 0000000..c10e034 --- /dev/null +++ b/go/catalogue/seat_pass_test.go @@ -0,0 +1,94 @@ +package catalogue + +import ( + "strings" + "testing" +) + +func passSeat(t *testing.T) { + t.Helper() + was := Seats() + t.Cleanup(func() { UseSeats(was) }) + UseSeats([]Seat{ + {Name: "mesh-broker", Scope: ScopeMesh, Delivers: "mesh-bus", Decision: "test"}, + {Name: "mesh-dns-resolver", Scope: ScopeMesh, Replicated: true, Decision: "test"}, + }) +} + +func broker(module string) Manifest { + return Manifest{Module: module, Provides: []Offer{{Name: "mesh-bus", Scope: ScopeMesh}}, + Claims: []Claim{{Name: "mesh-broker", Scope: ScopeMesh}}} +} + +// A module that still runs but no longer claims the seat. +func dropped(module string) Manifest { return Manifest{Module: module} } + +func recorded(node, module string) []Held { + return []Held{{Claim: "mesh-broker", Scope: ScopeMesh, Node: node, Module: module}} +} + +// novox/hq ADR 0660 "How it is checked", and ADR 0449 §4's original case. +func TestSeatsToPassOn(t *testing.T) { + passSeat(t) + rec := recorded("novox", "old") + cases := []struct { + name string + rec []Held + as []Assignment + to string // "node/module" of the one pass, "" for none + saidW []string + }{ + {"holder assigned nowhere, one claimant (ADR 0449 §4)", rec, + []Assignment{{Node: "novox", Manifest: broker("new")}}, "novox/new", nil}, + {"holder assigned but its manifest dropped the claim", rec, + []Assignment{{Node: "novox", Manifest: dropped("old")}, {Node: "novox", Manifest: broker("new")}}, "novox/new", nil}, + {"holder claims but cannot hold the seat", rec, + []Assignment{{Node: "novox", Manifest: Manifest{Module: "old", Claims: []Claim{{Name: "mesh-broker", Scope: ScopeMesh}}}}, + {Node: "ace", Manifest: broker("new")}}, "ace/new", nil}, + {"the running build still claims: the holder keeps the seat", rec, + []Assignment{{Node: "novox", Manifest: broker("old")}, {Node: "novox", Manifest: broker("new")}}, "", nil}, + {"one of the holder's two nodes still claims it", rec, + []Assignment{{Node: "novox", Manifest: dropped("old")}, {Node: "ace", Manifest: broker("old")}, + {Node: "g14", Manifest: broker("new")}}, "", nil}, + {"holder's manifest unread: still assigned", rec, + []Assignment{{Node: "novox", Manifest: Manifest{Module: "old"}, Unread: true}, {Node: "novox", Manifest: broker("new")}}, "", nil}, + {"an unread assignment is no claimant", rec, + []Assignment{{Node: "novox", Manifest: dropped("old")}, {Node: "ace", Manifest: Manifest{Module: "new"}, Unread: true}}, "", nil}, + {"no claimant keeps the record", rec, + []Assignment{{Node: "novox", Manifest: dropped("old")}, {Node: "novox", Manifest: dropped("other")}}, "", nil}, + {"holder gone, no claimant keeps the record", rec, nil, "", nil}, + {"one module on two nodes is two claimants: none passed, said", rec, + []Assignment{{Node: "novox", Manifest: dropped("old")}, {Node: "ace", Manifest: broker("new")}, {Node: "novox", Manifest: broker("new")}}, + "", []string{"old", "new on ace", "new on novox", "seat mesh-broker --to"}}, + {"two claimants, holder gone: said", rec, + []Assignment{{Node: "novox", Manifest: broker("a")}, {Node: "ace", Manifest: broker("b")}}, + "", []string{"a on novox", "b on ace", "seat mesh-broker --to"}}, + {"a seat with two holders on record is not read", append(rec[:1:1], Held{Claim: "mesh-broker", Scope: ScopeMesh, Node: "ace", Module: "x"}), + []Assignment{{Node: "novox", Manifest: broker("new")}}, "", nil}, + {"a replicated seat is never passed", + []Held{{Claim: "mesh-dns-resolver", Scope: ScopeMesh, Node: "novox", Module: "gone"}}, + []Assignment{{Node: "novox", Manifest: Manifest{Module: "r", Claims: []Claim{{Name: "mesh-dns-resolver", Scope: ScopeMesh}}}}}, "", nil}, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + passes, said := SeatsToPassOn(c.rec, c.as) + if c.to == "" && len(passes) != 0 || c.to != "" && len(passes) != 1 { + t.Fatalf("passes=%+v said=%v", passes, said) + } + if c.to != "" { + p := passes[0] + if p.Seat.Name != "mesh-broker" || p.From.Module != "old" || p.To.Node+"/"+p.To.Module != c.to { + t.Fatalf("the pass is %+v", p) + } + } + if (len(c.saidW) == 0) != (len(said) == 0) { + t.Fatalf("said=%v, want %v", said, c.saidW) + } + for _, w := range c.saidW { + if !strings.Contains(said[0], w) { + t.Fatalf("the answer does not say %q: %s", w, said[0]) + } + } + }) + } +} -- 2.54.0 From 3379b24c8092c62718045baa4487bce0b0da3ada Mon Sep 17 00:00:00 2001 From: jochen Date: Sun, 8 Nov 2026 15:30:47 +0100 Subject: [PATCH 2/2] SeatsToPassOn tests: a claimant whose running manifest does not claim is not counted; the answer names the settling command Claude-Session: https://claude.ai/code/session_01NcbgfFfQ4Z9UE5jK32PmE5 --- go/catalogue/seat_pass_test.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/go/catalogue/seat_pass_test.go b/go/catalogue/seat_pass_test.go index c10e034..a475228 100644 --- a/go/catalogue/seat_pass_test.go +++ b/go/catalogue/seat_pass_test.go @@ -54,15 +54,17 @@ func TestSeatsToPassOn(t *testing.T) { []Assignment{{Node: "novox", Manifest: Manifest{Module: "old"}, Unread: true}, {Node: "novox", Manifest: broker("new")}}, "", nil}, {"an unread assignment is no claimant", rec, []Assignment{{Node: "novox", Manifest: dropped("old")}, {Node: "ace", Manifest: Manifest{Module: "new"}, Unread: true}}, "", nil}, + {"a claimant whose running manifest does not claim is not counted", rec, + []Assignment{{Node: "novox", Manifest: dropped("old")}, {Node: "ace", Manifest: dropped("new")}}, "", nil}, {"no claimant keeps the record", rec, []Assignment{{Node: "novox", Manifest: dropped("old")}, {Node: "novox", Manifest: dropped("other")}}, "", nil}, {"holder gone, no claimant keeps the record", rec, nil, "", nil}, {"one module on two nodes is two claimants: none passed, said", rec, []Assignment{{Node: "novox", Manifest: dropped("old")}, {Node: "ace", Manifest: broker("new")}, {Node: "novox", Manifest: broker("new")}}, - "", []string{"old", "new on ace", "new on novox", "seat mesh-broker --to"}}, + "", []string{"old", "new on ace", "new on novox", "seat mesh-broker --to /"}}, {"two claimants, holder gone: said", rec, []Assignment{{Node: "novox", Manifest: broker("a")}, {Node: "ace", Manifest: broker("b")}}, - "", []string{"a on novox", "b on ace", "seat mesh-broker --to"}}, + "", []string{"a on novox", "b on ace", "seat mesh-broker --to /"}}, {"a seat with two holders on record is not read", append(rec[:1:1], Held{Claim: "mesh-broker", Scope: ScopeMesh, Node: "ace", Module: "x"}), []Assignment{{Node: "novox", Manifest: broker("new")}}, "", nil}, {"a replicated seat is never passed", -- 2.54.0