Checker: the manifests of a change are judged against the seat list its reader holds (the request's Seats; a list not read fails the check), the module graph follows it, the manifest invokes seat-definitions and consumes its two events; composer from the delivery sdk (D-29; issue 1966)

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
This commit is contained in:
2026-11-05 15:47:35 +01:00
parent 839e27a064
commit b58e2616c7
13 changed files with 136 additions and 16 deletions
@@ -15,8 +15,8 @@ import (
nec "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
ne "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
"git.novox.be/novox/mesh-delivery/sdk/composer"
"git.novox.be/novox/mesh-sdk/go/buildstep"
"git.novox.be/novox/mesh-sdk/go/composer"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
@@ -395,6 +395,13 @@ func startBed(t *testing.T) *bed {
if err != nil {
t.Fatal(err)
}
// The seat mesh-catalog's holder answers the seat list with no trunk seat named: the checker's reader has then heard the seat
// and holds the bootstrapped definitions, which the judge is handed (ADR 0636 §1).
for _, subject := range []string{"mesh.seat.mesh-catalog.call.seat-definitions", "mesh.seat.mesh-catalog.call.seat-definitions.>"} {
if _, err := ctl.Subscribe(subject, func(m *nats.Msg) { _ = m.Respond([]byte(`{"result":{"revision":1,"seats":[]},"node":"ctl"}`)) }); err != nil {
t.Fatal(err)
}
}
ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
t.Cleanup(cancel)
b := &bed{ctx: ctx, js: js, buckets: map[string]jetstream.KeyValue{}, tools: map[string]jetstream.KeyValue{}, srvURL: srv.ClientURL()}
@@ -28,6 +28,8 @@ type judgeDeps struct {
asks check.Asks
renames check.Renames
seatDefs check.SeatDefinitions
// seats are the seat definitions the manifests are judged against (seatlist.go).
seats check.SeatList
tools *buildrun.ToolchainSet
trunks *check.Trunks
kept check.KV
@@ -220,7 +222,7 @@ func newJudge(d judgeDeps) check.Judge {
nodes = d.nodes
}
ex := &check.Examiner{Nodes: nodes, Compositions: d.compositions, Asks: d.asks, Toolchains: d.tools,
Kept: check.Kept{Store: d.kept}, Previews: d.previews, Modules: d.repos, Packages: d.repos, Published: d.published, KnownPackages: d.published, Forge: d.forge, Runs: d.repos, Builds: d.builds, BuildEnds: d.ends, BuildWait: d.buildWait, Announce: d.announce, Trunks: d.trunks, Manifests: d.repos, Read: d.repos, Dirs: d.repos, Predefined: d.predefined, Tags: d.tags, Renames: d.renames, SeatDefinitions: d.seatDefs, Logf: d.logf}
Kept: check.Kept{Store: d.kept}, Previews: d.previews, Modules: d.repos, Packages: d.repos, Published: d.published, KnownPackages: d.published, Forge: d.forge, Runs: d.repos, Builds: d.builds, BuildEnds: d.ends, BuildWait: d.buildWait, Announce: d.announce, Trunks: d.trunks, Manifests: d.repos, Read: d.repos, Dirs: d.repos, Predefined: d.predefined, Tags: d.tags, Renames: d.renames, SeatDefinitions: d.seatDefs, SeatList: d.seats, Logf: d.logf}
if d.nodes != nil && d.builds != nil && d.repos != nil {
ex.Serving = runningServes{have: d.nodes, builds: d.builds, read: d.repos, logf: d.logf}
}
@@ -26,6 +26,7 @@ import (
"git.novox.be/novox/mesh-sdk/go/announce"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/process"
"git.novox.be/novox/mesh-sdk/go/seatlist"
"git.novox.be/novox/mesh-sdk/go/views"
"mesh-delivery-checker/internal/check"
@@ -146,6 +147,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
// published, started once both are registered.
hub := process.NewEventHub(logger.Printf)
reader := &views.ProcessReader{Module: delivery.CheckerModule, Call: caller.Call, Logf: logger.Printf, Hub: hub}
seats := seatList{reader: followSeatList(seatlist.ProcessBus(caller.Call, hub), logger.Printf)}
findings := process.AwaitBucket(ctx, js, conn, findingsBucket, "a first view unanswered past its bound is said in the log alone", logger.Printf)
reader.Findings = func() views.FindingWriter { return sdkfindings.Writer(ctx, findings.KV) }
// Each by the owner's read tool and views family as the owner's manifest names them.
@@ -263,12 +265,14 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
// not there yet a group's check asks and waits in vain until its bound, said in its verdict.
tags := tagAsks{js: js, node: cred.Node, heard: &check.PublishedHeard{}, logf: logger.Printf, now: time.Now}
ch.Says = tags // a pull request's verdict event, on the same stream asks (design 26 *Out of step*)
if err := hub.Handle("mesh.seat.*.event.>", func(m jetstream.Msg) {
if !tags.heardPublished(m) {
_ = m.Ack() // nothing else the checker consumes rides the shared stream
for subject := range publishedSeats {
if err := hub.Handle(subject, func(m jetstream.Msg) {
if !tags.heardPublished(m) {
_ = m.Ack() // nothing else the checker consumes rides the shared stream
}
}); err != nil {
return err
}
}); err != nil {
return err
}
ch.Judge = newJudge(judgeDeps{
asks: buildrun.Asker{Publisher: asks, Waiter: waiter, Bound: stepBound}, tools: tools, trunks: trunks,
@@ -279,6 +283,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
predefined: keptPredefined{predefined},
renames: seatRenames{call: caller.Call},
seatDefs: seatDefinitionsOf(seatDefs),
seats: seats,
})
ch.Announce = announce
tryer.Judge = ch.Judge
@@ -0,0 +1,51 @@
package main
import (
"encoding/json"
"fmt"
"maps"
"slices"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/seatlist"
)
// The checker reads the seat list as every part does (novox/hq ADR 0635 §1, ADR 0636): through the seat mesh-catalog's verb
// seat-definitions, its two events heard as handlers of the module's event hub, from the bootstrapped definitions until the seat
// has answered. What the reader holds is given to catalogue at every change, which the module graph is worked out with, and to the
// indexer's judge as a request's Seats, which the manifests of a change are judged against.
// followSeatList starts the checker's reader of the seat list over the bus; the hub is started by the caller once every handler is registered.
func followSeatList(bus seatlist.Bus, logf func(string, ...any)) *seatlist.Reader {
reader, err := seatlist.Start(seatlist.Config{Bus: bus, Bootstrap: seatlist.Bootstrapped(),
OnChange: func(seats []catalogue.Seat) { catalogue.UseSeats(seats) }})
if err != nil {
logf("the checker cannot follow its seat list, so no manifest is judged until it starts again: %v", err)
}
if reader != nil {
catalogue.UseSeats(reader.Seats())
}
return reader
}
// seatList is the reader as the check's SeatList: the held definitions raw, and an error while the seat has not answered.
type seatList struct{ reader *seatlist.Reader }
func (s seatList) Definitions() ([]json.RawMessage, error) {
if s.reader == nil {
return nil, fmt.Errorf("the checker follows no seat list")
}
if st := s.reader.Status(); !st.Heard {
return nil, fmt.Errorf("%s", st.Health())
}
defs := s.reader.Definitions()
var out []json.RawMessage
for _, seat := range slices.Sorted(maps.Keys(defs)) {
raw, err := json.Marshal(defs[seat])
if err != nil {
return nil, fmt.Errorf("the definition of %s cannot be encoded: %w", seat, err)
}
out = append(out, raw)
}
return out, nil
}
@@ -11,7 +11,7 @@ import (
nec "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
ne "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
"git.novox.be/novox/mesh-sdk/go/composer"
"git.novox.be/novox/mesh-delivery/sdk/composer"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
@@ -205,10 +205,14 @@ func (e EngineCompositions) inSandbox(ctx context.Context, c *Case, head, node s
}
}
}
verdicts := map[string]delivery.ManifestVerdict{}
verdicts := map[string]json.RawMessage{}
if e.Verdicts != nil {
for name, v := range e.Verdicts() {
verdicts[name] = v
raw, err := json.Marshal(v)
if err != nil {
return a, fmt.Errorf("the verdict of the manifest of %s cannot be encoded: %w", name, err)
}
verdicts[name] = raw
}
}
for _, p := range c.Pulls {
@@ -11,7 +11,7 @@ import (
nec "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
ne "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
"git.novox.be/novox/mesh-sdk/go/composer"
"git.novox.be/novox/mesh-delivery/sdk/composer"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
@@ -21,8 +21,8 @@ import (
gobuild "git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep"
"git.novox.be/novox/mesh-contracts/go/seats/meshconfig"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
"git.novox.be/novox/mesh-delivery/sdk/composer"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/composer"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
@@ -1054,7 +1054,13 @@ func TestTheIndexersJudgeJudgesTheChangesManifests(t *testing.T) {
// trunk's, in a second ask (ADR 0633 clause 6, 1585/T22, T23)
indexer string
ownJudge bool
// seats is the seat list the checker holds: "read" gives the judge its two definitions as the request's Seats, "unread"
// is a reader the seat has not answered, and the manifests are then judged by nothing (ADR 0636 §1).
seats string
}{
{name: "the seat list read is the request's Seats", seats: "read"},
{name: "the seat list not read judges nothing", seats: "unread",
fails: "the change's manifests cannot be judged: the seat list is not read (the seat list is held from the bootstrap"},
{name: "a seat definition that does not parse", unparsed: "seats/web-api/seat.json: unexpected end of JSON input",
fails: "novox/mesh-delivery#7: the seat definition of web-api (module web-contracts, go) does not parse: seats/web-api/seat.json: unexpected end of JSON input"},
{name: "a rename brought, the verb not served", renames: "bring", seat: "not served",
@@ -1162,6 +1168,9 @@ func TestTheIndexersJudgeJudgesTheChangesManifests(t *testing.T) {
b.ex.Read = fixedManifests{strings.Repeat("a", 64): {Module: "mail"}, strings.Repeat("b", 64): {Module: "web"},
web: {Module: "web", Claims: []catalogue.Claim{{Name: CatalogSeat}}}}
}
if row.seats != "" {
b.ex.SeatList = fakeSeatList{read: row.seats == "read"}
}
var judged []delivery.ComposeJob
answer := b.asks.Answer
b.asks.Answer = func(a delivery.StepAsk) delivery.Step {
@@ -1207,6 +1216,12 @@ func TestTheIndexersJudgeJudgesTheChangesManifests(t *testing.T) {
}) {
t.Errorf("%s: the case says %v, want the new indexer's manifest said accepted by the trunk's manifest validator", row.name, cs.notes)
}
if row.seats == "unread" {
if v.State != delivery.VerdictFail || !strings.Contains(v.Failed.Words, row.fails) || len(judged) != 0 {
t.Errorf("%s: %+v, judged %d, want a fail saying %q before any judge", row.name, v.Failed, len(judged), row.fails)
}
continue
}
if row.ownJudge && (len(judged) != 2 || judged[0].Fetch["commit"] != head1 || judged[1].Fetch["commit"] != trunk) {
t.Fatalf("%s: judged %+v, want the head's judge and then the trunk's", row.name, judged)
}
@@ -1224,6 +1239,12 @@ func TestTheIndexersJudgeJudgesTheChangesManifests(t *testing.T) {
req.Cases[0].Manifests["mail"].Digest != strings.Repeat("a", 64) {
t.Fatalf("%s: the request %+v: %v", row.name, req, err)
}
if row.seats == "read" && (len(req.Seats) != 2 || string(req.Seats[0]) != `{"seat":"a"}` || string(req.Seats[1]) != `{"seat":"b"}`) {
t.Errorf("%s: the request's seats %s, want the two definitions the reader holds", row.name, req.Seats)
}
if row.seats == "" && len(req.Seats) != 0 {
t.Errorf("%s: the request's seats %s, want none from a checker that follows no list", row.name, req.Seats)
}
if !reflect.DeepEqual(req.Packages, row.known) || len(req.Input) != 0 && string(req.Input) != "null" {
t.Errorf("%s: the judge's known packages %v and input %s, want the known packages %v and no input", row.name,
req.Packages, req.Input, row.known)
@@ -1242,6 +1263,16 @@ func TestTheIndexersJudgeJudgesTheChangesManifests(t *testing.T) {
}
}
// fakeSeatList is the seat list reader as a test gives it: two definitions once read, else the reader's words.
type fakeSeatList struct{ read bool }
func (f fakeSeatList) Definitions() ([]json.RawMessage, error) {
if !f.read {
return nil, errors.New("the seat list is held from the bootstrap (2 seats) and nothing new is acted on")
}
return []json.RawMessage{json.RawMessage(`{"seat":"a"}`), json.RawMessage(`{"seat":"b"}`)}, nil
}
// knownNames are the packages state's names as a test gives them, each with the <module>/<artifact> it is built from.
type knownNames map[string]string
@@ -7,8 +7,8 @@ import (
"slices"
"strings"
"git.novox.be/novox/mesh-delivery/sdk/composer"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/composer"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/sdk/graph"
@@ -137,6 +137,9 @@ type Examiner struct {
Manifests TrunkManifests
// SeatDefinitions reads the indexer's seat definitions of a pull request (seatdefs.go, design 26); nil reads none.
SeatDefinitions SeatDefinitions
// SeatList gives the seat definitions the change's manifests are judged against, as the seat list reader holds them
// (manifests_judge.go, novox/hq ADR 0636 §1); nil gives the judge none.
SeatList SeatList
// Renames asks the seat mesh-config's verb renames for a change that brings or drops a `renamed-from` (renames.go,
// design 32); nil fails such a change, saying it cannot ask.
Renames Renames
@@ -9,9 +9,9 @@ import (
"slices"
"strings"
"git.novox.be/novox/mesh-delivery/sdk/composer"
"git.novox.be/novox/mesh-sdk/go/buildstep"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/composer"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/sdk/packagename"
@@ -63,6 +63,13 @@ type TrunkManifests interface {
ManifestDigests() map[string]string
}
// SeatList gives the seat definitions the manifests are judged against, whole, as raw definitions (a request's Seats): the
// list the seat list reader holds. It is an error when the list has not been read from the seat yet, since a manifest judged
// against the bootstrapped seats alone would be refused for a seat it names that the mesh defines (novox/hq ADR 0636 §1).
type SeatList interface {
Definitions() ([]json.RawMessage, error)
}
// PredefinedList is the catalogue's list of predefined keys the indexer keeps (its state predefined, key list), the one a
// config value falling back to a key is judged against (ADR 0348 §7); nil when it keeps none.
type PredefinedList interface {
@@ -124,6 +131,13 @@ func (st *state) judgeManifests(ctx context.Context) *delivery.Verdict {
st.c.note("the manifests are judged against the predefined keys of " + from)
}
req.Packages = st.knownPackages(ctx)
if st.j.SeatList != nil {
seats, err := st.j.SeatList.Definitions()
if err != nil {
return fail(goModule, "the change's manifests cannot be judged: the seat list is not read ("+err.Error()+")")
}
req.Seats = seats
}
cv, trunkCase, v := st.runJudge(ctx, req, touched, commit, at)
if v != nil {
return v
@@ -124,7 +124,7 @@ func TestManifestDeclarations(t *testing.T) { // 1678/AC1
"mesh-delivery-assigner.assigner_read_placement", "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:mesh-config.renames", "seat:mesh-hand-acts.hand-act", "seat:node-engine.compose-candidate"}; !reflect.DeepEqual(sorted(m.Invokes), want) {
"seat:mesh-catalog.seat-definitions", "seat:mesh-config.renames", "seat:mesh-hand-acts.hand-act", "seat:node-engine.compose-candidate"}; !reflect.DeepEqual(sorted(m.Invokes), want) {
t.Errorf("invokes = %v, want the forge's reads and compose-candidate", m.Invokes)
}
if want := []string{"node-build-go", "node-build-node"}; !reflect.DeepEqual(sorted(m.Uses), want) {
@@ -133,7 +133,7 @@ func TestManifestDeclarations(t *testing.T) { // 1678/AC1
// The checker is woken by a build module's report of a toolchain, as the indexer is: the bucket may not exist when it starts.
// And a group's check by the package registry seats' published of its tagged versions (design 51 *Tagged versions*).
if want := []string{"go-package-registry.published", "mesh-build-go.toolchain.reported", "mesh-build-node.toolchain.reported",
"mesh-delivery-assigner.views.placement", "mesh-delivery-indexer.views.candidates", "mesh-delivery-indexer.views.manifests", "mesh-delivery-indexer.views.modules",
"mesh-catalog.seat-definition-changed", "mesh-catalog.seat-definition-removed", "mesh-delivery-assigner.views.placement", "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", "npm-package-registry.published"}; !reflect.DeepEqual(sorted(m.Consumes), want) {
t.Errorf("consumes = %v, want %v", m.Consumes, want)
}
@@ -15,6 +15,8 @@
],
"consumes": [
"go-package-registry.published",
"mesh-catalog.seat-definition-changed",
"mesh-catalog.seat-definition-removed",
"mesh-build-go.toolchain.reported",
"mesh-build-node.toolchain.reported",
"mesh-delivery-assigner.views.placement",
@@ -81,6 +83,7 @@
"mesh-delivery-indexer.indexer_read_packages",
"mesh-delivery-indexer.indexer_read_predefined",
"mesh-delivery-indexer.indexer_read_seat_definitions",
"seat:mesh-catalog.seat-definitions",
"seat:mesh-config.renames",
"seat:mesh-hand-acts.hand-act",
"seat:node-engine.compose-candidate"