CK9: the checker follows the seat list through mesh-sdk's seatlist.Part, its own seatlist.go goes

Builds: design 47 G §204, §264, §320

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
2026-11-12 05:30:45 +01:00
parent b4155cc5a2
commit 9a28de8ab0
4 changed files with 34 additions and 72 deletions
@@ -7,28 +7,12 @@ import (
"slices"
"sync/atomic"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/seatdef"
"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), changed func()) *seatlist.Reader {
reader, err := seatlist.Start(seatlist.Config{Bus: bus, Bootstrap: seatlist.Bootstrapped(),
OnChange: func(seats []catalogue.Seat) { catalogue.UseSeats(seats); changed() }})
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
}
// The checker reads the seat list through mesh-sdk's seatlist.Part, the one wiring (design 47 G §204): what the reader holds is the
// indexer's judge's Seats of a request, which the manifests of a change are judged against.
// 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 }
@@ -52,15 +36,6 @@ func (s seatList) Definitions() ([]json.RawMessage, error) {
return out, nil
}
// readIfUnread is called when the checker's hub is heard. The seat may have answered, and said that it does, before the
// checker's consumer existed, whose events start at the stream's end: the first read found it silent and nothing else reads
// again until its next event, so while the list is unread it is read now, on a goroutine of its own (ADR 0636 §1).
func readIfUnread(reader *seatlist.Reader) {
if reader != nil {
reader.ReloadIfUnread()
}
}
// buildsWatch is what is done when the build modules may have changed (design 47 G A-82): the seat list names the build seats
// and the indexer's modules the holders of the seats, so a change of either may name a build module not watched yet. It is set once
// the parts that watch the build modules are made, and called from then on.
@@ -161,7 +161,8 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
hub := process.NewEventHub(logger.Printf)
reader := &views.ProcessReader{Module: check.CheckerModule, Call: caller.Call, Logf: logger.Printf, Hub: hub}
var buildsChanged buildsWatch
seats := seatList{reader: followSeatList(seatlist.ProcessBus(caller.Call, hub), logger.Printf, buildsChanged.Changed)}
seatPart := seatlist.NewPart("checker", nil)
seats := seatList{reader: seatPart.Follow(seatlist.ProcessBus(caller.Call, hub), buildsChanged.Changed, logger.Printf)}
// The finding writer is made once the checker is (its observe reads the checker's kept checks); until then a first view
// unanswered is said in the log alone.
var findings atomic.Pointer[checkerFindings]
@@ -503,7 +504,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
reader.Start(ctx, js, cred, process.AwaitBucket(ctx, js, conn, firstViewsBucket, "checker_first_views reads nothing and checker_first_view_again is not heard", logger.Printf))
// Every part has registered. Where the consumer is not there yet the hub binds it as soon as it is, and the seat list is read
// again then if it is still unread.
if err := hub.StartUntilBound(ctx, js, cred, nil, func() { readIfUnread(seats.reader) }); err != nil {
if err := hub.StartUntilBound(ctx, js, cred, nil, seatPart.EventsLive); err != nil {
logger.Printf("%v, so no package registry's published is heard until the consumer is there; the checker tries again", err)
}
defer hub.Stop()
@@ -1,43 +0,0 @@
package main
import (
"encoding/json"
"errors"
"sync/atomic"
"testing"
"time"
"git.novox.be/novox/mesh-sdk/go/seatlist"
)
// silentThenAnswering is a seat that is silent at the reader's first read and answers after.
type silentThenAnswering struct{ answering atomic.Bool }
func (b *silentThenAnswering) Ask(string, any) (json.RawMessage, error) {
if !b.answering.Load() {
return nil, errors.New("not connected yet")
}
return json.RawMessage(`{"revision":1,"seats":[]}`), nil
}
func (b *silentThenAnswering) Subscribe(string, func(string, json.RawMessage) error) error {
return nil
}
// A seat that spoke before the checker's consumer existed is read when the hub is heard (ADR 0636 §1).
func TestTheSeatListIsReadWhenTheHubIsHeard(t *testing.T) {
bus := &silentThenAnswering{}
reader := followSeatList(bus, t.Logf, func() {})
<-reader.Ready()
if reader.Status().Heard {
t.Fatal("a silent seat was heard")
}
bus.answering.Store(true)
readIfUnread(reader)
for deadline := time.Now().Add(5 * time.Second); !reader.Status().Heard; time.Sleep(10 * time.Millisecond) {
if time.Now().After(deadline) {
t.Fatal("the list was not read when the hub was heard")
}
}
var _ seatlist.Bus = bus
}
@@ -0,0 +1,29 @@
package main
import (
"os"
"path/filepath"
"strings"
"testing"
)
// The checker wires no reader of the seat list of its own (design 47 G §204, CK9): it follows the list through mesh-sdk's
// seatlist.Part, and its code holds no seatlist.Start nor a file of its own for the list.
func TestTheCheckerWiresNoSeatListReaderOfItsOwn(t *testing.T) {
if _, err := os.Stat("seatlist.go"); err == nil {
t.Error("seatlist.go is the checker's own wiring of the seat list: mesh-sdk's seatlist.Part is the one wiring")
}
files, _ := filepath.Glob("*.go")
for _, f := range files {
if strings.HasSuffix(f, "_test.go") {
continue
}
src, err := os.ReadFile(f)
if err != nil {
t.Fatal(err)
}
if strings.Contains(string(src), "seatlist.Start(") {
t.Errorf("%s starts a seat list reader of its own: use seatlist.Part.Follow", f)
}
}
}