From c2a3f3553f37d4c6df2253ec18900a7e826e8c10 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 11 Nov 2026 22:59:03 +0100 Subject: [PATCH] Assigner: serves the seat mesh-module-assigner's verb placement MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Builds: design 47 G A-62 (§704, §402) Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h --- .../cmd/mesh-delivery-assigner/seat_tools.go | 28 +++++++++++++++++++ .../mesh-delivery-assigner/seat_tools_test.go | 16 +++++++++-- .../mesh-delivery-assigner/manifest_test.go | 2 +- modules/mesh-delivery-assigner/module.json | 3 +- 4 files changed, 45 insertions(+), 4 deletions(-) diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/seat_tools.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/seat_tools.go index e459a792..c55cb433 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/seat_tools.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/seat_tools.go @@ -357,6 +357,33 @@ func (s *seatTools) doAgain(args map[string]any) (any, error) { return map[string]any{"proposal": p.ID, "again": p.Again, "follow": followProposal(p.ID)}, nil } +// doPlacement is the verb placement (design 47 G A-62, §704): the module's placements as the assignments rows hold them, one +// per branch a row names, with the nodes that run the module from it. A row naming no branch is one placement with the branch +// empty, as the row holds it. +func (s *seatTools) doPlacement(args map[string]any) (any, error) { + module := arg(args, "module") + if module == "" { + return nil, errors.New("placement takes the module") + } + all, _, err := s.assignments() + if err != nil { + return nil, err + } + byBranch := map[string][]string{} + for node, a := range all { + for _, m := range a.Modules { + if m.Module == module { + byBranch[m.Branch] = append(byBranch[m.Branch], node) + } + } + } + out := mma.PlacementAnswer{Placements: []mma.ModulePlacement{}} + for _, branch := range slices.Sorted(maps.Keys(byBranch)) { + out.Placements = append(out.Placements, mma.ModulePlacement{Branch: branch, Nodes: slices.Sorted(slices.Values(byBranch[branch]))}) + } + return out, nil +} + // assignmentsAnswer is what the verb assignments answers. type assignmentsAnswer struct { Keys map[string]keyed `json:"keys,omitempty"` @@ -524,6 +551,7 @@ func (s *seatTools) tools() []stdio.Tool { mma.VerbPropose: func(a map[string]any) (any, error) { return s.doPropose(a, stdio.CallerOf(a)) }, mma.VerbWithdraw: func(a map[string]any) (any, error) { return s.doWithdraw(a, stdio.CallerOf(a)) }, mma.VerbAgain: s.doAgain, + mma.VerbPlacement: s.doPlacement, } var out []stdio.Tool for _, v := range mma.Verbs() { diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/seat_tools_test.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/seat_tools_test.go index 18454ddf..67087e98 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/seat_tools_test.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/seat_tools_test.go @@ -80,6 +80,18 @@ func TestTheSeatsVerbs(t *testing.T) { setAssignments(st[mma.StateAssignments], anchor) + // placement (design 47 G A-62): a module's placements as the rows hold them, one per branch, with the nodes that run it from there. + before := maps.Clone(st[mma.StateAssignments].values) + setAssignments(st[mma.StateAssignments], mma.Assignments{V: keys.Version, Node: "laptop", Modules: []mma.PlacedModule{{Module: "web", Branch: "feat/x"}}}) + if got, err := s.doPlacement(map[string]any{"module": "web"}); err != nil || + !reflect.DeepEqual(got, mma.PlacementAnswer{Placements: []mma.ModulePlacement{{Branch: "", Nodes: []string{"anchor"}}, {Branch: "feat/x", Nodes: []string{"laptop"}}}}) { + t.Errorf("placement of web = %+v, %v", got, err) + } + if got, err := s.doPlacement(map[string]any{"module": "ghost"}); err != nil || len(got.(mma.PlacementAnswer).Placements) != 0 { + t.Errorf("placement of a module placed nowhere = %+v, %v", got, err) + } + st[mma.StateAssignments].values = before + // withdraw and again mark the proposal; the holder ends or judges it. got, err := s.doPropose(map[string]any{"why": "hold", "changes": `[{"op":"hold","node":"anchor","module":"web","claim":"node-shell","scope":"node"}]`}, "op") if err != nil { @@ -201,8 +213,8 @@ func TestTheSeatsVerbs(t *testing.T) { } else if rm := got.(proposed).Proposal; !slices.Equal(plc.Nodes(rm), []string{"joiner"}) || !slices.Equal(plc.RemovedNodes(rm), []string{"joiner"}) { t.Errorf("the remove-node proposed %+v", rm) } - // 1126/B8: the seat serves its five verbs; adopt and hand-back went with the controller (ADR 0590). - if tools := s.tools(); len(tools) != 5 || tools[2].Name != "mesh-module-assigner.propose" || tools[2].Run == nil { + // 1126/B8: the seat serves its six verbs; adopt and hand-back went with the controller (ADR 0590). + if tools := s.tools(); len(tools) != 6 || tools[2].Name != "mesh-module-assigner.propose" || tools[2].Run == nil { t.Errorf("the seat's verbs as served: %+v", tools) } // Issue 1966: propose checks its modules against the indexer's first view of modules, every page of it, asked again at diff --git a/modules/mesh-delivery-assigner/manifest_test.go b/modules/mesh-delivery-assigner/manifest_test.go index c0e9e793..d28cbc9c 100644 --- a/modules/mesh-delivery-assigner/manifest_test.go +++ b/modules/mesh-delivery-assigner/manifest_test.go @@ -88,7 +88,7 @@ func TestManifest(t *testing.T) { // ADR 0565 §7, §9 (issue 1953 D4): and the seat mesh-wants, serving upgrade and bus, and (design 47 lines 1362-1366) // revision, entry and difference from its revisions kept as objects in the old section, form 1, capped at 512 MiB. if len(m.Claims) != 2 || m.Claims[0].Name != meshmoduleassigner.Seat || m.Claims[0].At() != catalogue.ScopeMesh || - !slices.Equal(m.Claims[0].Serves, []string{"assignments", "proposals", "propose", "withdraw", "again"}) || + !slices.Equal(m.Claims[0].Serves, []string{"assignments", "proposals", "propose", "withdraw", "again", "placement"}) || m.Claims[1].Name != meshwants.Seat || m.Claims[1].At() != catalogue.ScopeMesh || !slices.Equal(m.Claims[1].Serves, []string{"upgrade", "bus", "revision", "entry", "difference", "input"}) { t.Errorf("the assigner claims %+v, want mesh-module-assigner with its five verbs and mesh-wants with its five", m.Claims) } diff --git a/modules/mesh-delivery-assigner/module.json b/modules/mesh-delivery-assigner/module.json index 989712c5..869eb605 100644 --- a/modules/mesh-delivery-assigner/module.json +++ b/modules/mesh-delivery-assigner/module.json @@ -12,7 +12,8 @@ "proposals", "propose", "withdraw", - "again" + "again", + "placement" ] }, {