§717 (3), the builder, the checker and the indexer follow the assignments by the seat, at the holder the seat-holders view names

buildrun's AssignerHolder asked the seat's verb for the holder every 5 seconds. The builder and the checker now follow
mesh-module-assigner's assignments through views.ProcessReader.FollowSeat with the catalog half's holder function (the
checker gains the catalog half's reader of §614 for it); the indexer reads the claimant from its own catalog
(Indexer.ClaimantNow) and follows its seat-holders view again at each refresh of its build modules. AssignerHolder goes; the
manifests consume the seat-holders view (the checker also the catalog half's events and verbs), and the assigner's
seat-holders first-view tool answers the checker and the indexer. The beds give each part a claimant and a seat-holders view
in place of the seat verb's answer; the indexer's beds hold the claimant from the start, so the merge test reads the newest
revision.

Builds: design 47 G §717, §614, A-298, A-302

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
2026-11-12 20:17:36 +01:00
parent 2165b3b296
commit 04bfc92d3d
18 changed files with 267 additions and 182 deletions
@@ -115,7 +115,7 @@ func TestManifest(t *testing.T) {
// Design 47 G §717: the teller and the status view take their first views of the seat mesh-wants' asks and wants, and of
// the assignments, from the tools named for the states.
callers := map[string]string{"assignments": "mesh-grants,node-tools,mesh-delivery-indexer,mesh-delivery-builder,mesh-delivery-checker,mesh-delivery-view",
"seat-holders": "node-tools,mesh-delivery-builder,mesh-delivery-view,mesh-delivery-teller", "wants": "mesh-grants,mesh-delivery-view", "asks": "mesh-delivery-teller,mesh-delivery-view"}
"seat-holders": "node-tools,mesh-delivery-builder,mesh-delivery-view,mesh-delivery-teller,mesh-delivery-checker,mesh-delivery-indexer", "wants": "mesh-grants,mesh-delivery-view", "asks": "mesh-delivery-teller,mesh-delivery-view"}
env := m.Build.Artifacts[0].Env
for tool, list := range callers {
if env[views.CallersEnv(tool)] != list || views.ParseCallers(env[views.CallersEnv(tool)]).Allows("n1."+m.Module) {
+1 -1
View File
@@ -249,7 +249,7 @@
"env": {
"MESH_VIEW_CALLERS_ASKS": "mesh-delivery-teller,mesh-delivery-view",
"MESH_VIEW_CALLERS_ASSIGNMENTS": "mesh-grants,node-tools,mesh-delivery-indexer,mesh-delivery-builder,mesh-delivery-checker,mesh-delivery-view",
"MESH_VIEW_CALLERS_SEAT_HOLDERS": "node-tools,mesh-delivery-builder,mesh-delivery-view,mesh-delivery-teller",
"MESH_VIEW_CALLERS_SEAT_HOLDERS": "node-tools,mesh-delivery-builder,mesh-delivery-view,mesh-delivery-teller,mesh-delivery-checker,mesh-delivery-indexer",
"MESH_VIEW_CALLERS_WANTS": "mesh-grants,mesh-delivery-view"
}
}
@@ -1,45 +0,0 @@
package buildrun
import (
"context"
"time"
mma "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
"git.novox.be/novox/mesh-sdk/go/seatlist"
)
// **No part names the assigner** (design 47 G A-119, A-126, A-131): the module holding the seat mesh-module-assigner is asked of
// the seat that places modules through mesh-sdk's seatlist.HolderOf, and the names of the view of its state assignments are made
// from it, so a new holder is followed with no change of code. The builder and the checker follow the assignments so.
// AssignerHolder is the module holding the seat mesh-module-assigner now, asked until it answers or ctx ends. wait is the pause
// between two asks that failed; sleep says false when ctx ended during it.
func AssignerHolder(ctx context.Context, call seatlist.HolderCaller, wait time.Duration, sleep func(context.Context, time.Duration) bool, logf func(string, ...any)) (string, bool) {
for {
holder, err := seatlist.HolderOf(call, mma.Seat)
if err == nil {
return holder, true
}
logf("who holds the seat %s is not known yet, so no node is counted by the assignments: %v", mma.Seat, err)
if !sleep(ctx, wait) {
return "", false
}
}
}
// AssignmentsNames are the names made from the holder: the view's first-view tool `<holder>.assignments` and the bucket
// `<holder>_assignments` of the state.
type AssignmentsNames struct{ Holder, Tool, Bucket string }
// AssignmentsOf is the names of the holder's state assignments.
func AssignmentsOf(holder string) AssignmentsNames {
return AssignmentsNames{Holder: holder, Tool: holder + "." + mma.StateAssignments, Bucket: holder + "_" + mma.StateAssignments}
}
// SleepFor waits d, a context's bound, and says whether ctx is still alive.
func SleepFor(ctx context.Context, d time.Duration) bool {
w, cancel := context.WithTimeout(ctx, d)
defer cancel()
<-w.Done()
return ctx.Err() == nil
}
@@ -1,37 +0,0 @@
package buildrun
import (
"context"
"encoding/json"
"errors"
"testing"
"time"
"git.novox.be/novox/mesh-sdk/go/seatlist"
)
// The builder and the checker find the assigner's holder at run time: asked again while the seat list does not answer, and the names of the view
// are made from the holder (design 47 G A-119), not fixed in code.
func TestTheAssignerHolderIsFoundAtRunTime(t *testing.T) {
asks, slept := 0, 0
call := func(key string, _ any) (json.RawMessage, error) {
if key != seatlist.HolderKey() {
t.Errorf("asked %q, want %q", key, seatlist.HolderKey())
}
if asks++; asks == 1 {
return nil, errors.New("down")
}
return json.RawMessage(`{"seats":[{"seat":"mesh-module-assigner","holders":[{"module":"acme-assigner"}]}]}`), nil
}
holder, ok := AssignerHolder(context.Background(), call, time.Second, func(context.Context, time.Duration) bool { slept++; return true }, func(string, ...any) {})
if !ok || holder != "acme-assigner" || slept != 1 {
t.Fatalf("holder = %q, %v after %d pauses, want acme-assigner after one", holder, ok, slept)
}
if n := AssignmentsOf(holder); n.Tool != "acme-assigner.assignments" || n.Bucket != "acme-assigner_assignments" {
t.Errorf("names = %+v", n)
}
if _, ok := AssignerHolder(context.Background(), func(string, any) (json.RawMessage, error) { return nil, errors.New("down") }, time.Second,
func(context.Context, time.Duration) bool { return false }, func(string, ...any) {}); ok {
t.Error("a holder was found though ctx ended")
}
}
@@ -283,15 +283,14 @@ func startBedWith(t *testing.T, closed bool) *bed {
"git": "mesh-delivery-forge-bridge", "mesh-catalog": "mesh-delivery-indexer", // the floor's sources are the holders of git and mesh-catalog
"mesh-builds": builds.BuilderModule}) // the builder's own seat, whose verb reserved it answers once its definition is read (A-115, A-278)
readers := []string{"n1_" + builds.BuilderModule, "n2_" + builds.BuilderModule}
// The seat mesh-module-assigner's holder is read from the seat's verb assignments, as the builder finds it at run time (A-119).
assignerSub, err := ctl.Subscribe("mesh.seat.mesh-module-assigner.call.assignments.>", func(m *nats.Msg) {
reply, _ := json.Marshal(map[string]any{"result": map[string]any{"seats": []map[string]any{{"seat": "mesh-module-assigner", "holders": []map[string]string{{"module": testAssigner}}}}}, "node": "ctl"})
_ = m.Respond(reply)
})
if err != nil {
// Design 47 G §717, A-298: the catalog half names testAssigner the claimant of mesh-module-assigner, and its seat-holders view
// names it the seat's holder, so the builder follows the assignments at it; nothing answers the seat's verb assignments.
b.catalog.put(t, meshcatalog.CatalogEntry{Module: testAssigner, Repository: "novox/mesh-catalog", Commit: "c0", Fp: "src2:" + strings.Repeat("a", 64),
ManifestDigest: "sha256:" + strings.Repeat("b", 64), Manifest: map[string]any{"module": testAssigner, "claims": []any{map[string]any{"name": "mesh-module-assigner"}}}})
seatHolders := viewstest.Serve(t, ctx, js, ctl, testAssigner, "seat-holders", testAssigner+".seat-holders", readers...)
if _, err := seatHolders.Put(ctx, "mesh-module-assigner", []byte(`{"seat":"mesh-module-assigner","holders":[{"module":"`+testAssigner+`","node":"n1"}]}`)); err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = assignerSub.Unsubscribe() })
viewstest.Serve(t, ctx, js, ctl, testAssigner, "assignments", testAssigner+".assignments", readers...)
b.builds = mk(builds.BuilderBuildsBucket, builds.BuilderBuildsHistory)
b.instances = mk(instancesBucket, 1)
@@ -990,6 +989,12 @@ func TestTheBuilderAsksTheBuildSeatsAndCallsNoNode(t *testing.T) {
if n := calls.Load(); n != 0 {
t.Errorf("%d tool calls reached a build module", n)
}
// Design 47 G §717, A-298: the assignments are followed at the holder the seat-holders view names, no seat verb asked for it.
for deadline := time.Now().Add(10 * time.Second); !strings.Contains(b.out.String(), "holds "+testAssigner+"'s assignments, the holder of the seat mesh-module-assigner"); time.Sleep(10 * time.Millisecond) {
if time.Now().After(deadline) {
t.Fatalf("the builder did not follow the assignments at the seat's holder:\n%s", b.out.String())
}
}
// 1887/AC1: one ask per build module the build needs (netcheck: a Go and a TypeScript bundle), each on its seat's
// delivery stream.
got := asked()
@@ -119,7 +119,9 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
// The build modules are no names (design 47 G A-82): the holders of the build seats, the seat list's definitions and the
// catalog half's claims, found at each use.
seatGate := newSeatGate()
seatGate.Holders(branches.SeatHolders(ctx))
// The part's one holder function (A-298): the seat list's callers and the views found by their seat (§717) read it.
holders := branches.SeatHolders(ctx)
seatGate.Holders(holders)
seats := buildrun.NewBuildSeats(seatGate.definitions, versions.HolderOf(branches))
bound := defaultStepBound
if v := strings.TrimSpace(getenv("MESH_BUILDER_STEP_BOUND")); v != "" {
@@ -344,26 +346,21 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
// asked when the bound ends), is call-unanswered in the builder's findings (issue 1966, ADR 0668 §3, ADR 0596 §2).
reads.callsOf(found, pubBoundsKV.KV, heardBy(calls.CallKept), logger.Printf)
b.Forms = reads.built
// The assignments names the nodes the builder counts, once the seat list names the seat's holder and its first view is answered:
// the view is the holder's first-view tool, followed by its views family (A-119, A-131); no assigner module is named.
// The assignments names the nodes the builder counts: the seat mesh-module-assigner's view, followed at the holder the
// seat-holders view names and as it moves (design 47 G §717), its first view the holder's tool named for the state; no
// assigner module is named and nothing asks for the holder on a timer.
assignmentsErrs := make(chan error, 1)
assignments, err := reader.FollowSeat(mma.Seat, mma.StateAssignments, holders)
if err != nil {
return err
}
go func() {
holder, ok := buildrun.AssignerHolder(ctx, calls.Call, 5*time.Second, buildrun.SleepFor, logger.Printf)
if !ok {
return
}
names := buildrun.AssignmentsOf(holder)
assignments, err := reader.Follow(names.Holder, mma.StateAssignments, names.Tool)
if err != nil {
assignmentsErrs <- err
return
}
select {
case <-ctx.Done():
return
case <-assignments.Follower().Ready():
}
pe, err := buildrun.FollowAssignmentsIn(ctx, reader, names.Bucket, gate, b.NodesChanged, logger.Printf)
pe, err := buildrun.FollowAssignmentsIn(ctx, reader, assignments.Bucket(), gate, b.NodesChanged, logger.Printf)
if err != nil {
assignmentsErrs <- err
return
@@ -547,7 +547,11 @@ func startBed(t *testing.T) *bed {
for repo, commit := range trunkView.Load().(map[string]string) {
heads = append(heads, meshcatalog.Head{Repository: repo, Commit: commit})
}
body, _ := json.Marshal(meshcatalog.RevisionAnswer{Revision: &meshcatalog.CatalogRevision{Kind: "trunk", Revision: 1, Trunk: heads}})
// Design 47 G §717, A-298: the trunk's entry of testAssigner claims mesh-module-assigner, so the catalog half names it
// the claimant whose seat-holders view the checker follows.
body, _ := json.Marshal(meshcatalog.RevisionAnswer{Revision: &meshcatalog.CatalogRevision{Kind: "trunk", Branch: "main", Revision: 1, Trunk: heads},
Entries: []meshcatalog.CatalogEntry{{Module: testAssigner, ManifestDigest: "sha256:" + strings.Repeat("a", 64),
Manifest: map[string]any{"module": testAssigner, "claims": []any{map[string]any{"name": meshmoduleassigner.Seat}}}}}})
reply, _ := json.Marshal(map[string]any{"result": json.RawMessage(body), "node": "ctl"})
_ = m.Respond(reply)
}); err != nil {
@@ -615,15 +619,21 @@ func startBed(t *testing.T) *bed {
for _, st := range []string{candidatesState, modulesState, manifestsState, meshcatalog.StatePackages, check.IndexerPredefinedState, seatDefinitionsState} {
viewstest.Serve(t, ctx, js, ctl, check.IndexerModule, st, check.IndexerModule+".indexer_read_"+strings.ReplaceAll(st, "-", "_"), "n1_"+check.CheckerModule)
}
// The seat mesh-module-assigner's holder is read from the seat's verb assignments, as the checker finds it at run time (A-119, A-126).
for _, subject := range []string{"mesh.seat.mesh-module-assigner.call.assignments", "mesh.seat.mesh-module-assigner.call.assignments.>"} {
if _, err := ctl.Subscribe(subject, func(m *nats.Msg) {
reply, _ := json.Marshal(map[string]any{"result": map[string]any{"seats": []map[string]any{{"seat": "mesh-module-assigner", "holders": []map[string]string{{"module": testAssigner}}}}}, "node": "ctl"})
_ = m.Respond(reply)
}); err != nil {
t.Fatal(err)
// The catalog half's registrations and the gate's source branch (design 47 G §614), and the claimant's seat-holders view naming
// testAssigner the holder of mesh-module-assigner (§717): nothing answers the seat's verb assignments.
for subject, result := range map[string]any{"mesh.seat.mesh-catalog.call.registrations": meshcatalog.RegistrationsAnswer{Registrations: []meshcatalog.Registration{}},
"mesh.seat.mesh-delivery.call.source": map[string]any{"branches": []string{"main"}}} {
reply, _ := json.Marshal(map[string]any{"result": result, "node": "ctl"})
for _, s := range []string{subject, subject + ".>"} {
if _, err := ctl.Subscribe(s, func(m *nats.Msg) { _ = m.Respond(reply) }); err != nil {
t.Fatal(err)
}
}
}
seatHolders := viewstest.Serve(t, ctx, js, ctl, testAssigner, meshmoduleassigner.StateSeatHolders, testAssigner+"."+meshmoduleassigner.StateSeatHolders, "n1_"+check.CheckerModule)
if _, err := seatHolders.Put(ctx, meshmoduleassigner.Seat, []byte(`{"seat":"`+meshmoduleassigner.Seat+`","holders":[{"module":"`+testAssigner+`","node":"n1"}]}`)); err != nil {
t.Fatal(err)
}
viewstest.Serve(t, ctx, js, ctl, testAssigner, meshmoduleassigner.StateAssignments, testAssigner+"."+meshmoduleassigner.StateAssignments, "n1_"+check.CheckerModule)
// 1929/AC1: the indexer's value of the repository's module, with no TypeScript manifest, so the Go build module alone
// judges the repository (an unknown repository is judged by both, issue 1887 A12).
@@ -1281,6 +1291,9 @@ func TestTheCheckerSaysOwedReadableOnTheBus(t *testing.T) { // R2-1
!said.Keeps("asks") || said.Readable("asks") || !said.Keeps("first-views") || said.Readable("first-views") || !said.Keeps("findings") || !said.Keeps("inputs") || said.Readable("inputs") || !said.Keeps("compositions") || said.Readable("compositions") || !said.Keeps("could-not-run") || said.Readable("could-not-run") || len(said.States) != 12 {
t.Fatalf("the checker said %+v, want its twelve states, asks, hearing, first-views, inputs, compositions and could-not-run its own (DLV-M2; W6.9), with kept, owed, previews, tries and verdicts readable, tries by the delivery view (1929/AC4; 1887 A5b; build_usage)", said)
}
// Design 47 G §717, A-298: the assignments are followed at the holder the seat-holders view of the catalog half's claimant
// names, no seat verb asked for it.
b.said(t, "holds "+testAssigner+"'s assignments, the holder of the seat mesh-module-assigner")
}
const testAssigner = "acme-assigner" // a holder of the seat mesh-module-assigner no code names
@@ -30,6 +30,7 @@ import (
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-checker/checkermsg"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-sdk/buildstep"
"git.novox.be/novox/mesh-sdk/go/announce"
@@ -343,26 +344,26 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
watch.put = gate.Toolchain
// Until the assignments's first view is answered the gate passes every report on, as before the assignments is settled.
assignmentsErrs := make(chan error, 1)
// The assignments is the seat mesh-module-assigner's view, followed at the holder the seat-holders view names and as it
// moves (design 47 G §717): the holder function is the catalog half's claimant's seat-holders view (A-298), so no assigner
// module is named and nothing asks for the holder on a timer.
branches, err := meshcatalogclient.FollowBranchesProcess(reader, caller.Call)
if err != nil {
return err
}
branches.Logf = logger.Printf
pic, err := reader.FollowSeat(meshmoduleassigner.Seat, meshmoduleassigner.StateAssignments, branches.SeatHolders(ctx))
if err != nil {
return err
}
assignments.Store(pic)
go func() {
// The module holding the seat mesh-module-assigner is found at run time (design 47 G A-119, A-126, A-131): the view is its
// first-view tool, followed by its views family; no assigner module is named.
holder, ok := buildrun.AssignerHolder(ctx, caller.Call, 5*time.Second, buildrun.SleepFor, logger.Printf)
if !ok {
return
}
names := buildrun.AssignmentsOf(holder)
pic, err := reader.Follow(names.Holder, meshmoduleassigner.StateAssignments, names.Tool)
if err != nil {
assignmentsErrs <- err
return
}
assignments.Store(pic)
select {
case <-ctx.Done():
return
case <-pic.Follower().Ready():
}
pe, err := buildrun.FollowAssignmentsIn(ctx, reader, names.Bucket, gate, ch.NodesChanged, logger.Printf)
pe, err := buildrun.FollowAssignmentsIn(ctx, reader, pic.Bucket(), gate, ch.NodesChanged, logger.Printf)
if err != nil {
assignmentsErrs <- err
return
@@ -132,7 +132,7 @@ func TestManifestDeclarations(t *testing.T) { // 1678/AC1
if want := []string{"mesh-delivery-indexer.indexer_read_candidates", "mesh-delivery-indexer.indexer_read_manifests",
"mesh-delivery-indexer.indexer_read_modules", "mesh-delivery-indexer.indexer_read_packages", "mesh-delivery-indexer.indexer_read_predefined",
"mesh-delivery-indexer.indexer_read_seat_definitions",
"seat:git.file", "seat:mesh-catalog.revision", "seat:mesh-catalog.seat-definitions", "seat:mesh-config.renames", "seat:mesh-hand-acts.hand-act", "seat:mesh-module-assigner.assignments", "seat:node-build-go.drop", "seat:node-build-node.drop", "seat:node-engine.compose-candidate",
"seat:git.file", "seat:mesh-catalog.registrations", "seat:mesh-catalog.revision", "seat:mesh-catalog.seat-definitions", "seat:mesh-delivery.source", "seat:mesh-config.renames", "seat:mesh-hand-acts.hand-act", "seat:mesh-module-assigner.assignments", "seat:node-build-go.drop", "seat:node-build-node.drop", "seat:node-engine.compose-candidate",
// The image build seat's drop too: a check of a module built by it is asked of it, and dropped of it (buildrun.SeatDrops).
"seat:node-build-image.drop",
// A-290, A-291: a step's answer its key keeps as an object, read through each build seat's verb output.
@@ -146,8 +146,9 @@ func TestManifestDeclarations(t *testing.T) { // 1678/AC1
t.Errorf("uses = %v, want the Go and Node build seats", m.Uses)
}
// The checker is woken by a build seat's report of a toolchain, as the builder is: the bucket may not exist when it starts.
if want := []string{"mesh-catalog.catalog-revision", "mesh-catalog.seat-definition-changed", "mesh-catalog.seat-definition-removed", "mesh-conditions.findings-asked", "mesh-delivery-indexer.views.candidates", "mesh-delivery-indexer.views.manifests", "mesh-delivery-indexer.views.modules",
"mesh-delivery-indexer.views.packages", "mesh-delivery-indexer.views.predefined", "mesh-delivery-indexer.views.seat-definitions", "node-build-go.toolchain-reported", "node-build-node.toolchain-reported", "node-engine.running", "seat:mesh-module-assigner.views.assignments"}; !reflect.DeepEqual(sorted(m.Consumes), want) {
// Design 47 G §717, §614: the catalog half, whose claimant's seat-holders view names the holder of the assignments.
if want := []string{"mesh-catalog.catalog-revision", "mesh-catalog.registration-changed", "mesh-catalog.seat-definition-changed", "mesh-catalog.seat-definition-removed", "mesh-conditions.findings-asked", "mesh-delivery-indexer.views.candidates", "mesh-delivery-indexer.views.manifests", "mesh-delivery-indexer.views.modules",
"mesh-delivery-indexer.views.packages", "mesh-delivery-indexer.views.predefined", "mesh-delivery-indexer.views.seat-definitions", "mesh-delivery.source-moved", "node-build-go.toolchain-reported", "node-build-node.toolchain-reported", "node-engine.running", "seat:mesh-module-assigner.views.assignments", "seat:mesh-module-assigner.views.seat-holders"}; !reflect.DeepEqual(sorted(m.Consumes), want) {
t.Errorf("consumes = %v, want %v", m.Consumes, want)
}
if b, ok := m.OwnSecrets["broker"]; !ok || len(m.OwnSecrets) != 1 || b != "${dir:state}/broker" {
@@ -16,6 +16,9 @@
],
"consumes": [
"mesh-catalog.catalog-revision",
"mesh-catalog.registration-changed",
"mesh-delivery.source-moved",
"seat:mesh-module-assigner.views.seat-holders",
"mesh-catalog.seat-definition-changed",
"mesh-catalog.seat-definition-removed",
"mesh-delivery-indexer.views.candidates",
@@ -89,6 +92,8 @@
"mesh-delivery-indexer.indexer_read_seat_definitions",
"seat:git.file",
"seat:mesh-catalog.revision",
"seat:mesh-catalog.registrations",
"seat:mesh-delivery.source",
"seat:mesh-catalog.seat-definitions",
"seat:mesh-config.renames",
"seat:mesh-hand-acts.hand-act",
@@ -2,6 +2,7 @@ package main
import (
"context"
"encoding/json"
"maps"
"slices"
"sync"
@@ -9,7 +10,10 @@ import (
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshcatalog"
mma "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
"git.novox.be/novox/mesh-sdk/go/seatdef"
"git.novox.be/novox/mesh-sdk/go/seatlist"
"git.novox.be/novox/mesh-sdk/go/views"
"mesh-delivery-indexer/internal/index"
)
@@ -29,10 +33,15 @@ type buildModulesFollow struct {
hear func(stream, module string) // nil until the indexer's consumers are bound (hearWith)
heard map[string]bool // streams heard
watched map[string]bool // build modules whose toolchains watch was asked
// seatHolders is told at each refresh, once the reader is made (A-302).
seatHolders atomic.Pointer[seatHoldersFollow]
}
// refresh finds the build modules again and acts on what moved.
func (f *buildModulesFollow) refresh() {
if h := f.seatHolders.Load(); h != nil {
h.refresh()
}
by, err := f.ix.BuildModulesNow(f.ctx, f.defs())
if err != nil {
f.logf("the build modules are not found again, the ones found before stand: %v", err)
@@ -106,3 +115,56 @@ func refreshingBuildModules(say func(context.Context, meshcatalog.CatalogRevisio
return err
}
}
// seatHoldersFollow is the indexer's follow of the seat-holders view of the module claiming mesh-module-assigner, its holder
// function (design 47 G A-302, A-298): the claimant is read from the indexer's own catalog (it holds the catalog seat), and at
// each refresh (the start, a change of the seat list, a catalog revision) a claimant other than the one followed has its view
// followed and the old one's dropped. Nothing is asked on a timer.
type seatHoldersFollow struct {
ctx context.Context
ix interface {
ClaimantNow(context.Context, string) (string, bool)
}
reader *views.ProcessReader
logf func(string, ...any)
mu sync.Mutex
owner string
values func() map[string]json.RawMessage
}
// refresh follows the claimant's seat-holders view when the claimant is another than the one followed.
func (h *seatHoldersFollow) refresh() {
claimant, ok := h.ix.ClaimantNow(h.ctx, mma.Seat)
h.mu.Lock()
defer h.mu.Unlock()
if !ok || claimant == h.owner {
return
}
if h.owner != "" {
if err := h.reader.Drop(h.owner, mma.StateSeatHolders); err != nil {
h.logf("the seat-holders view of %s is not dropped: %v", h.owner, err)
}
h.owner, h.values = "", nil
}
p, err := h.reader.Follow(claimant, mma.StateSeatHolders, claimant+"."+mma.StateSeatHolders)
if err != nil {
h.logf("the seat-holders view of %s is not followed: %v", claimant, err)
return
}
h.owner, h.values = claimant, p.Follower().Values
h.logf("the seat-holders view of %s, the claimant of %s in the indexer's catalog, is followed", claimant, mma.Seat)
}
// holders is the holder function: the holders of a seat as the followed seat-holders view says them.
func (h *seatHoldersFollow) holders() func(seat string) []string {
return seatlist.HoldersIn(func() (map[string]json.RawMessage, error) {
h.mu.Lock()
values := h.values
h.mu.Unlock()
if values == nil {
return nil, nil
}
return values(), nil
})
}
@@ -49,6 +49,7 @@ import (
"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/views"
"git.novox.be/novox/mesh-sdk/go/views/viewstest"
"mesh-delivery-indexer/internal/index"
"mesh-delivery-indexer/judge"
@@ -805,6 +806,7 @@ func startIndexerRun(t *testing.T, put func(context.Context, buildLog), before .
b(ctx, js)
}
seedClaimantAlone(t, ctx, js)
serveAssignerAssignments(t, ctx, js, ctl)
broker := filepath.Join(t.TempDir(), "broker")
@@ -905,6 +907,7 @@ func seedBuildModules(t *testing.T, ctx context.Context, js jetstream.JetStream,
Manifest: digest, None2: sdkstep.ToolchainNoReport}
entries[b.module] = index.CatalogEntryOf(b.module, held, catalogue.Manifest{}, "", graph.Node{None: held.None2})
}
seedClaimant(t, ctx, js, manifests, entries)
maps.Copy(entries, others)
rev, objects, err := index.CatalogRevisionOf(meshcatalog.CatalogRevision{Revision: 1, Kind: "trunk"}, entries)
if err != nil {
@@ -915,6 +918,57 @@ func seedBuildModules(t *testing.T, ctx context.Context, js jetstream.JetStream,
}
}
// seedClaimant keeps the module claiming mesh-module-assigner in entries, its manifest by digest and the seat's definition (design
// 47 G A-302): the indexer reads the claimant from its own catalog and follows its seat-holders view, which serveAssignerAssignments
// serves.
func seedClaimant(t *testing.T, ctx context.Context, js jetstream.JetStream, manifests jetstream.KeyValue, entries map[string]meshcatalog.CatalogEntry) {
t.Helper()
if _, err := js.KeyValue(ctx, index.Bucket(index.Module, index.SeatDefinitionsState)); errors.Is(err, jetstream.ErrBucketNotFound) {
if _, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: index.Bucket(index.Module, index.SeatDefinitionsState), History: 1}); err != nil {
t.Fatal(err)
}
}
seedAssignerSeat(t, ctx, js)
manifest := `{"module":"` + assignerHolderModule + `","version":"1","claims":[{"name":"` + meshmoduleassigner.Seat + `","scope":"mesh"}]}`
digest := meshcatalogclient.ManifestDigest([]byte(manifest))
mkey, err := meshcatalogclient.ManifestsKey(digest)
if err != nil {
t.Fatal(err)
}
mdata, err := meshcatalogclient.EncodeManifest(meshcatalog.ManifestsByDigestValue{Module: assignerHolderModule, Manifest: manifest})
if err != nil {
t.Fatal(err)
}
if _, err := manifests.Put(ctx, mkey, mdata); err != nil {
t.Fatal(err)
}
held := meshcatalog.ModulesByModuleValue{Repository: "novox/mesh-build-modules", Commit: "4444444444444444444444444444444444444444", Path: "modules/" + assignerHolderModule,
Manifest: digest, None2: sdkstep.ToolchainNoReport}
entries[assignerHolderModule] = index.CatalogEntryOf(assignerHolderModule, held, catalogue.Manifest{}, "", graph.Node{None: held.None2})
}
// seedClaimantAlone keeps a first trunk revision holding the claimant alone, where no test kept one (seedBuildModules).
func seedClaimantAlone(t *testing.T, ctx context.Context, js jetstream.JetStream) {
t.Helper()
objects := newBusObjects(js, testAccount, index.CatalogRevisionsObjects)
if list, err := index.ListRevisions(ctx, objects, false); err != nil || len(list.Revisions) > 0 {
return
}
manifests, err := js.KeyValue(ctx, index.Bucket(index.Module, index.ManifestsState))
if err != nil {
t.Fatal(err)
}
entries := map[string]meshcatalog.CatalogEntry{}
seedClaimant(t, ctx, js, manifests, entries)
rev, kept, err := index.CatalogRevisionOf(meshcatalog.CatalogRevision{Revision: 1, Kind: "trunk"}, entries)
if err != nil {
t.Fatal(err)
}
if err := index.KeepRevision(ctx, objects, rev, kept); err != nil {
t.Fatal(err)
}
}
// seedAssignerSeat keeps the trunk definition of the seat mesh-module-assigner, which offers its state assignments as a view (§616,
// §699): the indexer consumes it in the seat's form, and its own manifest is judged against the seat list it holds.
func seedAssignerSeat(t *testing.T, ctx context.Context, js jetstream.JetStream) {
@@ -925,7 +979,8 @@ func seedAssignerSeat(t *testing.T, ctx context.Context, js jetstream.JetStream)
}
assignerDef := json.RawMessage(`{"format":1,"seat":"mesh-module-assigner","scope":"mesh","grain":"whole","domain":"Delivery","decision":"novox/hq ADR 0567",` +
`"verbs":[{"name":"assignments","input":{"type":"object"},"answer":{"type":"object"}}],"states":[{"name":"assignments","template":"state","view":true,` +
`"key-forms":[{"key":"<node>.node","value":{"type":"object"}}]}]}`)
`"key-forms":[{"key":"<node>.node","value":{"type":"object"}}]},{"name":"seat-holders","template":"state","view":true,` +
`"key-forms":[{"key":"<seat>","value":{"type":"object"}}]}]}`)
assignerSum := sha256.Sum256(assignerDef)
assignerEntry, _ := json.Marshal(index.SeatDefinitionEntry{Seat: "mesh-module-assigner", Source: index.SourceTrunk, Repository: "novox/mesh-contracts", Module: "mesh-contracts",
Directory: ".", Commit: "4444444444444444444444444444444444444444", Digest: hex.EncodeToString(assignerSum[:]), Definition: assignerDef})
@@ -998,17 +1053,20 @@ func TestIndexerWritesModulesAtAMerge(t *testing.T) { // 1239/AC9; I1, I3: the m
// as objects, said as catalog-revision on the seat's subject, every entry it lists stored once by digest.
objects := newBusObjects(js, testAccount, index.CatalogRevisionsObjects)
list, err := index.ListRevisions(ctx, objects, false)
if err != nil || len(list.Revisions) != 1 || list.Revisions[0].Name != "trunk.1" || !list.Revisions[0].Current ||
list.Entries != list.Revisions[0].Entries || len(list.Unlisted) != 0 || list.Fill.Bytes == 0 {
t.Fatalf("revisions kept = %+v (%v): want trunk.1 alone, current, each entry it lists stored once", list, err)
// The bed's catalog holds the claimant of mesh-module-assigner from the start (seedClaimantAlone, A-302): the merge's revision
// is the newest, current, lists the merge's module beside the claimant, and every entry it lists is stored.
if err != nil || len(list.Revisions) == 0 || !list.Revisions[len(list.Revisions)-1].Current || list.Revisions[len(list.Revisions)-1].Entries != 2 ||
list.Entries < list.Revisions[len(list.Revisions)-1].Entries || len(list.Unlisted) != 0 || list.Fill.Bytes == 0 {
t.Fatalf("revisions kept = %+v (%v): want the merge's revision newest, current, its two entries stored", list, err)
}
newest := list.Revisions[len(list.Revisions)-1].Revision
events, err := js.Stream(ctx, "EVENTS")
if err != nil {
t.Fatal(err)
}
if msg, err := events.GetLastMsgForSubject(ctx, "mesh.seat.mesh-catalog.event."+meshcatalog.EventCatalogRevision); err != nil ||
!strings.Contains(string(msg.Data), `"revision":1`) {
t.Errorf("catalog-revision said: %v (%v): want revision 1", msg, err)
!strings.Contains(string(msg.Data), fmt.Sprintf(`"revision":%d`, newest)) {
t.Errorf("catalog-revision said: %v (%v): want revision %d", msg, err, newest)
}
// W4.12 through the process: a recompute by hand moves nothing and answers equal; a candidate's half dropped by hand
// leaves the trunk's revision and its entries.
@@ -1038,15 +1096,15 @@ func TestIndexerWritesModulesAtAMerge(t *testing.T) { // 1239/AC9; I1, I3: the m
return index.TakenIn{}
}
key, value, _ := index.NewRecomputeAsk("a check", "ctl", time.Now())
if r := answer(key, value); !r.Stored || !strings.Contains(r.Answer, "equal to the kept revision 1") {
t.Errorf("indexer_recompute answered %+v: want equal to the kept revision 1", r)
if r := answer(key, value); !r.Stored || !strings.Contains(r.Answer, fmt.Sprintf("equal to the kept revision %d", newest)) {
t.Errorf("indexer_recompute answered %+v: want equal to the kept revision %d", r, newest)
}
key, value, _ = index.NewDropCandidateGraphAsk("g1", "h1", "its gate event never came", "ctl", time.Now())
if r := answer(key, value); !r.Stored || !slices.Equal(r.Dropped, []string{"entry.dx"}) {
t.Errorf("indexer_drop_candidate_graph answered %+v: want candidate.g1.h1 dropped with entry.dx", r)
}
if after, err := index.ListRevisions(ctx, objects, false); err != nil || len(after.Revisions) != 1 || after.Entries != list.Entries {
t.Errorf("revisions after the drop = %+v (%v): want trunk.1 and its entries alone", after, err)
if after, err := index.ListRevisions(ctx, objects, false); err != nil || len(after.Revisions) != len(list.Revisions) || after.Entries != list.Entries {
t.Errorf("revisions after the drop = %+v (%v): want the trunk's revisions and their entries alone", after, err)
}
}
@@ -1580,17 +1638,11 @@ func serveAssignerAssignments(t *testing.T, ctx context.Context, js jetstream.Je
t.Fatal(err)
}
late := assignerLate
// The seat's verb assignments with seats true answers who holds each seat (mesh-sdk's seatlist.HolderOf, A-126).
holder := func(m *nats.Msg) {
raw, _ := json.Marshal(map[string]any{"result": map[string]any{"seats": []map[string]any{{"seat": meshmoduleassigner.Seat, "holders": []map[string]string{{"module": assignerHolderModule}}}}}, "node": "n0"})
_ = m.Respond(raw)
}
for _, subject := range []string{"mesh.seat." + meshmoduleassigner.Seat + ".call.assignments.>", "mesh.seat." + meshmoduleassigner.Seat + ".tool.assignments"} {
hs, err := nc.Subscribe(subject, holder)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = hs.Unsubscribe() })
// Design 47 G §717, A-302: the claimant's seat-holders view names it the holder of mesh-module-assigner; nothing answers the
// seat's verb assignments.
seatHolders := viewstest.Serve(t, ctx, js, nc, assignerHolderModule, meshmoduleassigner.StateSeatHolders, assignerHolderModule+"."+meshmoduleassigner.StateSeatHolders)
if _, err := seatHolders.Put(ctx, meshmoduleassigner.Seat, []byte(`{"seat":"`+meshmoduleassigner.Seat+`","holders":[{"module":"`+assignerHolderModule+`","node":"n1"}]}`)); err != nil {
t.Fatal(err)
}
sub, err := nc.Subscribe("mesh.mod."+assignerHolderModule+".call.assignments.>", func(m *nats.Msg) {
if late != nil { // its runtime comes up after the indexer started (DLV-F5r)
@@ -560,7 +560,12 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
hub := process.NewEventHub(logger.Printf)
reader := &views.ProcessReader{Module: index.Module, Call: calls.Call, Logf: logger.Printf, Hub: hub,
Findings: func() views.WireWriter { return writer }}
followAssignments(ctx, reader, calls.Call, process.AwaitBucket(ctx, js, conn, index.Bucket(cred.Module, views.StatusState), "indexer_first_views reads nothing and indexer_first_view_again is not heard", logger.Printf), cred, js, gate, func(ctx context.Context) {
// The holder function (A-302): the seat-holders view of the claimant of mesh-module-assigner in the indexer's own catalog,
// followed again at each refresh of the build modules (a seat list change, a catalog revision).
seatHolders := &seatHoldersFollow{ctx: ctx, ix: ix, reader: reader, logf: logger.Printf}
seatHolders.refresh()
buildModules.seatHolders.Store(seatHolders)
followAssignments(ctx, reader, seatHolders.holders(), process.AwaitBucket(ctx, js, conn, index.Bucket(cred.Module, views.StatusState), "indexer_first_views reads nothing and indexer_first_view_again is not heard", logger.Printf), cred, js, gate, func(ctx context.Context) {
if err := ix.Recompute(ctx); err != nil {
logger.Printf("writing the modules again after the assignments of a build module changed: %v", err)
}
@@ -223,8 +223,8 @@ func TestAMeshSDKMergeMovesOnlyTheModulesImportingAChangedPackage(t *testing.T)
// No index names the seats: the indexer lists the module's `seats/` directory (ADR 0635 §6, D-I5).
seatFiles[contractsMerge] = map[string]string{"seats/git/seat.json": git}
seatFiles[badHead] = map[string]string{"seats/git/seat.json": `{"format":1,"seat":"git"}`}
// The indexer starts with the two build seats the fixture keeps (seedBuildModules) and no other definition: the verb
// seat-definitions answers them at once (ADR 0636 §4, §8).
// The indexer starts with the two build seats and the seat mesh-module-assigner the fixture keeps (seedBuildModules, its claimant
// for A-302) and no other definition: the verb seat-definitions answers them at once (ADR 0636 §4, §8).
if got, err := index.SeatDefinitionsOf(jsSource{ctx, seatDefs}, nil, ""); err != nil || func() string {
list, _ := got.(index.SeatDefinitionList)
var names []string
@@ -232,8 +232,8 @@ func TestAMeshSDKMergeMovesOnlyTheModulesImportingAChangedPackage(t *testing.T)
names = append(names, d.Seat)
}
return strings.Join(names, ",")
}() != "node-build-go,node-build-node" {
t.Errorf("seat-definitions of an indexer that kept the build seats alone = %+v, %v", got, err)
}() != "mesh-module-assigner,node-build-go,node-build-node" {
t.Errorf("seat-definitions of an indexer that kept the build seats and the assigner's alone = %+v, %v", got, err)
}
contracts := `{"module":"mesh-contracts","version":"1.0","build":{"artifacts":[{"name":"go","kind":"package","language":"go","paths":["go"],"series":"0.1","generated-from":"."}]}}`
merged("novox/mesh-contracts", 80, contractsMerge, forgemsg.Forge{Root: "dddddddddddddddddddddddddddddddddddddddd",
@@ -16,7 +16,6 @@ import (
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
"git.novox.be/novox/mesh-sdk/go/keys"
"git.novox.be/novox/mesh-sdk/go/process"
"git.novox.be/novox/mesh-sdk/go/seatlist"
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient/heard"
mcseat "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions"
@@ -105,28 +104,24 @@ func clearHeld(ctx context.Context, w index.FindingWriter, scope, id, kind, why
const graphState = graph.State
// followAssignments reads the seat mesh-module-assigner's assignments through its holder's first view and then its views family,
// never its bucket (ADR 0615 §1, DLV-F1r), into gate: the holder is the module the seat list names for the seat, asked through
// mesh-sdk's seatlist.HolderOf (design 47 G A-119, A-126, A-131), so no module is named and a new holder is followed with no change
// of code. The module's views reader follows it (design 47 G §532, A-26), which asks the first view, hears the views family and
// raises and clears the finding of a first view unanswered through the indexer's finding writer. Each key is taken as it changes,
// settled once answered, with changed told whenever what the gate passes on changed. It waits for the holder and the first answer
// at most views.Bound, so a normal start, the two owners coming up together, computes the fingerprints once; past it the indexer
// goes on, the gate passing every report, and the holder is asked again until it answers.
func followAssignments(ctx context.Context, reader *views.ProcessReader, call seatlist.HolderCaller, status *process.AwaitedBucket, cred process.Account, js jetstream.JetStream,
// never its bucket (ADR 0615 §1, DLV-F1r), into gate: the holder is the one the seat-holders view names, found and followed as it
// moves through views.ProcessReader.FollowSeat with holders, the indexer's holder function (design 47 G §717, A-302), so no module
// is named and nothing asks for the holder on a timer. The module's views reader follows it (design 47 G §532, A-26), which asks
// the first view, hears the views family and raises and clears the finding of a first view unanswered through the indexer's
// finding writer. Each key is taken as it changes, settled once answered, with changed told whenever what the gate passes on
// changed. It waits for the first answer at most views.Bound, so a normal start, the two owners coming up together, computes the
// fingerprints once; past it the indexer goes on, the gate passing every report, until the holder's first view arrives.
func followAssignments(ctx context.Context, reader *views.ProcessReader, holders func(seat string) []string, status *process.AwaitedBucket, cred process.Account, js jetstream.JetStream,
gate *buildrun.ToolchainGate, changed func(context.Context), logf func(string, ...any)) {
var settled atomic.Bool // the first view answered: the gate's nodes are the counted ones from now on
answered := make(chan struct{})
pic, err := reader.FollowSeat(mma.Seat, mma.StateAssignments, holders)
if err != nil {
logf("the assignments of the seat %s is not followed: %v", mma.Seat, err)
reader.Start(ctx, js, cred, status)
return
}
go func() {
holder, ok := buildrun.AssignerHolder(ctx, call, 5*time.Second, buildrun.SleepFor, logf)
if !ok {
return
}
names := buildrun.AssignmentsOf(holder)
pic, err := reader.Follow(names.Holder, mma.StateAssignments, names.Tool)
if err != nil {
logf("the assignments of the seat %s is not followed: %v", mma.Seat, err)
return
}
f := pic.Follower()
keep := f.Changed // the picture's own, kept
f.Changed = func(key string, value json.RawMessage, deleted bool, at time.Time) {
@@ -102,6 +102,41 @@ func (ix *Indexer) BuildModules() map[string]BuildModule {
// BuildModulesNow finds the build modules from the seat definitions defs and the manifests of the modules the indexer
// holds (BuildModulesOf), says each problem in the log, uses them (UseBuildModules) and answers them.
func (ix *Indexer) BuildModulesNow(ctx context.Context, defs map[string]seatdef.Definition) (map[string]BuildModule, error) {
manifests, err := ix.heldManifests(ctx)
if err != nil {
return nil, err
}
by, problems := BuildModulesOf(defs, manifests)
for _, p := range problems {
ix.logf("%s", p)
}
ix.UseBuildModules(by)
return by, nil
}
// ClaimantNow is the module whose manifest, among the modules the indexer holds, claims seat: the one claimant, read from the
// indexer's own catalog, the catalog seat's holder needing no catalog half (design 47 G A-302, A-298). None, or more than
// one, is none.
func (ix *Indexer) ClaimantNow(ctx context.Context, seat string) (string, bool) {
manifests, err := ix.heldManifests(ctx)
if err != nil {
ix.logf("who claims %s is not read from the modules held: %v", seat, err)
return "", false
}
var claimants []string
for _, m := range manifests {
if slices.ContainsFunc(m.Claims, func(c catalogue.Claim) bool { return c.Name == seat }) {
claimants = append(claimants, m.Module)
}
}
if len(claimants) != 1 {
return "", false
}
return claimants[0], true
}
// heldManifests are the manifests of the modules the indexer holds, one that cannot be read said in the log.
func (ix *Indexer) heldManifests(ctx context.Context) ([]catalogue.Manifest, error) {
held, err := ix.heldModules(ctx)
if err != nil {
return nil, err
@@ -118,12 +153,7 @@ func (ix *Indexer) BuildModulesNow(ctx context.Context, defs map[string]seatdef.
}
manifests = append(manifests, m)
}
by, problems := BuildModulesOf(defs, manifests)
for _, p := range problems {
ix.logf("%s", p)
}
ix.UseBuildModules(by)
return by, nil
return manifests, nil
}
// languageBuiltBy is the language module is the build module of, or "".
@@ -169,7 +169,7 @@ func TestManifestDeclarations(t *testing.T) { // 1238/AC1, 1444 (reads the build
// internal/broker/state.go:62-93).
// The registry seats' published fill each (§413: no consume of version.numbered) package's series (DLV-I5, design 51 *The versions*).
if want := []string{"go-package-registry.published", "mesh-build-go.toolchain.reported", "mesh-build-node.toolchain.reported", "mesh-builds.package-published", "mesh-builds.toolchain-agreed", "mesh-conditions.findings-asked", "mesh-delivery-checker.verdict", "mesh-delivery-forge-bridge.forge.**",
"mesh-delivery.freed", "mesh-delivery.heads-replaced", "mesh-delivery.pinned", "mesh-delivery.pins-stated", "mesh-delivery.source-moved", "mesh-pipeline-runner.pipeline-ended", "mesh-pipeline-runner.run-started", "mesh-wants.version-delivered", "mesh-wants.wait-ended", "mesh-wants.wanted", "node-build-go.step-ended", "node-engine.running", "npm-package-registry.published", "operator-channel.decided", "seat:mesh-module-assigner.views.assignments"}; !reflect.DeepEqual(sorted(m.Consumes), want) {
"mesh-delivery.freed", "mesh-delivery.heads-replaced", "mesh-delivery.pinned", "mesh-delivery.pins-stated", "mesh-delivery.source-moved", "mesh-pipeline-runner.pipeline-ended", "mesh-pipeline-runner.run-started", "mesh-wants.version-delivered", "mesh-wants.wait-ended", "mesh-wants.wanted", "node-build-go.step-ended", "node-engine.running", "npm-package-registry.published", "operator-channel.decided", "seat:mesh-module-assigner.views.assignments", "seat:mesh-module-assigner.views.seat-holders"}; !reflect.DeepEqual(sorted(m.Consumes), want) {
t.Errorf("consumes = %v, want %v", m.Consumes, want)
}
if b, ok := m.OwnSecrets["broker"]; !ok || len(m.OwnSecrets) != 1 || b != "${dir:state}/broker" {
@@ -84,6 +84,7 @@
"mesh-build-node.toolchain.reported",
"mesh-conditions.findings-asked",
"seat:mesh-module-assigner.views.assignments",
"seat:mesh-module-assigner.views.seat-holders",
"mesh-builds.package-published",
"mesh-builds.toolchain-agreed",
"mesh-wants.version-delivered",