Assigner: serves the seat mesh-module-assigner's verb placement

Builds: design 47 G A-62 (§704, §402)

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
2026-11-11 22:59:03 +01:00
parent 9493d0d289
commit c2a3f3553f
4 changed files with 45 additions and 4 deletions
@@ -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() {
@@ -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
@@ -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)
}
+2 -1
View File
@@ -12,7 +12,8 @@
"proposals",
"propose",
"withdraw",
"again"
"again",
"placement"
]
},
{