Item 5 (join 11): an asker's build-seat queue is served by the seat's holders, so the indexer draws its kind-6 edge

An asker names the queue it asks as seat:<seat>.accept.<queue> (A-231); no module's serves lists that form, so
graph.Servers matched nothing and the indexer drew no served-verb edge from a build seat's holders to the asker.
Servers now reads the form through mesh-sdk's catalogue.QueueInvoke and answers every module serving a verb of the
seat; the kind-6 edge asks the module graph's Servers, the one rule, instead of a match of its own.

Builds: design 47 G §544, §605, A-231

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
2026-11-12 13:08:40 +01:00
parent 99c21957de
commit daa7737392
4 changed files with 31 additions and 6 deletions
@@ -9,6 +9,7 @@ import (
"strings"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-sdk/graph"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/seatdef"
)
@@ -232,6 +233,12 @@ func edgesOf(a meshcatalog.CatalogEntry, all map[string]meshcatalog.CatalogEntry
}
// Kind 6: a verb or a tool it invokes and another entry serves (§47), unless its edge lies on a cycle and is left out (ADR 0614 §3).
// A verb whose answer's fields it relies on (the long form of `invokes`, §558) names them in the edge's `why`.
// Who serves an invoke is the module graph's one rule (graph.Servers): a build seat's queue, `seat:<seat>.accept.<queue>`,
// is its holders' (A-231).
serving := graph.Graph{}
for _, m := range modules {
serving[m] = graph.Node{Serves: all[m].Serves}
}
fields := manifestOf(a).InvokeFields
for _, verb := range a.Invokes {
if slices.Contains(a.LeftOut, verb) {
@@ -241,9 +248,9 @@ func edgesOf(a meshcatalog.CatalogEntry, all map[string]meshcatalog.CatalogEntry
if named := fields[verb]; len(named) > 0 {
why += " fields " + strings.Join(named, ", ")
}
for _, m := range modules {
if b := all[m]; m != a.Module && slices.Contains(b.Serves, verb) {
add(meshcatalog.Edge{Kind: 6, From: primaryOf(b), To: delivers, Word: verb, Why: why})
for _, m := range serving.Servers(verb) {
if m != a.Module {
add(meshcatalog.Edge{Kind: 6, From: primaryOf(all[m]), To: delivers, Word: verb, Why: why})
}
}
}
@@ -20,9 +20,11 @@ func TestAnEntryCarriesItsEdgesOfKindsOneToFourAndSix(t *testing.T) {
Provides: []string{"builder"}, Serves: []string{"seat:mesh-builds.build"}},
"db": {Module: "db", Artifacts: []meshcatalog.Artifact{{Ref: "db:image", Kind: "image"}}, Provides: []string{"sql"}},
"cfg": {Module: "cfg", Artifacts: []meshcatalog.Artifact{{Ref: "cfg:archive", Kind: "archive"}}},
// A build seat's holder: the queue an asker names in its invokes, `seat:<seat>.accept.<queue>`, is its (A-231).
"node-go": {Module: "node-go", Artifacts: []meshcatalog.Artifact{{Ref: "node-go:program", Language: "go"}}, Serves: []string{"seat:node-build-go.steps"}},
"app": {Module: "app", Packages: map[string]string{"git.example/lib/go": "1.2.3"}, Bases: []string{"db"},
Toolchains: map[string]meshcatalog.BuildEdge{"go": {Module: "build-go"}},
Awaits: []string{"sql"}, Invokes: []string{"seat:mesh-builds.build", "seat:mesh-builds.cycle"}, LeftOut: []string{"seat:mesh-builds.cycle"},
Awaits: []string{"sql"}, Invokes: []string{"seat:mesh-builds.build", "seat:mesh-builds.cycle", "seat:node-build-go.accept.check"}, LeftOut: []string{"seat:mesh-builds.cycle"},
Manifest: map[string]any{"build": map[string]any{"artifacts": []any{map[string]any{"name": "program", "needs": []any{"cfg:archive", "nothing:here"}}}}},
Artifacts: []meshcatalog.Artifact{{Ref: "app:program", Language: "go"}}},
}
@@ -35,6 +37,7 @@ func TestAnEntryCarriesItsEdgesOfKindsOneToFourAndSix(t *testing.T) {
{Kind: 3, From: "db:image", To: "app:program", Stale: true, Word: "build.on", Why: "db"},
{Kind: 4, From: "db:image", To: "app:program", Word: "requires", Why: "sql"},
{Kind: 6, From: "build-go:program", To: "app:program", Word: "seat:mesh-builds.build", Why: "seat:mesh-builds.build"},
{Kind: 6, From: "node-go:program", To: "app:program", Word: "seat:node-build-go.accept.check", Why: "seat:node-build-go.accept.check"},
}
if !slices.Equal(got, want) {
t.Errorf("edges\n got %+v\nwant %+v", got, want)
+8 -2
View File
@@ -338,11 +338,17 @@ func (g Graph) Dependents(module string) []string {
return out
}
// Servers are the modules whose Serves names invoke (a seat verb or a module tool), sorted.
// Servers are the modules whose Serves names invoke (a seat verb or a module tool), sorted. An asker's queue of a build
// seat, `seat:<seat>.accept.<queue>` (design 47 G §544, A-231, read by catalogue.QueueInvoke), is served by every holder
// of the seat: each module whose Serves names a verb of it.
func (g Graph) Servers(invoke string) []string {
seat, _, queue := catalogue.QueueInvoke(invoke)
var out []string
for name, n := range g {
if slices.Contains(n.Serves, invoke) {
if slices.Contains(n.Serves, invoke) || queue && slices.ContainsFunc(n.Serves, func(v string) bool {
s, _, ok := catalogue.SeatInvoke(v)
return ok && s == seat
}) {
out = append(out, name)
}
}
@@ -298,6 +298,15 @@ func readersOfTheSeatList(t *testing.T) {
if got := g.Servers(list); !reflect.DeepEqual(got, []string{"indexer"}) {
t.Errorf("servers of the seat list = %v", got)
}
// A-231: an asker's queue of a build seat, `seat:<seat>.accept.<queue>`, is served by the seat's holders.
g["build-go"] = node(func(n *Node) { n.Serves = []string{"seat:node-build-go.steps"} })
if got := g.Servers("seat:node-build-go.accept.check"); !reflect.DeepEqual(got, []string{"build-go"}) {
t.Errorf("servers of a build seat's queue = %v", got)
}
if got := g.Servers("seat:node-build-go.accept.check.extra"); len(got) != 0 {
t.Errorf("servers of what is no queue = %v", got)
}
delete(g, "build-go")
if got := g.Before("checker"); !reflect.DeepEqual(got, []string{"indexer"}) {
t.Errorf("the checker waits for %v, want the indexer", got)
}