Checker: the build modules are the holders of the build seats on the seat list, never a name

The checker's fixed names of the build modules (buildmodules.go in its program, rules and check) go: the
Examiner finds each build module by the language its seat's acting rule builds (buildrun.BuildSeats over
the seat list's definitions and the holders the indexer's modules name by their manifests' claims), the facts
carry the language where a level counts by it, and the program's manifest reads the steps and toolchains of
the seats and hears their toolchain-reported. The tools read the steps of each holder and follow the seat
list as the view does. Timers stay (A-118).

Builds: design 47 G A-82, A-118, §204
Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
2026-11-12 01:31:24 +01:00
parent 1eb481ccc8
commit bd20ba5b8f
33 changed files with 528 additions and 225 deletions
@@ -19,6 +19,8 @@ import (
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/buscalls"
"git.novox.be/novox/mesh-sdk/go/seatdef"
"git.novox.be/novox/mesh-sdk/go/seatlist"
"git.novox.be/novox/mesh-sdk/go/views"
)
@@ -418,11 +420,42 @@ func stepBound() time.Duration {
return check.DefaultStepBound
}
// seatListAgainCallers are who may ask the seat list again by hand (design 47 G §204).
var seatListAgainCallers = []string{stdio.AnyPerson, "node-tools", "mesh-healers"}
// holderOf is a seat's holder as the seat that places modules answers it (seatlist.HolderOf, design 47 G A-126): false while the
// seat has none or the answer cannot be read.
func holderOf(seat string) (string, bool) {
module, err := seatlist.HolderOf(stdio.Ask, seat)
return module, err == nil
}
func main() {
// The build modules are no names (design 47 G A-82): the build seats on the seat list, each held by the module the seat
// that places modules names for it; the seat list and its two tools are wired once by mesh-sdk (§204).
gate := seatlist.NewPart("checker", seatListAgainCallers)
go func() {
<-stdio.Served()
gate.Follow(seatlist.StdioBus(), nil, func(f string, a ...any) { fmt.Fprintf(os.Stderr, "[mesh-delivery-checker-tools] "+f+"\n", a...) })
}()
seats := buildrun.NewBuildSeats(func() map[string]seatdef.Definition {
if rd := gate.Reader(); rd != nil {
return rd.Definitions()
}
return nil
}, holderOf)
r := check.Reader{VerdictState: read(stdio.State("verdicts")), KeptState: read(stdio.State("kept")), CandidateState: ownerRead(stdio.Ask, indexerCandidatesTool),
PreviewState: read(stdio.State("previews")), InputState: read(stdio.State(check.InputsStateName)),
StepStates: map[string]check.StateRead{"mesh-build-go": read(stdio.State("mesh-build-go.steps")), "mesh-build-node": read(stdio.State("mesh-build-node.steps"))},
HaveState: read(stdio.State("node-engine.have")), StepBound: stepBound(),
StepStates: func() map[string]check.StateRead {
out := map[string]check.StateRead{}
for _, m := range seats.All() {
if m.Module != "" && m.Toolchain != "" {
out[m.Module] = read(stdio.State(m.Module + ".steps"))
}
}
return out
},
HaveState: read(stdio.State("node-engine.have")), StepBound: stepBound(),
CouldNotRunState: read(stdio.State(check.CouldNotRunState)), HearingState: read(stdio.State(buildrun.HearingState)),
AskState: read(stdio.State(check.AsksState)), BuildState: read(stdio.State("mesh-delivery-builder.builds"))}
r.Runs = runsOf{ownerRead(stdio.Ask, indexerModulesTool)} // the node-engine at the commit the mesh runs, as the checker reads it
@@ -466,6 +499,7 @@ func main() {
// checker_first_views and checker_first_view_again: where the checker's first views of the indexer's and the assigner's
// states stand, and asking one again by hand (ADR 0615, DLV-M2).
tools = append(tools, views.StatusTools(check.CheckerModule, "checker_first_views", "checker_first_view_again", stdio.State(views.StatusState), meshhandactsclient.Recorder{Ask: stdio.Ask}, time.Now)...)
tools = append(tools, gate.Tools()...)
tools = append(tools, SeatInputTools(r)...)
tools = append(tools, RequestTools(read(stdio.State(check.RequestsStateName)))...)
if err := stdio.Serve("", logged(tools)); err != nil {
@@ -5,9 +5,11 @@ package main
import (
"bytes"
"context"
"crypto/sha256"
"encoding/json"
"errors"
"fmt"
"maps"
"os"
"path/filepath"
"slices"
@@ -47,6 +49,9 @@ import (
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/keys"
"git.novox.be/novox/mesh-sdk/go/process"
"git.novox.be/novox/mesh-sdk/go/seatdef"
"git.novox.be/novox/mesh-sdk/go/seatlist"
"git.novox.be/novox/mesh-sdk/go/seatlist/seatlisttest"
"git.novox.be/novox/mesh-sdk/go/views/viewstest"
"mesh-delivery-checker/rules"
@@ -294,7 +299,7 @@ func (f *fakeBuildModules) serve(t *testing.T, module string, kept jetstream.Key
if err := gobuild.DeclareAskWorkers(f.ctx, f.js, language, 30*time.Second); err != nil {
t.Fatal(err)
}
prefix := map[string]string{"mesh-build-go": "build_go_", "mesh-build-node": "build_node_"}[module]
prefix := map[string]string{goBuildModule: "build_go_", nodeBuildModule: "build_node_"}[module]
call := func(name string) gobuild.StepFunc {
return func(ctx context.Context, args map[string]any) (any, error) {
tool := prefix + name
@@ -320,7 +325,7 @@ func (f *fakeBuildModules) serve(t *testing.T, module string, kept jetstream.Key
v, err := meshdeliverycheckerclient.DecodeKept(e.Value())
return int(v.Tests), err == nil, err
}}
if module == "mesh-build-go" {
if module == goBuildModule {
steps.Digest = call("digest")
}
put := func(key string, value any) (uint64, error) {
@@ -527,8 +532,10 @@ func startBed(t *testing.T) *bed {
}
// 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).
// It lists the build seats (design 47 G A-82): the seat whose acting rule builds Go and the one that builds TypeScript, from
// which the checker finds its build modules.
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 {
if _, err := ctl.Subscribe(subject, func(m *nats.Msg) { _ = m.Respond(seatDefinitionsAnswer(m.Data)) }); err != nil {
t.Fatal(err)
}
}
@@ -550,13 +557,7 @@ func startBed(t *testing.T) *bed {
ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
t.Cleanup(cancel)
b := &bed{trunkNow: trunkView, ctx: ctx, js: js, buckets: map[string]jetstream.KeyValue{}, tools: map[string]jetstream.KeyValue{}, srvURL: srv.ClientURL()}
for module, stream := range toolchainStreams {
if _, err := js.CreateStream(ctx, jetstream.StreamConfig{Name: stream, Subjects: []string{"mesh.stream." + module + ".event.toolchain.reported"}}); err != nil {
t.Fatal(err)
}
if _, err := js.CreateOrUpdateConsumer(ctx, stream, jetstream.ConsumerConfig{Durable: "n1_mesh-delivery-checker", AckPolicy: jetstream.AckExplicitPolicy}); err != nil {
t.Fatal(err)
}
for _, module := range []string{goBuildModule, nodeBuildModule} {
kv, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: buildrun.ToolchainsBucket(module), History: 1})
if err != nil {
t.Fatal(err)
@@ -626,16 +627,19 @@ func startBed(t *testing.T) *bed {
}
b.setOnline(t, "n1", true)
b.setNode(t, "n1")
for _, module := range []string{"mesh-build-go", "mesh-build-node"} {
// The indexer's modules name the holders of the build seats, by the seats their manifests claim.
b.putBuildModule(t, goBuildModule, "node-build-go")
b.putBuildModule(t, nodeBuildModule, "node-build-node")
for _, module := range []string{goBuildModule, nodeBuildModule} {
b.setHealth(t, "n1", module)
}
b.serveForge(t, ctl)
b.serveInputVerb(t, ctl)
b.fake = &fakeBuildModules{js: js, ctx: ctx, calls: map[string]int{}}
b.fake.serve(t, "mesh-build-go", b.buckets[check.CheckerKeptBucket])
b.fake.serve(t, "mesh-build-node", b.buckets[check.CheckerKeptBucket])
b.report(t, "mesh-build-go", "n1", "go", "go1.27.1", "1")
b.report(t, "mesh-build-node", "n1", "typescript", "tsc5", "1")
b.fake.serve(t, goBuildModule, b.buckets[check.CheckerKeptBucket])
b.fake.serve(t, nodeBuildModule, b.buckets[check.CheckerKeptBucket])
b.report(t, goBuildModule, "n1", "go", "go1.27.1", "1")
b.report(t, nodeBuildModule, "n1", "typescript", "tsc5", "1")
b.setTrunk(t, map[string]string{repo: trunk})
return b
}
@@ -693,7 +697,11 @@ func (b *bed) report(t *testing.T, module, node, language, version, recipe strin
if _, err := b.tools[module].Put(b.ctx, key, data); err != nil {
t.Fatal(err)
}
if _, err := b.js.Publish(b.ctx, "mesh.stream."+module+".event.toolchain.reported", []byte(`{}`)); err != nil {
seat := "node-build-go"
if module == nodeBuildModule {
seat = "node-build-node"
}
if _, err := b.js.Publish(b.ctx, "mesh.seat."+seat+".event.toolchain-reported."+node, []byte(`{}`)); err != nil {
t.Fatal(err)
}
}
@@ -884,7 +892,7 @@ func TestCheckerJudgesACandidateAndOwesItsStatus(t *testing.T) { // N6/AC5, 1300
t.Errorf("owed = %+v", o)
}
// Two checks of the same heads: the one failed on m's build and the one it asked itself again; the second runs no test.
for tool, want := range map[string]int{"mesh-build-go.build_go_fetch": 2, "mesh-build-go.build_go_digest": 4, "mesh-build-go.build_go_test": 2} {
for tool, want := range map[string]int{goBuildModule + ".build_go_fetch": 2, goBuildModule + ".build_go_digest": 4, goBuildModule + ".build_go_test": 2} {
if got := b.fake.count(tool); got != want {
t.Errorf("%s called %d times, want %d", tool, got, want)
}
@@ -917,13 +925,13 @@ func TestACheckOrACarryOfEqualInputsRunsNothing(t *testing.T) { // 1287/AC5, 128
first := b.candidate(t, "feat/x", pull(repo, 7, head1))
b.start(t, nil)
b.verdict(t, first, ended)
tests, kept := b.fake.count("mesh-build-go.build_go_test"), keptCount(t, b)
tests, kept := b.fake.count(goBuildModule+".build_go_test"), keptCount(t, b)
second := b.candidate(t, "feat/y", pull(repo, 8, head2))
v := b.verdict(t, second, ended)
if v.State != meshdeliverycheckerclient.VerdictPass || v.Levels[buildstep.LevelUnit].Run != 0 || v.Levels[buildstep.LevelUnit].Kept != 2 || v.Levels[buildstep.LevelIntegration].Run != 0 {
t.Fatalf("verdict = %+v, want everything taken from the kept verdicts", v)
}
if got := b.fake.count("mesh-build-go.build_go_test"); got != tests {
if got := b.fake.count(goBuildModule + ".build_go_test"); got != tests {
t.Errorf("build_go_test called %d times, was %d: a test ran for equal inputs", got, tests)
}
if got := keptCount(t, b); got != kept {
@@ -935,14 +943,14 @@ func TestACheckOrACarryOfEqualInputsRunsNothing(t *testing.T) { // 1287/AC5, 128
b.verdict(t, third, ended)
// §5: the trunk moves; the first verdict is carried by its digests, nothing runs.
tests = b.fake.count("mesh-build-go.build_go_test")
tests = b.fake.count(goBuildModule + ".build_go_test")
const moved = "abababababababababababababababababababab"
b.setTrunk(t, map[string]string{repo: moved})
c := b.verdict(t, first, func(v meshdeliverychecker.VerdictsByGroupHeadsValue) bool { return ended(v) && v.Trunk[repo] == moved })
if c.State != meshdeliverycheckerclient.VerdictPass || c.CarriedFrom[repo] != trunk || c.Levels[buildstep.LevelUnit].Run != 0 || c.Levels[buildstep.LevelIntegration].Run != 0 {
t.Fatalf("the carried verdict = %+v, want a pass carried from %s with no test run", c, trunk[:12])
}
if got := b.fake.count("mesh-build-go.build_go_test"); got != tests {
if got := b.fake.count(goBuildModule + ".build_go_test"); got != tests {
t.Errorf("build_go_test called %d times on the carry, was %d: a carry of unchanged digests runs nothing", got, tests)
}
if o := b.owed(t, 7, head1, "success"); !strings.HasPrefix(o.Text, "pass 111111111111 on abababababab | carried from aaaaaaaaaaaa") {
@@ -992,7 +1000,7 @@ func TestACheckThatCannotBeJudgedFailsNamingWhy(t *testing.T) { // 1289/AC4, 192
!strings.Contains(o.Text, map[bool]string{true: row.words, false: "the checking program"}[row.program]) {
t.Errorf("owed = %+v", o)
}
if n := b.fake.count("mesh-build-go.build_go_digest") + b.fake.count("mesh-build-go.build_go_test"); (n != 0) != row.tests {
if n := b.fake.count(goBuildModule+".build_go_digest") + b.fake.count(goBuildModule+".build_go_test"); (n != 0) != row.tests {
t.Errorf("%d digest and test steps ran, want them run %v", n, row.tests)
}
if n := b.fake.dropped.Load(); n != row.dropped {
@@ -1170,7 +1178,7 @@ func TestATryIsJudgedAndItsEndWrittenWithNoStatusOwed(t *testing.T) {
if got := asked(); len(got) != 1 || !strings.HasPrefix(got[0].Candidate, id+".") {
t.Errorf("the placed node's engine was asked %+v, want the try's candidate %s.<heads>", got, id)
}
if b.fake.count("mesh-build-go.build_go_fetch") == 0 {
if b.fake.count(goBuildModule+".build_go_fetch") == 0 {
t.Errorf("the try fetched nothing")
}
// A whole try's start and end are said on the stream checks, which the indexer hears to write and delete the
@@ -1201,7 +1209,7 @@ func TestATryIsJudgedAndItsEndWrittenWithNoStatusOwed(t *testing.T) {
if err != nil {
t.Fatal(err)
}
steps, err := b.js.KeyValue(b.ctx, buildrun.StepsBucket("mesh-build-go"))
steps, err := b.js.KeyValue(b.ctx, buildrun.StepsBucket(goBuildModule))
if err != nil {
t.Fatal(err)
}
@@ -1265,3 +1273,72 @@ func TestTheCheckerSaysOwedReadableOnTheBus(t *testing.T) { // R2-1
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)
}
}
// buildSeatDefinitions are the seat definitions the bed's seat mesh-catalog lists: the build seats, as the mesh's own definitions of
// them, whose acting rules say which language each builds.
var buildSeatDefinitions = func() map[string]seatdef.Definition {
out := map[string]seatdef.Definition{}
for _, d := range seatlisttest.Definitions() {
if d.Seat == "node-build-go" || d.Seat == "node-build-node" {
out[d.Seat] = d
}
}
return out
}()
// seatDefinitionsAnswer is the seat mesh-catalog's answer to the seat list's asks: every trunk seat with its digest when no
// seat is named, one seat's definition when it is.
func seatDefinitionsAnswer(body []byte) []byte {
var ask struct {
Seat string `json:"seat"`
}
_ = json.Unmarshal(body, &ask)
entry := func(seat string) seatlist.Entry {
raw, _ := json.Marshal(buildSeatDefinitions[seat])
return seatlist.Entry{Seat: seat, Source: seatlist.SourceTrunk, Digest: fmt.Sprintf("%x", sha256.Sum256(raw)), Definition: raw}
}
var out any
if ask.Seat == "" {
l := seatlist.Listing{Revision: 1}
for _, seat := range slices.Sorted(maps.Keys(buildSeatDefinitions)) {
e := entry(seat)
e.Definition = nil
l.Seats = append(l.Seats, e)
}
out = l
} else if _, ok := buildSeatDefinitions[ask.Seat]; ok {
e := entry(ask.Seat)
out = seatlist.Answer{Seat: ask.Seat, Found: true, Entry: &e, Revision: 1}
} else {
out = seatlist.Answer{Seat: ask.Seat, Revision: 1}
}
raw, _ := json.Marshal(map[string]any{"result": out, "node": "ctl"})
return raw
}
// putBuildModule writes the indexer's module and manifest of a module that claims a build seat, so the checker finds the seat's
// holder (design 47 G A-82). Its repository is its own, so no repository the tests judge changes the build modules it names.
func (b *bed) putBuildModule(t *testing.T, module, seat string) {
t.Helper()
manifest := fmt.Sprintf(`{"module":%q,"version":"0.1","slug":"b","claims":[{"name":%q,"scope":"node"}]}`, module, seat)
digest := fmt.Sprintf("%x", sha256.Sum256([]byte(manifest)))
mkey, err := meshcatalogclient.ManifestsKey(digest)
if err != nil {
t.Fatal(err)
}
stored, err := meshcatalogclient.EncodeManifest(meshcatalog.ManifestsByDigestValue{Manifest: manifest, Module: module})
if err != nil {
t.Fatal(err)
}
if _, err := b.buckets[check.ManifestsBucket].Put(b.ctx, mkey, stored); err != nil {
t.Fatal(err)
}
value, err := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Repository: "novox/build-modules", Commit: strings.Repeat("b", 40), Path: "modules/" + module, Manifest: digest,
None2: "the fingerprint is computed with its chain"})
if err != nil {
t.Fatal(err)
}
if _, err := b.buckets[check.ModulesBucket].Put(b.ctx, module, value); err != nil {
t.Fatal(err)
}
}
@@ -5,6 +5,7 @@ import (
"encoding/json"
"errors"
"fmt"
"slices"
"sort"
"strconv"
"strings"
@@ -38,11 +39,13 @@ type judgeDeps struct {
renames check.Renames
seatDefs check.SeatDefinitions
// seats are the seat list the checker's reader holds, the base the checking program lays the heads over (seatlist.go).
seats check.SeatList
tools *buildrun.ToolchainSet
trunks *check.Trunks
kept check.KV
previews check.KV
seats check.SeatList
// buildSeats finds the build modules from the seat list and the holders of seats (design 47 G A-82).
buildSeats *buildrun.BuildSeats
tools *buildrun.ToolchainSet
trunks *check.Trunks
kept check.KV
previews check.KV
// candidates answers the indexer's candidate of a try's group (try_candidates.go); nil leaves a whole try with no
// manifest or composition judged, said.
candidates check.TryCandidates
@@ -232,7 +235,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, Candidates: d.candidates, 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, Renames: d.renames, SeatDefinitions: d.seatDefs, SeatList: d.seats, Inputs: d.inputs, BeginBound: d.beginBound, Logf: d.logf}
Kept: check.Kept{Store: d.kept}, Previews: d.previews, Candidates: d.candidates, 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, Renames: d.renames, SeatDefinitions: d.seatDefs, SeatList: d.seats, Seats: d.buildSeats, Inputs: d.inputs, BeginBound: d.beginBound, 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}
}
@@ -252,8 +255,11 @@ type repoModules struct {
logf func(format string, args ...any)
// verdicts keeps the indexer's verdict on each module's manifest, for the sandbox's composer (engines.go); nil keeps none.
verdicts *manifestVerdicts
// seats finds the build modules (design 47 G A-82); set before the first check.
seats *buildrun.BuildSeats
mu sync.Mutex
claims map[string][]string // by manifest digest: the seats its module claims
langs map[string]map[string]bool // by manifest digest: the languages of its artifacts
tsPkgs map[string][]string // by manifest digest: the names of its TypeScript package artifacts
path map[string]string // by module: its directory in its repository
@@ -338,10 +344,15 @@ func (r *repoModules) Put(ctx context.Context, key string, value []byte, deleted
return
}
set := map[string]bool{}
var pkgs []string
var pkgs, claimed []string
if mk, err := meshcatalogclient.ManifestsKey(m.Manifest); err == nil {
if raw, _, found, err := r.manifests.Get(ctx, mk); err == nil && found {
if kept, err := meshcatalogclient.DecodeManifest(raw); err == nil {
if man, err := parseManifest(kept.Manifest); err == nil {
for _, c := range man.Claims {
claimed = append(claimed, c.Name)
}
}
if man, err := parseManifest(kept.Manifest); err == nil && man.Build != nil {
for _, a := range man.Build.Artifacts {
if a.Language != "" {
@@ -360,6 +371,10 @@ func (r *repoModules) Put(ctx context.Context, key string, value []byte, deleted
r.mu.Lock()
r.langs[m.Manifest] = set
r.tsPkgs[m.Manifest] = pkgs
if r.claims == nil {
r.claims = map[string][]string{}
}
r.claims[m.Manifest] = claimed
r.mu.Unlock()
}
@@ -399,14 +414,35 @@ func (r *repoModules) PackagePaths(repository string) []string {
return out
}
// BuildModules are the build modules that judge a repository, sorted: the Go build module always, and the Node build
// module when a module of the repository builds TypeScript. A repository of which the mesh knows no module (mesh-lab, hq)
// is judged by both, each finding its own directories in the tree (go.mod files; package.json files), so a repository
// with tests in either language and no manifest is judged whole (issue 1887 A12).
func (r *repoModules) BuildModules(repository string) []string {
out := []string{goBuildModule}
// HolderOf is the module whose manifest claims the seat, as the indexer's modules hold them: none while no module of the mesh
// claims it, and the first by name where several do (design 47 G A-82, as the catalog half's holder is read by the builder).
func (r *repoModules) HolderOf(seat string) (string, bool) {
r.mu.Lock()
defer r.mu.Unlock()
found := ""
for module, digest := range r.typed {
if slices.Contains(r.claims[digest], seat) && (found == "" || module < found) {
found = module
}
}
return found, found != ""
}
// BuildModules are the build modules that judge a repository, sorted: the build module of Go always, and that of TypeScript
// when a module of the repository builds TypeScript. A repository of which the mesh knows no module (mesh-lab, hq)
// is judged by both, each finding its own directories in the tree (go.mod files; package.json files), so a repository
// with tests in either language and no manifest is judged whole (issue 1887 A12). A language no build seat on the seat list
// builds has none.
func (r *repoModules) BuildModules(repository string) []string {
var out []string
add := func(language string) {
if m, err := r.seats.For(language, ""); err == nil {
out = append(out, m.Module)
}
}
add("go")
r.mu.Lock()
typescript := false
known := false
for module, repo := range r.repoOf {
if repo != repository {
@@ -414,12 +450,13 @@ func (r *repoModules) BuildModules(repository string) []string {
}
known = true
if r.langs[r.typed[module]]["typescript"] {
out = append(out, nodeBuildModule)
typescript = true
break
}
}
if !known {
out = append(out, nodeBuildModule)
r.mu.Unlock()
if typescript || !known {
add("typescript")
}
sort.Strings(out)
return out
@@ -66,7 +66,7 @@ func TestARepositoryWithATypeScriptModuleIsJudgedByTheNodeBuildModuleToo(t *test
}
store[meshcatalogclient.ManifestDigest([]byte(m))] = data
}
r := &repoModules{manifests: store}
r := &repoModules{manifests: store, seats: testBuildSeats()}
putModule(t, r, "mesh-delivery-checker", "novox/mesh-delivery", goManifest)
putModule(t, r, "mesh-ui", "novox/mesh-catalog", tsManifest)
if got := r.BuildModules("novox/mesh-delivery"); strings.Join(got, ",") != goBuildModule {
@@ -221,3 +221,28 @@ func TestAManifestFallingBackToAPredefinedKeyIsRead(t *testing.T) {
t.Errorf("a waiting verdict is handed to the sandbox as %+v", v)
}
}
// The build modules are no names (design 47 G A-82): the holder of a seat is the module whose manifest, as the indexer's modules
// hold it, claims the seat.
func TestTheHolderOfASeatIsTheModuleWhoseManifestClaimsIt(t *testing.T) {
manifest := `{"module":"go-builds","version":"1","slug":"g","claims":[{"name":"node-build-go","scope":"node"}]}`
data, err := meshcatalogclient.EncodeManifest(meshcatalog.ManifestsByDigestValue{Module: "go-builds", Manifest: manifest})
if err != nil {
t.Fatal(err)
}
r := &repoModules{manifests: memManifests{meshcatalogclient.ManifestDigest([]byte(manifest)): data}}
putModule(t, r, "go-builds", "novox/builds", manifest)
if got, ok := r.HolderOf("node-build-go"); !ok || got != "go-builds" {
t.Errorf("the holder of node-build-go = %q, %v, want go-builds", got, ok)
}
if got, ok := r.HolderOf("node-build-node"); ok {
t.Errorf("the holder of a seat no manifest claims = %q, want none", got)
}
var found []string
var changed buildsWatch
changed.Set(func() { found = append(found, "changed") })
changed.Changed()
if len(found) != 1 {
t.Errorf("the build modules' change was not called through its watch: %v", found)
}
}
@@ -34,6 +34,7 @@ import (
"git.novox.be/novox/mesh-delivery/sdk/buildstep"
"git.novox.be/novox/mesh-sdk/go/announce"
"git.novox.be/novox/mesh-sdk/go/process"
"git.novox.be/novox/mesh-sdk/go/seatdef"
"git.novox.be/novox/mesh-sdk/go/seatlist"
"git.novox.be/novox/mesh-sdk/go/views"
)
@@ -160,7 +161,8 @@ 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: check.CheckerModule, Call: caller.Call, Logf: logger.Printf, Hub: hub}
seats := seatList{reader: followSeatList(seatlist.ProcessBus(caller.Call, hub), logger.Printf)}
var buildsChanged buildsWatch
seats := seatList{reader: followSeatList(seatlist.ProcessBus(caller.Call, hub), logger.Printf, buildsChanged.Changed)}
// 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]
@@ -188,6 +190,12 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
if followed != nil {
return followed
}
// The build modules are no names (design 47 G A-82): the holders of the build seats, the seat list's definitions and the
// holders the indexer's modules name by their manifests' claims, found at each use.
verdicts := &manifestVerdicts{}
repos := &repoModules{manifests: kvStore{manifests}, logf: logger.Printf, verdicts: verdicts}
buildSeats := buildrun.NewBuildSeats(func() map[string]seatdef.Definition { return definitionsOf(seats.reader) }, repos.HolderOf)
repos.seats = buildSeats
stepBound, err := durationEnv(getenv, "MESH_CHECKER_STEP_BOUND", check.DefaultStepBound)
if err != nil {
@@ -210,7 +218,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
if link == "" {
link = defaultLink
}
tools := &buildrun.ToolchainSet{Seats: checkerBuildSeats}
tools := &buildrun.ToolchainSet{Seats: buildSeats}
presence := &buildrun.PresenceSet{}
trunks := &check.Trunks{}
// A check is one ask per pull request and build module on the build seats' check stream, pulled by a build node with a
@@ -228,7 +236,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
defer hearing.Stop()
// Each ask's step is heard by its own watch and, at every step end of its build module and every reconnect, read
// (buildrun.HearEnds): a watch that goes deaf loses no end (the lost check of 2026-10-28).
heard := buildrun.HearEnds(ctx, asks, asks, []string{goBuildModule, nodeBuildModule}, logger.Printf)
heard := buildrun.HearEnds(ctx, asks, asks, buildSeats.Modules(), logger.Printf)
conn.SetReconnectHandler(func(*nats.Conn) { hearing.Again(); heard.Nudge("") })
waiter := buildrun.LostAskWaiter{Inner: heard, Dead: dead, Hearing: hearing}
asksKV := process.AwaitBucket(ctx, js, conn, check.AsksBucket, "checker_ask_again refuses every check, as it cannot tell whether its ask waits in a stream", logger.Printf)
@@ -279,13 +287,12 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
}
answerBound := set.answer
holdCallsFor(answerBound)
verdicts := &manifestVerdicts{}
compositions := check.EngineCompositions{
Requests: check.RequestKeeper{Store: requestsKV},
Asker: engineAsks{call: caller.CallOn},
Placed: placedNodes{kv: func(context.Context) (jetstream.KeyValue, error) { return assignments, nil }}.Placed,
Bound: answerBound,
Sandbox: seatSandbox{asker: buildrun.Asker{Seats: checkerBuildSeats, Publisher: asks, Waiter: waiter, Bound: stepBound, Dropper: buildrun.SeatDrops{Caller: caller}}},
Sandbox: seatSandbox{asker: buildrun.Asker{Seats: buildSeats, Publisher: asks, Waiter: waiter, Bound: stepBound, Dropper: buildrun.SeatDrops{Caller: caller}}},
Verdicts: verdicts.all,
// The node-engine's trunk head: a group with no node-engine member composes with the composer built there (ADR 0656 §5).
Composer: func() string { return trunks.Heads()[check.NodeEngineRepository] },
@@ -304,7 +311,6 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
logger.Printf("%s of %s was written and the part that reads it could not be told: %v", event, key, err)
}
}
repos := &repoModules{manifests: kvStore{manifests}, logf: logger.Printf, verdicts: verdicts}
compositions.Composer = func() string { return repos.CommitOf(check.NodeEngineModule) }
compositions.Manifests = repos
// The indexer's packages state (ADR 0593 §2), so a Go check builds every mesh package at its published version: read
@@ -314,7 +320,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
ch.Runs = repos // the node-engine a check reads beside at its running commit (ADR 0548 §6)
ch.Says = verdictSays{js: js, node: cred.Node, now: time.Now} // a pull request's verdict event, on the stream asks (design 26 *Out of step*)
ch.Judge = newJudge(judgeDeps{
asks: buildrun.Asker{Seats: checkerBuildSeats, Publisher: asks, Waiter: waiter, Bound: stepBound, Dropper: buildrun.SeatDrops{Caller: caller}}, tools: tools, trunks: trunks,
asks: buildrun.Asker{Seats: buildSeats, Publisher: asks, Waiter: waiter, Bound: stepBound, Dropper: buildrun.SeatDrops{Caller: caller}}, tools: tools, trunks: trunks,
kept: kvStore{buckets[check.CheckerKeptBucket]}, previews: kvStore{buckets[check.PreviewsBucket]}, candidates: tryCandidates{candidates},
repos: repos, published: published, builds: buildStates, ends: ends, buildWait: set.buildWait, forge: forgeReads{call: caller.Call}, logf: logger.Printf,
announce: announce, compositions: compositions, inputs: runInputs{awaitedKV{inputsKV}}, beginBound: beginBound,
@@ -323,15 +329,16 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
renames: seatRenames{call: caller.Call},
seatDefs: seatDefinitionsOf(seatDefs),
seats: seats,
buildSeats: buildSeats,
})
ch.Announce = announce
tryer.Judge = ch.Judge
// The toolchains first, so a check that starts from the first candidate finds the nodes.
watch := newToolchainWatch(ctx, js, tools, ch.NodesChanged, logger)
watch := newToolchainWatch(ctx, js, buildSeats, tools, ch.NodesChanged, logger)
// Only the toolchain reports of nodes the catalogue places the build module on count (issue 1940, ADR 0549 §3): the
// assignments is settled first, so the toolchains settle through the gate already knowing it.
gate := buildrun.NewToolchainGate([]string{goBuildModule, nodeBuildModule}, tools.Put, logger.Printf)
gate := buildrun.NewToolchainGate(buildSeats.Modules(), tools.Put, logger.Printf)
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)
@@ -351,22 +358,26 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
if err := watch.start(); err != nil {
return err
}
// A build module says each time it wrote its toolchains key: its stream, and the checker's durable consumer on it, are
// made by the controller. Where the stream is not there yet the checker says so and is started again with the build
// module.
for _, module := range []string{goBuildModule, nodeBuildModule} {
stream := toolchainStreams[module]
cons, err := cred.EventConsumer(ctx, js, stream)
if err != nil {
logger.Printf("the consumer %s_%s on %s does not exist yet, so %s's reports are not heard until the checker starts again: %v", cred.Node, cred.Module, stream, module, err)
continue
}
cc, err := process.HearEvents(cons, func(m jetstream.Msg) { watch.Reported(m) }, logger.Printf)
// A build seat says each time a node reported its toolchain (its event toolchain-reported, design 47 G §492): the bucket of
// the seat's holder is watched then, if it is not (Reported), and again when the seat list or the holders change (below).
for _, lang := range []string{buildstep.AskGo, buildstep.AskNode} {
seat, err := buildstep.BuildSeat(lang)
if err != nil {
return err
}
defer cc.Stop()
if err := hub.Handle("mesh.seat."+seat+".event.toolchain-reported.>", func(m jetstream.Msg) { watch.Reported(m) }); err != nil {
return err
}
}
// The build modules are named by the seat list and the indexer's modules: a change of either watches the steps and toolchains of
// a build module not watched yet and tells the gate which modules' reports it passes on.
buildsChanged.Set(func() {
heard.Watch(buildSeats.Modules())
gate.UseModules(buildSeats.Modules())
if err := watch.start(); err != nil {
logger.Printf("the toolchains of a build module cannot be watched: %v", err)
}
})
// The node-engine seat's running.<node>.<module>, kept last per subject (design 47): a part a check could not hear runs,
// and the check is asked again (issue 1373). Its stream and the checker's durable consumer on it are made from the
// manifest's consumes; where they are not there yet the checker says so, and a check that could not run waits for its
@@ -419,10 +430,12 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
if err != nil {
return err
}
buildsChanged.Changed() // the holders of the build seats are known from here
go func() {
errs <- follow(ctx, mw, func(e jetstream.KeyValueEntry) {
before := repos.CommitOf(check.NodeEngineModule)
repos.Put(ctx, e.Key(), e.Value(), !opOf(e))
buildsChanged.Changed() // a holder of a build seat may have moved
if after := repos.CommitOf(check.NodeEngineModule); after != before {
ch.TrunkMoved(ctx) // the node-engine the mesh runs moved: a verdict that read it beside is carried (ADR 0548 §6)
}
@@ -20,11 +20,11 @@ import (
// its grant. The gate over the seat's assignments, and its refusal, are mesh-sdk's (buildrun.ToolchainGate,
// issue 774).
func TestARefusedBucketIsSaidAndThePartGoesOn(t *testing.T) {
set := &buildrun.ToolchainSet{Seats: checkerBuildSeats}
set := &buildrun.ToolchainSet{Seats: testBuildSeats()}
var logs bytes.Buffer
logger := log.New(&logs, "", 0)
refused := refusingBus{err: errors.New("nats: permissions violation for publish to $JS.API.CONSUMER.CREATE.KV_mesh-build-node_toolchains")}
if err := newToolchainWatch(context.Background(), refusingBus{refuseOpen: true, err: refused.err}, set, func(context.Context) {}, logger).start(); err != nil {
if err := newToolchainWatch(context.Background(), refusingBus{refuseOpen: true, err: refused.err}, testBuildSeats(), set, func(context.Context) {}, logger).start(); err != nil {
t.Errorf("a refused toolchains bucket stopped the start: %v", err)
}
if !strings.Contains(logs.String(), "cannot be read") {
@@ -5,8 +5,10 @@ import (
"fmt"
"maps"
"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"
)
@@ -16,9 +18,9 @@ import (
// 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 {
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) }})
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)
}
@@ -58,3 +60,26 @@ func readIfUnread(reader *seatlist.Reader) {
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.
type buildsWatch struct{ fn atomic.Pointer[func()] }
// Set takes the function in.
func (b *buildsWatch) Set(f func()) { b.fn.Store(&f) }
// Changed calls it, none before it is set.
func (b *buildsWatch) Changed() {
if f := b.fn.Load(); f != nil {
(*f)()
}
}
// definitionsOf are the seat definitions the reader holds, none before it has answered.
func definitionsOf(reader *seatlist.Reader) map[string]seatdef.Definition {
if reader == nil {
return nil
}
return reader.Definitions()
}
@@ -27,7 +27,7 @@ func (b *silentThenAnswering) Subscribe(string, func(string, json.RawMessage) er
// 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)
reader := followSeatList(bus, t.Logf, func() {})
<-reader.Ready()
if reader.Status().Heard {
t.Fatal("a silent seat was heard")
@@ -5,19 +5,19 @@ import (
"git.novox.be/novox/mesh-sdk/go/seatdef"
)
// The checker's names of the build modules, kept until its part finds them on the seat list as the builder does (design 47 G
// A-82 is the builder's change; the builder's package holds no name of them any more): the holders of the three build seats.
// The holders of the build seats in the tests' seat list: names the checker's code does not know, found by the seats' acting
// rules (design 47 G A-82).
const (
goBuildModule = "mesh-build-go"
nodeBuildModule = "mesh-build-node"
)
// checkerBuildSeats is the finder over the build seats those names hold.
var checkerBuildSeats = func() *buildrun.BuildSeats {
// testBuildSeats is the finder over the build seats of the tests' seat list.
func testBuildSeats() *buildrun.BuildSeats {
rule := func(on string) seatdef.Definition {
return seatdef.Definition{Acts: []seatdef.ActingRule{{Step: seatdef.StepBuild, On: on}}}
}
defs := map[string]seatdef.Definition{"node-build-go": rule("go"), "node-build-node": rule("typescript")}
holders := map[string]string{"node-build-go": goBuildModule, "node-build-node": nodeBuildModule}
return buildrun.NewBuildSeats(func() map[string]seatdef.Definition { return defs }, func(seat string) (string, bool) { h, ok := holders[seat]; return h, ok })
}()
}
@@ -15,18 +15,15 @@ import (
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
)
// toolchainStreams are the stream each build module carries its toolchain.reported event on, as the bus names it
// (declared by the build modules, heard by this module through consumes).
var toolchainStreams = map[string]string{goBuildModule: "TOOLCHAINS_GO", nodeBuildModule: "TOOLCHAINS_NODE"}
// toolchainWatch keeps the build modules' toolchains in a buildrun.ToolchainSet and tells the builder when one changed. A
// build module's bucket is made when it is assigned, after the builder started, and the builder holds no timer (ADR 0437
// §8): it watches the buckets that exist at start, and a build module says each time it wrote its toolchains key
// (toolchain.reported, handled by Reported); a bucket not watched yet is then watched.
type toolchainWatch struct {
ctx context.Context
js bucketOpener
set *buildrun.ToolchainSet
ctx context.Context
js bucketOpener
seats *buildrun.BuildSeats // the build modules: the holders of the build seats (design 47 G A-82)
set *buildrun.ToolchainSet
// put takes a change of a toolchains bucket in: set.Put, or the gate of the assigned build nodes before it (issue 1940).
put func(module, key string, value []byte, deleted bool) bool
changed func(ctx context.Context) // a language's entries changed: the failed builds are tried again
@@ -36,13 +33,13 @@ type toolchainWatch struct {
watched map[string]bool
}
func newToolchainWatch(ctx context.Context, js bucketOpener, set *buildrun.ToolchainSet, changed func(context.Context), logger *log.Logger) *toolchainWatch {
return &toolchainWatch{ctx: ctx, js: js, set: set, put: set.Put, changed: changed, logger: logger, errs: make(chan error, len(checkerBuildSeats.Modules())), watched: map[string]bool{}}
func newToolchainWatch(ctx context.Context, js bucketOpener, seats *buildrun.BuildSeats, set *buildrun.ToolchainSet, changed func(context.Context), logger *log.Logger) *toolchainWatch {
return &toolchainWatch{ctx: ctx, js: js, seats: seats, set: set, put: set.Put, changed: changed, logger: logger, errs: make(chan error, 16), watched: map[string]bool{}}
}
// start watches the buckets that exist now; a missing one is said and waits for its build module's event.
func (w *toolchainWatch) start() error {
for _, module := range checkerBuildSeats.Modules() {
for _, module := range w.seats.Modules() {
if err := w.ensure(module); err != nil {
return err
}
@@ -91,13 +88,15 @@ type delivered interface {
Term() error
}
// Reported takes one toolchain.reported event of a build module in: the module is read off the subject,
// mesh.stream.<module>.event.toolchain.reported, and its bucket is watched if it is not. A subject of no build module is
// terminated; a failure stays unacknowledged, and the server delivers it again.
// Reported takes one toolchain-reported event of a build seat in: the seat is read off the subject,
// mesh.seat.<seat>.event.toolchain-reported.<node>, and the bucket of its holder is watched if it is not. A subject of no build
// seat with a holder is terminated; a failure stays unacknowledged, and the server delivers it again.
func (w *toolchainWatch) Reported(m delivered) {
module, _, _ := strings.Cut(strings.TrimPrefix(m.Subject(), "mesh.stream."), ".event.")
if _, known := checkerBuildSeats.LanguageOf(module); !known || !strings.HasPrefix(m.Subject(), "mesh.stream.") {
w.logger.Printf("%s: not a build module's event, dropped", m.Subject())
seat, _, _ := strings.Cut(strings.TrimPrefix(m.Subject(), "mesh.seat."), ".event.")
bm, known := w.seats.OfSeat(seat)
module := bm.Module
if !known || module == "" || bm.Toolchain == "" || !strings.HasPrefix(m.Subject(), "mesh.seat.") {
w.logger.Printf("%s: not the event of a build seat with a holder, dropped", m.Subject())
_ = m.Term()
return
}
@@ -74,13 +74,16 @@ func (st *state) buildsOfTheCheck(ctx context.Context) (map[string]string, []str
if !readable {
return wait, words
}
for _, bm := range slices.Sorted(maps.Keys(languageOf)) {
for _, bm := range st.j.Seats.Modules() {
if st.j.languageOf(bm) == "" {
continue
}
if _, alters := changed[bm]; !alters {
continue
}
var built []string
for module, m := range changed {
if _, isBuildModule := languageOf[module]; !isBuildModule && buildsWith(m, languageOf[bm]) {
if isBuildModule := st.j.languageOf(module) != ""; !isBuildModule && buildsWith(m, st.j.languageOf(bm)) {
built = append(built, module)
delete(wait, module)
}
@@ -1,11 +0,0 @@
package check
// The checker's names of the build modules, kept until its part finds them on the seat list as the builder does (design 47 G
// A-82 is the builder's change; the builder's package holds no name of them any more).
const (
goBuildModule = "mesh-build-go"
nodeBuildModule = "mesh-build-node"
)
// languageOf is the language each build module's toolchain reports hold.
var languageOf = map[string]string{goBuildModule: "go", nodeBuildModule: "typescript"}
@@ -0,0 +1,56 @@
package check
import (
"git.novox.be/novox/mesh-delivery/sdk/buildstep"
"mesh-delivery-checker/rules"
)
// **The checker names no build module** (design 47 G A-82): a build module is the holder of a build seat found on the seat list
// (buildrun.BuildSeats), by the language of the toolchain its seat's acting rule `build` names. The checker's own logic keys on
// that language: the Go repositories and the manifests are judged by the build module of Go, a TypeScript module's by that of
// TypeScript.
// Languages of the toolchains the checker's logic is keyed on (the `on` of the build seats' acting rules).
const (
languageGo = rules.LanguageGo
languageTypeScript = rules.LanguageTypeScript
)
// buildModuleOf is the module that holds the build seat building a language's artifacts; the seat's name while no module is
// known to hold it or the seat list is not read, so a failure always names where it is.
func (j *Examiner) buildModuleOf(language string) string {
if m, err := j.Seats.For(language, ""); err == nil {
return m.Module
}
for _, m := range j.Seats.All() {
if m.Toolchain == language {
return m.Seat
}
}
ask := buildstep.AskGo
if language == languageTypeScript {
ask = buildstep.AskNode
}
seat, _ := buildstep.BuildSeat(ask)
return seat
}
// goModule is the build module the Go repositories are judged by.
func (j *Examiner) goModule() string { return j.buildModuleOf(languageGo) }
// goModule is the build module the manifests, the module graph and the checking program are judged by.
func (st *state) goModule() string { return st.j.goModule() }
// languageOf is the language of the toolchain a build module builds with, empty for a module no build seat names.
func (j *Examiner) languageOf(module string) string {
l, _ := j.Seats.LanguageOf(module)
return l
}
// askLanguageOf is the language of the asks the module's build seat takes (buildstep.AskGo, buildstep.AskNode, ...).
func (j *Examiner) askLanguageOf(module string) string {
if m, ok := j.Seats.OfModule(module); ok {
return m.AskLanguage()
}
return ""
}
@@ -1,11 +1,20 @@
package check
import (
"testing"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
"git.novox.be/novox/mesh-sdk/go/seatdef"
)
// testSeats is the finder over the build seats the checker's names hold (buildmodules.go).
// The holders of the build seats in the tests' seat list: names the checker's code does not know, found by the seats'
// acting rules (design 47 G A-82).
const (
goBuildModule = "mesh-build-go"
nodeBuildModule = "mesh-build-node"
)
// testSeats is the finder over the build seats of the tests' seat list.
func testSeats() *buildrun.BuildSeats {
rule := func(on string) seatdef.Definition {
return seatdef.Definition{Acts: []seatdef.ActingRule{{Step: seatdef.StepBuild, On: on}}}
@@ -14,3 +23,36 @@ func testSeats() *buildrun.BuildSeats {
holders := map[string]string{"node-build-go": goBuildModule, "node-build-node": nodeBuildModule}
return buildrun.NewBuildSeats(func() map[string]seatdef.Definition { return defs }, func(seat string) (string, bool) { h, ok := holders[seat]; return h, ok })
}
// stepStatesOf is a Reader's StepStates over fixed states.
func stepStatesOf(m map[string]StateRead) func() map[string]StateRead {
return func() map[string]StateRead { return m }
}
// The checker names no build module (design 47 G A-82): it finds each by the language its seat's acting rule builds, whatever the
// holder is called, and says that holder's name in what it judges.
func TestTheBuildModulesAreFoundOnTheSeatListWhateverTheyAreCalled(t *testing.T) {
rule := func(on string) seatdef.Definition {
return seatdef.Definition{Acts: []seatdef.ActingRule{{Step: seatdef.StepBuild, On: on}}}
}
defs := map[string]seatdef.Definition{"node-build-go": rule("go"), "node-build-node": rule("typescript"), "node-build-image": rule("kind:image")}
holders := map[string]string{"node-build-go": "compilers", "node-build-node": "bundlers"}
j := &Examiner{Seats: buildrun.NewBuildSeats(func() map[string]seatdef.Definition { return defs }, func(seat string) (string, bool) { h, ok := holders[seat]; return h, ok })}
if got := j.goModule(); got != "compilers" {
t.Errorf("the module that judges Go = %q, want its seat's holder", got)
}
if got, lang, ask := j.buildModuleOf(languageTypeScript), j.languageOf("bundlers"), j.askLanguageOf("bundlers"); got != "bundlers" || lang != languageTypeScript || ask != "node" {
t.Errorf("TypeScript: module %q, language %q, ask language %q", got, lang, ask)
}
if got := j.languageOf("mesh-build-go"); got != "" {
t.Errorf("a module that holds no build seat builds with %q", got)
}
if !changesBuildGo(catalogueRepository, "compilers", []string{"modules/compilers/main.go"}) || changesBuildGo(catalogueRepository, "compilers", []string{"modules/mesh-build-go/main.go"}) {
t.Error("a change under the Go build module's own directory is the one that changes it")
}
// With no seat on the list the failure still names where it is: the seat.
none := &Examiner{Seats: buildrun.NewBuildSeats(func() map[string]seatdef.Definition { return defs }, nil)}
if got := none.goModule(); got != "node-build-go" {
t.Errorf("a build seat no module holds is named by the seat, got %q", got)
}
}
@@ -279,7 +279,7 @@ func newBench(t testing.TB) *bench {
now := time.Date(2026, 10, 21, 19, 0, 0, 0, time.UTC)
b.asks = &buildrun.FakeAsks{Answer: b.answer}
b.ex = &Examiner{Asks: buildrun.Asker{Seats: testSeats(), Publisher: b.asker(nil, nil), Waiter: b.asks, Bound: 5 * time.Second}, Toolchains: goEntries("shanks"), Kept: Kept{Store: b.kept}, Previews: b.prev,
Modules: langs{}, Trunks: &Trunks{heads: map[string]string{DeliveryRepository: trunkD}}, Inputs: memInputs{b.inputs}, Now: func() time.Time { now = now.Add(50 * time.Millisecond); return now }}
Modules: langs{}, Seats: testSeats(), Trunks: &Trunks{heads: map[string]string{DeliveryRepository: trunkD}}, Inputs: memInputs{b.inputs}, Now: func() time.Time { now = now.Add(50 * time.Millisecond); return now }}
return b
}
@@ -28,7 +28,7 @@ func TestAFailuresWordsAreItsWholeMessageWithinTheirBound(t *testing.T) {
test func(t *testing.T)
}{
{"a failed tests verdict keeps its whole failure message", func(t *testing.T) { // 1901/AC1
v := rules.LevelFailure("", "", "unit", Tested{Failed: []Failure{fatalAfterLog()}})
v := rules.LevelFailure("", "", "", "unit", Tested{Failed: []Failure{fatalAfterLog()}})
for _, want := range []string{"3 subscription(s) derive to a subject no emitter owns:", "a.b.c", "d.e.f", "g.h.i"} {
if !strings.Contains(v.Failed.Words, want) {
t.Errorf("words lack %q: %q", want, v.Failed.Words)
@@ -39,7 +39,7 @@ func TestAFailuresWordsAreItsWholeMessageWithinTheirBound(t *testing.T) {
}
}},
{"the status line names the failure not the first logged line", func(t *testing.T) { // 1901/AC2
v := rules.LevelFailure("", "", "unit", Tested{Failed: []Failure{fatalAfterLog()}})
v := rules.LevelFailure("", "", "", "unit", Tested{Failed: []Failure{fatalAfterLog()}})
got := firstFailed(*v)
if !strings.Contains(got, "3 subscription(s) derive to a subject no emitter owns:") || strings.Contains(got, "judged against") || strings.Contains(got, "\n") {
t.Errorf("firstFailed = %q", got)
@@ -47,14 +47,14 @@ func TestAFailuresWordsAreItsWholeMessageWithinTheirBound(t *testing.T) {
// With no cause told apart (an older build module), the words still start at the first line, as before.
f := fatalAfterLog()
f.Cause = ""
v = rules.LevelFailure("", "", "unit", Tested{Failed: []Failure{f}})
v = rules.LevelFailure("", "", "", "unit", Tested{Failed: []Failure{f}})
if !strings.HasPrefix(afterHid(v.Failed.Words), "agreement_catalogue_test.go:130") {
t.Errorf("words = %q", v.Failed.Words)
}
}},
{"words longer than the bound keep their last lines", func(t *testing.T) { // 1901/AC1
f := Failure{Output: strings.Repeat("log line of some length\n", 500) + "the cause is here"}
v := rules.LevelFailure("", "", "unit", Tested{Failed: []Failure{f}})
v := rules.LevelFailure("", "", "", "unit", Tested{Failed: []Failure{f}})
if !strings.HasSuffix(v.Failed.Words, "the cause is here") || len(v.Failed.Words) > rules.MaxWords+200 {
t.Errorf("%d bytes, ends %q", len(v.Failed.Words), v.Failed.Words[len(v.Failed.Words)-30:])
}
@@ -70,7 +70,7 @@ func TestAFailuresWordsAreItsWholeMessageWithinTheirBound(t *testing.T) {
if err := json.Unmarshal([]byte(old), &f); err != nil || f.Cause != "" {
t.Fatalf("1901/AC3: err = %v, cause = %q", err, f.Cause)
}
v := rules.LevelFailure("", "", "unit", Tested{Failed: []Failure{f}})
v := rules.LevelFailure("", "", "", "unit", Tested{Failed: []Failure{f}})
if afterHid(v.Failed.Words) != "x_test.go:1: logged\nx_test.go:2: the old kept lines" {
t.Errorf("1901/AC3: words = %q", v.Failed.Words)
}
@@ -88,7 +88,7 @@ func TestAFailuresWordsAreItsWholeMessageWithinTheirBound(t *testing.T) {
{"a real cause", "mesh-build-node", by, "one plus one is not three", "not ok 3 - fails", ""},
} {
t.Run(c.name, func(t *testing.T) {
v := rules.LevelFailure("", c.module, "unit", Tested{By: c.by, Failed: []Failure{{Package: "modules/gitea", Test: "fails", Cause: c.cause, Output: c.output}}})
v := rules.LevelFailure("", c.module, "", "unit", Tested{By: c.by, Failed: []Failure{{Package: "modules/gitea", Test: "fails", Cause: c.cause, Output: c.output}}})
if v.State != meshdeliverycheckerclient.VerdictFail {
t.Errorf("1901/AC8: the verdict is %q, want it failed", v.State)
}
@@ -51,7 +51,7 @@ func (st *state) gatherGraph(ctx context.Context) {
}
change, err := read(req.Cases[0].Manifests)
if err != nil {
st.fail(goModule, "the change's module graph cannot be worked out, so no cycle can be ruled out: "+err.Error())
st.fail(st.goModule(), "the change's module graph cannot be worked out, so no cycle can be ruled out: "+err.Error())
return
}
g := rules.Graph{}
@@ -73,7 +73,7 @@ func (st *state) gatherGraph(ctx context.Context) {
g.Edges = append(g.Edges, rules.Edge{From: pair[0], To: pair[1], Kind: trunk[pair], Only: rules.EdgeTrunk})
}
}
st.facts = append(st.facts, rules.Step{Graph: &g})
st.facts = append(st.facts, rules.Step{Module: st.goModule(), Graph: &g})
}
func cmpPair(a, b [2]string) int {
@@ -18,25 +18,22 @@ import (
"git.novox.be/novox/mesh-delivery/sdk/buildstep"
)
// goModule is the build module the Go repositories are judged by.
const goModule = goBuildModule
// RunnerWorkspace is the runner the Go build module's digest and test steps are asked with when the merge changes the build
// module itself: the installed module compiles the candidate's program from the merged tree and runs the step as it, shut
// off (ADR 0535 §1).
const RunnerWorkspace = "workspace"
// buildGoDir is the Go build module's directory in the catalogue: a merge changing a path under it is judged by its
// candidate (ADR 0535 §1). mesh-build-node's own directory is judged by the installed module until its part of issue 1898.
const buildGoDir = "modules/mesh-build-go/"
// candidate (ADR 0535 §1). The other build modules' own directories are judged by the installed module until its part of issue 1898.
func buildGoDir(goModule string) string { return "modules/" + goModule + "/" }
// changesBuildGo says whether a merge of the repository with these changed paths changes the Go build module.
func changesBuildGo(repository string, changed []string) bool {
if repository != catalogueRepository {
func changesBuildGo(repository, goModule string, changed []string) bool {
if repository != catalogueRepository || goModule == "" {
return false
}
for _, p := range changed {
if strings.HasPrefix(p, buildGoDir) {
if strings.HasPrefix(p, buildGoDir(goModule)) {
return true
}
}
@@ -44,11 +41,11 @@ func changesBuildGo(repository string, changed []string) bool {
}
// The words of the line on the program that judged a change to the Go build module (ADR 0535 §2, §5).
const (
judgedByOldInstalled = "judged by the installed mesh-build-go: it does not run a candidate yet"
judgedByInstalled = "judged by the installed mesh-build-go"
judgedByCandidate = "judged by the candidate's mesh-build-go at "
)
func judgedByOldInstalled(module string) string {
return "judged by the installed " + module + ": it does not run a candidate yet"
}
func judgedByInstalled(module string) string { return "judged by the installed " + module }
func judgedByCandidate(module string) string { return "judged by the candidate's " + module + " at " }
// Asks puts a check's ask on its build seat's stream and waits for its step (buildrun.Asker; ADR 0525 §1, §6): the
// checker chooses no node, starts no step and holds none; whichever build node pulls the ask runs the check.
@@ -68,9 +65,6 @@ type BuildModules interface {
BuildModules(repository string) []string
}
// askLanguage is the build seat language of each build module the checker asks.
var askLanguage = map[string]string{goBuildModule: buildstep.AskGo, nodeBuildModule: buildstep.AskNode}
// Examiner judges a case through the build seats (ADR 0484 clauses 3, 4, 10, 13; ADR 0525 §1, §3): for each pull request
// and build module it puts one check ask whose job fetches the head merged onto its trunk head, then runs the unit level
// and the integration level, taking the kept verdicts of unchanged packages and running the change's own tests first;
@@ -87,6 +81,8 @@ type Examiner struct {
// Previews is the previews bucket; nil writes none.
Previews KV
Modules BuildModules
// Seats finds the build modules from the seat list and the holders of seats (design 47 G A-82).
Seats *buildrun.BuildSeats
// Runs says which commit the mesh runs of a module (the indexer's modules); nil knows none.
Runs interface{ CommitOf(module string) string }
// Forge reads the forge's files for the siblings a repository's tests read; nil reads none.
@@ -217,8 +213,10 @@ func besideLines(refs []BesideRef) []string {
// target is a pull request on one build module: the node that pulled its ask, the workspace its fetch made and what it
// said.
type target struct {
pull Pull
module string
pull Pull
module string
// language is the language of the toolchain the build module builds with (the `on` of its seat's acting rule).
language string
node string
toolchain string
// agreed is the toolchain the ask names, so a node off it declines the check (ADR 0584); nil when none is agreed.
@@ -243,12 +241,12 @@ type target struct {
func (t *target) judgedBy(answer string) bool {
switch {
case strings.HasPrefix(answer, "candidate "):
t.judged = judgedByCandidate + short12(strings.TrimPrefix(answer, "candidate "))
t.judged = judgedByCandidate(t.module) + short12(strings.TrimPrefix(answer, "candidate "))
return true
case answer == "":
t.judged, t.runner = judgedByOldInstalled, ""
t.judged, t.runner = judgedByOldInstalled(t.module), ""
default:
t.judged, t.runner = judgedByInstalled, ""
t.judged, t.runner = judgedByInstalled(t.module), ""
}
return false
}
@@ -544,18 +542,18 @@ func (st *state) judgeAll(ctx context.Context) {
var merged []Fetched
modules := st.j.Modules.BuildModules(p.Repository)
for _, bm := range modules {
t := &target{pull: p, module: bm}
if tc, none := buildstep.AgreedToolchain(st.j.Toolchains.Entries(languageOf[bm])); none == "" {
t := &target{pull: p, module: bm, language: st.j.languageOf(bm)}
if tc, none := buildstep.AgreedToolchain(st.j.Toolchains.Entries(t.language)); none == "" {
t.toolchain = tc.Version
t.agreed = &buildstep.ToolchainUsed{Language: languageOf[bm], Version: tc.Version, Recipe: tc.Recipe}
t.agreed = &buildstep.ToolchainUsed{Language: t.language, Version: tc.Version, Recipe: tc.Recipe}
}
args := buildstep.Fetch{Repository: p.Repository, Commit: p.Head, Ref: refOf(p.Number), Base: p.Trunk}
if bm == nodeBuildModule {
if t.language == languageTypeScript {
if groupPackage != nil && p.Repository != SDKRepository {
args.Package = groupPackage
}
}
if bm == goBuildModule {
if t.language == languageGo {
beside, err := st.j.besideFor(ctx, st.c, p)
if err != nil {
st.fail(forgeMachine(err, bm), err.Error()+unheardWords(err))
@@ -632,7 +630,7 @@ func (st *state) askNeeding(ctx context.Context, t *target, merged *[]Fetched, l
job.NodeNames = names
}
if t.runner != "" {
job.Runner, job.RunnerWhen = t.runner, buildGoDir
job.Runner, job.RunnerWhen = t.runner, buildGoDir(st.goModule())
}
if st.c.Try != nil && st.c.Try.Run != "" {
job.Run, job.Kept = st.c.Try.Run, false // a try of some tests runs them alone: a partial run proves no package
@@ -641,7 +639,7 @@ func (st *state) askNeeding(ctx context.Context, t *target, merged *[]Fetched, l
if st.c.Kind == KindTrunk {
kind = buildstep.AskRecheck // a re-judge after a trunk move goes after every new head's check (ADR 0536 §4)
}
a := buildstep.StepAsk{ID: buildstep.NewID(), Kind: kind, Language: askLanguage[t.module], Asker: CheckerModule,
a := buildstep.StepAsk{ID: buildstep.NewID(), Kind: kind, Language: st.j.askLanguageOf(t.module), Asker: CheckerModule,
At: st.j.now().UTC(), Check: job, Needs: needs}
if a.Language == buildstep.AskGo {
a.Packages = st.packagesOf(p.Repository)
@@ -730,7 +728,7 @@ func (st *state) askNeeding(ctx context.Context, t *target, merged *[]Fetched, l
return
}
t.changed = t.fetched.Changed
if !changesBuildGo(p.Repository, t.changed) {
if !changesBuildGo(p.Repository, st.goModule(), t.changed) {
t.runner = "" // the build module used no runner: the merge does not change it
}
if !placed { // the placed ask fetched the same merge: its preview was read from the first
@@ -797,16 +795,16 @@ func unmetOf(levels []buildstep.LevelAnswer) string {
// one that did not pass, so the verdict names every failed test of the level (issue 1921). It answers the level's
// running time (its tests' and setup's).
func (st *state) level(ctx context.Context, t *target, la buildstep.LevelAnswer) float64 {
if t.runner != "" && t.module == goBuildModule && la.Digest != nil {
if t.runner != "" && t.language == languageGo && la.Digest != nil {
t.judgedBy(la.Digest.JudgedBy) // the installed module judged it: the runner is not used again
}
if t.runner != "" && la.Judged != "" {
t.judgedBy(la.Judged) // the candidate judged the level: nothing is kept (ADR 0535 §4)
}
_, elapsed, results := st.levels.Level(t.pull.Repository, t.pull.Number, t.module, t.node, la)
st.facts = append(st.facts, rules.Step{Repository: t.pull.Repository, Number: t.pull.Number, Module: t.module, Node: t.node, Level: rules.LevelFact(la)})
_, elapsed, results := st.levels.Level(t.pull.Repository, t.pull.Number, t.module, t.language, t.node, la)
st.facts = append(st.facts, rules.Step{Repository: t.pull.Repository, Number: t.pull.Number, Module: t.module, Language: t.language, Node: t.node, Level: rules.LevelFact(la)})
var digests map[string]string
if t.module == goBuildModule && la.Digest != nil {
if t.language == languageGo && la.Digest != nil {
// The packages come as a list, never keyed by a package path (issue 1930); an answer stored before reads too.
digests = la.Digest.Map()
}
@@ -189,7 +189,7 @@ func (e errorString) Error() string { return string(e) }
func TestTheStatusTheForgeShowsNamesTheProgramThatJudged(t *testing.T) { // 1898/AC6, the forge's line
ctx := context.Background()
words := judgedByCandidate + "fedcba987654"
words := judgedByCandidate(goBuildModule) + "fedcba987654"
j := &judgeFunc{f: func(context.Context, *Case, int) (Outcome, error) {
return Outcome{Verdict: passing(), Judged: map[string]string{repoA + "#7": words}}, nil
}}
@@ -314,16 +314,17 @@ func TestACheckWhoseAskJudgedNothingIsAskedAgainOrRead(t *testing.T) {
ckey, _ := meshcatalogclient.CandidatesKey("feat/x")
_, _ = cands.Create(ctx, ckey, cand)
none := StateRead{Keys: func() ([]string, error) { return nil, nil }, Get: func(string) (json.RawMessage, error) { return nil, nil }}
steps := map[string]StateRead{goBuildModule: none}
rd := Reader{CandidateState: kvRead(cands), VerdictState: kvRead(r.verdicts), AskState: kvRead(asksKept),
StepStates: map[string]StateRead{goBuildModule: none}} // no node took its ask yet
StepStates: func() map[string]StateRead { return steps }} // no node took its ask yet
if _, err := rd.AskAgain(asVerdicts{r.verdicts}, "feat/x", "its watch went deaf"); err == nil || !strings.Contains(err.Error(), "waits in its build seat's stream") {
t.Errorf("asked again while its ask waits in the stream (issue 1791's kind): %v", err)
}
rd.StepStates[goBuildModule] = tr.Steps(true)
steps[goBuildModule] = tr.Steps(true)
if _, err := rd.AskAgain(asVerdicts{r.verdicts}, "feat/x", "its watch went deaf"); err == nil || !strings.Contains(err.Error(), "a step of it runs") {
t.Errorf("asked again while its step runs: %v", err)
}
rd.StepStates[goBuildModule] = tr.Steps(false)
steps[goBuildModule] = tr.Steps(false)
if st, _ := rd.Status(); !strings.Contains(st.Lost["feat/x"], AskAgainTool) {
t.Errorf("status lost = %v, want feat/x named with %s", st.Lost, AskAgainTool)
}
@@ -368,8 +369,8 @@ func TestACheckWhoseAskJudgedNothingIsAskedAgainOrRead(t *testing.T) {
id := fmt.Sprint("mvl0frv18d19", 80+i)
vals["ace."+id], _ = buildstep.EncodeStep(buildstep.Step{ID: id, Node: "ace", Tool: "check ask", State: buildstep.StepFailed, Passing: p, Ended: time.Now(), What: "check " + repoA + "@" + short12(head1)})
}
rd := Reader{CandidateState: kvRead(cands2(t, cand)), VerdictState: kvRead(failedKV), AskState: kvRead(newMemKV()), StepStates: map[string]StateRead{goBuildModule: {
Keys: func() ([]string, error) { return slices.Collect(maps.Keys(vals)), nil }, Get: func(k string) (json.RawMessage, error) { return vals[k], nil }}}}
rd := Reader{CandidateState: kvRead(cands2(t, cand)), VerdictState: kvRead(failedKV), AskState: kvRead(newMemKV()), StepStates: stepStatesOf(map[string]StateRead{goBuildModule: {
Keys: func() ([]string, error) { return slices.Collect(maps.Keys(vals)), nil }, Get: func(k string) (json.RawMessage, error) { return vals[k], nil }}})}
if got, err := rd.AskAgain(asVerdicts{failedKV}, "feat/x", "the network"); (err == nil) != c.ok || (c.ok && !strings.HasPrefix(got.Said, AskedAgainWords)) {
t.Errorf("%s: asked again = %+v, %v, want asked again %v", c.name, got, err, c.ok)
}
@@ -125,7 +125,7 @@ func (st *state) judgeManifests(ctx context.Context) {
at = "mesh-delivery's trunk " + commit
}
if commit == "" {
st.fail(goModule, "the change's manifests cannot be judged: the trunk head of "+DeliveryRepository+" is not known")
st.fail(st.goModule(), "the change's manifests cannot be judged: the trunk head of "+DeliveryRepository+" is not known")
return
}
// The list of predefined keys the manifests are judged against (ADR 0578 §2): the catalogue member's predefined.json
@@ -146,7 +146,7 @@ func (st *state) judgeManifests(ctx context.Context) {
if !st.indexerOf(ctx, req, touched, cv, at, &m) {
return
}
st.facts = append(st.facts, rules.Step{Module: goModule, Manifests: &m})
st.facts = append(st.facts, rules.Step{Module: st.goModule(), Manifests: &m})
}
// runJudge asks the Go build seat to build the judge at the commit and run it over the request, and reads its change and
@@ -154,7 +154,7 @@ func (st *state) judgeManifests(ctx context.Context) {
func (st *state) runJudge(ctx context.Context, req composer.Request, touched []string, commit, at string, versions buildstep.PackageVersions) (change, trunk composer.CaseVerdict, ok bool) {
raw, err := json.Marshal(req)
if err != nil {
st.fail(goModule, err.Error())
st.fail(st.goModule(), err.Error())
return change, trunk, false
}
job := buildstep.ComposeJob{Fetch: buildstep.Fetch{Repository: DeliveryRepository, Commit: commit, Packages: versions}, Dir: JudgeDir,
@@ -164,17 +164,17 @@ func (st *state) runJudge(ctx context.Context, req composer.Request, touched []s
st.c.Progress.Set(fmt.Sprintf("the judge ask %s of the manifests of %s", a.ID, strings.Join(touched, ", ")), "")
st.c.Asked(a.ID)
if err := st.j.Asks.Put(ctx, a); err != nil {
st.fail(goModule, "the judge of the change's manifests could not be asked: "+err.Error())
st.fail(st.goModule(), "the judge of the change's manifests could not be asked: "+err.Error())
return change, trunk, false
}
step, err := st.j.Asks.Wait(ctx, a)
if err != nil {
st.fail(goModule, "the judge of the change's manifests: "+err.Error())
st.fail(st.goModule(), "the judge of the change's manifests: "+err.Error())
return change, trunk, false
}
change, trunk, why := judgeAnswer(step)
if why != "" {
st.fail(goModule, fmt.Sprintf("the indexer's judge at %s did not judge the change's manifests: %s", at, why))
st.fail(st.goModule(), fmt.Sprintf("the indexer's judge at %s did not judge the change's manifests: %s", at, why))
return change, trunk, false
}
return change, trunk, true
@@ -195,7 +195,7 @@ func (st *state) indexerOf(ctx context.Context, req composer.Request, touched []
for _, module := range touched {
man, err := st.j.Read.Manifest(ctx, req.Cases[0].Manifests[module].Digest)
if err != nil {
st.fail(goModule, fmt.Sprintf("the manifest of %s cannot be read, so the indexer cannot be ruled out: %v", module, err))
st.fail(st.goModule(), fmt.Sprintf("the manifest of %s cannot be read, so the indexer cannot be ruled out: %v", module, err))
return false
}
for _, c := range man.Claims {
@@ -216,7 +216,7 @@ func (st *state) indexerOf(ctx context.Context, req composer.Request, touched []
trunk = st.j.Trunks.Heads()[DeliveryRepository]
}
if trunk == "" {
st.fail(goModule, "the new indexer cannot be judged by the trunk's manifest validator: the trunk head of "+DeliveryRepository+" is not known")
st.fail(st.goModule(), "the new indexer cannot be judged by the trunk's manifest validator: the trunk head of "+DeliveryRepository+" is not known")
return false
}
judgedAt = "mesh-delivery's trunk " + trunk
@@ -39,7 +39,7 @@ func TestAPackageFailuresCauseIsWordedNeverPass(t *testing.T) {
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
v := rules.LevelFailure("", "", "integration", Tested{Failed: []Failure{{Package: "github.com/novox/mesh-controller/cmd/mesh-controller", Output: c.output}}})
v := rules.LevelFailure("", "", "", "integration", Tested{Failed: []Failure{{Package: "github.com/novox/mesh-controller/cmd/mesh-controller", Output: c.output}}})
for _, l := range strings.Split(v.Failed.Words, "\n") {
if l = strings.TrimSpace(l); l == "PASS" || strings.HasPrefix(l, "ok ") || strings.HasPrefix(l, "ok\t") {
t.Errorf("1901/AC5: the words keep %q: %q", l, v.Failed.Words)
@@ -57,7 +57,7 @@ func TestAPackageFailuresCauseIsWordedNeverPass(t *testing.T) {
// A cause the build module told apart is cut to a line's bound, so the words keep their bound (the 1901 review's note).
{"a long cause is cut to a lines bound", func(t *testing.T) { // 1901/AC5
f := Failure{Package: "p", Test: "TestX", Cause: strings.Repeat("c", 5000), Output: "x_test.go:1: something"}
v := rules.LevelFailure("", "", "unit", Tested{Failed: []Failure{f}})
v := rules.LevelFailure("", "", "", "unit", Tested{Failed: []Failure{f}})
if len(v.Failed.Words) > rules.MaxWords+200 {
t.Errorf("1901/AC5: words are %d bytes, bound %d", len(v.Failed.Words), rules.MaxWords)
}
@@ -67,7 +67,7 @@ func TestAPackageFailuresCauseIsWordedNeverPass(t *testing.T) {
// capture of the same name (go-test-json-pkg-timeout, -exiting, -killed): the package's own output, its "FAIL" line.
{"a package stopped with only its fail line is still worded", func(t *testing.T) { // 1901/AC5
for _, out := range []string{"FAIL\texample.com/timeout\t2.017s", "FAIL\texample.com/exiting\t0.006s", "FAIL\texample.com/killed\t0.006s"} {
v := rules.LevelFailure("", "", "integration", Tested{Failed: []Failure{{Package: "example.com/x", Output: out}}})
v := rules.LevelFailure("", "", "", "integration", Tested{Failed: []Failure{{Package: "example.com/x", Output: out}}})
if !strings.HasPrefix(afterHid(v.Failed.Words), "the package failed with no test to name and no panic, signal or build error in its output") {
t.Errorf("1901/AC5: %q is worded %q", out, v.Failed.Words)
}
@@ -77,7 +77,7 @@ func TestAPackageFailuresCauseIsWordedNeverPass(t *testing.T) {
// either (review round 1): go prints its summary in exact forms and the runtime's lines at column 0.
{"only gos exact forms are summary or cause", func(t *testing.T) { // 1901/AC5
out := " x_test.go:3: ok so far\nok then: the seat is free\n panic: child retried and recovered\npanic: the real one\nFAIL\tp\t1.0s"
v := rules.LevelFailure("", "", "unit", Tested{Failed: []Failure{{Package: "p", Output: out}}})
v := rules.LevelFailure("", "", "", "unit", Tested{Failed: []Failure{{Package: "p", Output: out}}})
if !strings.HasPrefix(afterHid(v.Failed.Words), "panic: the real one") || !strings.Contains(v.Failed.Words, "ok then: the seat is free") {
t.Errorf("1901/AC5: words = %q", v.Failed.Words)
}
@@ -184,7 +184,7 @@ func (st *state) seatList(ctx context.Context) bool {
if st.j.SeatList != nil {
defs, err := st.j.SeatList.Definitions()
if err != nil {
st.fail(goModule, "the change's manifests cannot be judged: the seat list is not read ("+err.Error()+")")
st.fail(st.goModule(), "the change's manifests cannot be judged: the seat list is not read ("+err.Error()+")")
return false
}
base = defs
@@ -223,7 +223,7 @@ func (st *state) seatList(ctx context.Context) bool {
}
}
}
st.fail(goModule, "the heads' seat list could not be made by the checking program: "+err.Error())
st.fail(st.goModule(), "the heads' seat list could not be made by the checking program: "+err.Error())
return false
}
@@ -275,7 +275,7 @@ func (st *state) judged(ctx context.Context) (rules.Answer, *meshdeliverychecker
}
}
}
st.fail(goModule, "the check could not be judged by the checking program: "+err.Error())
st.fail(st.goModule(), "the check could not be judged by the checking program: "+err.Error())
return rules.Answer{V: rules.AnswerVersion, Verdict: rules.Judge(st.factsOf())}, by
}
@@ -44,11 +44,11 @@ type StateRead struct {
type Reader struct {
VerdictState, KeptState, CandidateState StateRead
PreviewState StateRead // the checker's merge previews
InputState StateRead // the checking program's inputs (InputsStateName)
StepStates map[string]StateRead // the build modules' steps states, by module
HaveState StateRead // the node-engine's have: whether a build module is on a node and running
StepBound time.Duration // how long the checker waits for one step; DefaultStepBound when 0
PreviewState StateRead // the checker's merge previews
InputState StateRead // the checking program's inputs (InputsStateName)
StepStates func() map[string]StateRead // the build modules' steps states, by module: the holders of the build seats (design 47 G A-82), found at each read
HaveState StateRead // the node-engine's have: whether a build module is on a node and running
StepBound time.Duration // how long the checker waits for one step; DefaultStepBound when 0
// CouldNotRunState is the checker's own state could-not-run (CouldNotRunState); without it none is named.
CouldNotRunState StateRead
// Trunks are the trunk heads of every repository (the gate's source); nil names no re-judge after a trunk move.
@@ -605,8 +605,12 @@ type moduleStep struct {
// readSteps reads the newest MaxReads keys of each build module's steps state.
func (r Reader) readSteps() stepsRead {
out := stepsRead{running: []RunningStep{}, left: []LeftStep{}}
mods := make([]string, 0, len(r.StepStates))
for m := range r.StepStates {
var states map[string]StateRead
if r.StepStates != nil {
states = r.StepStates()
}
mods := make([]string, 0, len(states))
for m := range states {
mods = append(mods, m)
}
sort.Strings(mods)
@@ -618,7 +622,7 @@ func (r Reader) readSteps() stepsRead {
bound = DefaultStepBound
}
for _, m := range mods {
sr := r.StepStates[m]
sr := states[m]
if sr.Keys == nil || sr.Get == nil {
continue
}
@@ -211,7 +211,7 @@ func TestTheStatusToolReadsRunningSteps(t *testing.T) {
builds[k], _ = json.Marshal(buildsrec.BuilderBuild{V: 1, State: state})
}
rd := Reader{
StepStates: map[string]StateRead{"mesh-build-go": fakeState{"shanks.abc1": running, "ace.abc2": passed}.read()},
StepStates: stepStatesOf(map[string]StateRead{"mesh-build-go": fakeState{"shanks.abc1": running, "ace.abc2": passed}.read()}),
CandidateState: fakeState{ckey: cand, ykey: candY}.read(),
VerdictState: fakeState{keyOf(t, "feat/y", candY): runningV}.read(),
BuildState: builds.read(),
@@ -269,14 +269,14 @@ func stepOf(t *testing.T, id, node string, started time.Time) []byte {
func TestTheStatusCountsOnlyStepsThatCanStillRunAndSaysWhyTheOthersAreLeft(t *testing.T) { // 1890/AC2, AC3
now := time.Date(2026, 10, 23, 7, 0, 0, 0, time.UTC)
rd := Reader{
StepStates: map[string]StateRead{
StepStates: stepStatesOf(map[string]StateRead{
"mesh-build-go": fakeState{
"g14.fresh1": stepOf(t, "fresh1", "g14", now.Add(-5*time.Minute)),
"g14.old1": stepOf(t, "old1", "g14", now.Add(-33*time.Hour)),
"ace.gone1": stepOf(t, "gone1", "ace", now.Add(-2*time.Minute)),
"shanks.down1": stepOf(t, "down1", "shanks", now.Add(-time.Minute)),
}.read(),
},
}),
HaveState: fakeState{
"g14.mesh-build-go": haveOf(t, nodeengineclient.PhaseRunning),
"shanks.mesh-build-go": haveOf(t, nodeengineclient.PhaseFailed),
@@ -427,7 +427,7 @@ func TestReadToolsReadOnlyWhatTheyNeedInParallelAndSayWhenCut(t *testing.T) { //
steps["g14."+id] = raw
}
sb := &slowBucket{vals: steps}
st, err := Reader{StepStates: map[string]StateRead{"mesh-build-go": sb.read()}, HaveState: fakeState{"g14.mesh-build-go": haveOf(t, nodeengineclient.PhaseRunning)}.read(),
st, err := Reader{StepStates: stepStatesOf(map[string]StateRead{"mesh-build-go": sb.read()}), HaveState: fakeState{"g14.mesh-build-go": haveOf(t, nodeengineclient.PhaseRunning)}.read(),
Now: func() time.Time { return now }}.Status()
if err != nil || sb.reads.Load() != MaxReads || len(st.Running) != 1 || sb.maxFlight.Load() < 2 {
t.Errorf("status: %d reads (want %d), running %+v, in flight %d, %v", sb.reads.Load(), MaxReads, st.Running, sb.maxFlight.Load(), err)
@@ -112,8 +112,9 @@ func TestManifestDeclarations(t *testing.T) { // 1678/AC1
if want := []state{{Name: "asks", History: 1}, {Name: "hearing", History: 1}, {Name: "inputs", TTL: 86400}, {Name: "compositions", TTL: 259200}, {Name: "kept", History: 1}, {Name: "owed", History: 5}, {Name: "previews", History: 5}, {Name: "tries", History: 1}, {Name: "verdicts", History: 5}, {Name: "first-views", History: 1}, {Name: "findings", History: 1}, {Name: "could-not-run", History: 1}}; !reflect.DeepEqual(m.State, want) {
t.Errorf("state = %+v, want %+v", m.State, want)
}
if want := []string{"mesh-build-go.steps", "mesh-build-go.toolchains", "mesh-build-node.steps", "mesh-build-node.toolchains",
"mesh-delivery-builder.builds", "mesh-delivery-watchman.presence", "node-engine.have"}; !reflect.DeepEqual(sorted(m.Reads), want) {
// The build modules are no names (design 47 G A-82): the steps and toolchains of the build seats are read as the seat's holder's states.
if want := []string{"mesh-delivery-builder.builds", "mesh-delivery-watchman.presence", "node-engine.have",
"seat:node-build-go.steps", "seat:node-build-go.toolchains", "seat:node-build-node.steps", "seat:node-build-node.toolchains"}; !reflect.DeepEqual(sorted(m.Reads), want) {
t.Errorf("reads = %v, want %v", m.Reads, want)
}
// DLV-M2 (ADR 0615 §1, ADR 0596): the indexer's candidates, modules, manifests, packages and predefined keys and the
@@ -130,16 +131,15 @@ func TestManifestDeclarations(t *testing.T) { // 1678/AC1
if want := []string{"mesh-delivery-assigner.assigner_read_assignments", "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:node-build-go.drop", "seat:node-build-node.drop", "seat:node-engine.compose-candidate"}; !reflect.DeepEqual(sorted(m.Invokes), want) {
"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"}; !reflect.DeepEqual(sorted(m.Invokes), want) {
t.Errorf("invokes = %v, want the seat git's file, the seats' drop and compose-candidate", m.Invokes)
}
if want := []string{"node-build-go", "node-build-node"}; !reflect.DeepEqual(sorted(m.Uses), want) {
t.Errorf("uses = %v, want the Go and Node build seats", m.Uses)
}
// 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.
if want := []string{"mesh-build-go.toolchain.reported", "mesh-build-node.toolchain.reported",
"mesh-catalog.catalog-revision", "mesh-catalog.seat-definition-changed", "mesh-catalog.seat-definition-removed", "mesh-conditions.findings-asked", "mesh-delivery-assigner.views.assignments", "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-engine.running"}; !reflect.DeepEqual(sorted(m.Consumes), want) {
// 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-assigner.views.assignments", "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"}; !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" {
+8 -7
View File
@@ -18,8 +18,6 @@
"mesh-catalog.catalog-revision",
"mesh-catalog.seat-definition-changed",
"mesh-catalog.seat-definition-removed",
"mesh-build-go.toolchain.reported",
"mesh-build-node.toolchain.reported",
"mesh-delivery-assigner.views.assignments",
"mesh-delivery-indexer.views.candidates",
"mesh-delivery-indexer.views.manifests",
@@ -28,6 +26,8 @@
"mesh-delivery-indexer.views.predefined",
"mesh-delivery-indexer.views.seat-definitions",
"mesh-conditions.findings-asked",
"node-build-go.toolchain-reported",
"node-build-node.toolchain-reported",
"node-engine.running"
],
"streams": [
@@ -50,13 +50,13 @@
{ "name": "could-not-run", "history": 1 }
],
"reads": [
"mesh-build-go.steps",
"mesh-build-go.toolchains",
"mesh-build-node.steps",
"mesh-build-node.toolchains",
"mesh-delivery-builder.builds",
"mesh-delivery-watchman.presence",
"node-engine.have"
"node-engine.have",
"seat:node-build-go.steps",
"seat:node-build-go.toolchains",
"seat:node-build-node.steps",
"seat:node-build-node.toolchains"
],
"uses": [
"node-build-go",
@@ -91,6 +91,7 @@
"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"
@@ -1,8 +0,0 @@
package rules
// The checker's names of the build modules, kept until its part finds them on the seat list as the builder does (design 47 G
// A-82 is the builder's change; the builder's package holds no name of them any more).
const (
goBuildModule = "mesh-build-go"
nodeBuildModule = "mesh-build-node"
)
+16 -14
View File
@@ -62,14 +62,16 @@ type Member struct {
// answers (Manifests), the module graph's ordering edges (Graph) or a failure the asking met (Failed: a step not asked, refused or not answered, a conflict, worded
// by the running checker). A step with none only says which node ran it.
type Step struct {
Repository string `json:"repository"`
Number int `json:"number"`
Module string `json:"module,omitempty"`
Node string `json:"node,omitempty"`
Level *buildstep.LevelAnswer `json:"level,omitempty"`
Failed *FailedFirst `json:"failed,omitempty"`
Programs *Programs `json:"programs,omitempty"`
Manifests *Manifests `json:"manifests,omitempty"`
Repository string `json:"repository"`
Number int `json:"number"`
Module string `json:"module,omitempty"`
// Language is the language of the toolchain the build module that ran the step builds with (a level's counts differ by it).
Language string `json:"language,omitempty"`
Node string `json:"node,omitempty"`
Level *buildstep.LevelAnswer `json:"level,omitempty"`
Failed *FailedFirst `json:"failed,omitempty"`
Programs *Programs `json:"programs,omitempty"`
Manifests *Manifests `json:"manifests,omitempty"`
// Graph is the module graph's ordering edges, as the change leaves them and as the trunk has them (JudgeGraph).
Graph *Graph `json:"graph,omitempty"`
// Renames is what a module brings and drops of `renamed-from` and how the seat mesh-config answered (JudgeRenames).
@@ -141,7 +143,7 @@ func NewTally() Tally {
// result is read and counted, also after one that did not pass, so the verdict names every failed test of the level
// (issue 1921). It answers the level's failure (nil when it passed), its running time (its tests' and setup's) and the
// results read, for the running checker to keep the passes from.
func (t Tally) Level(repository string, number int, module, node string, la buildstep.LevelAnswer) (*meshdeliverychecker.VerdictsByGroupHeadsValue, float64, []Tested) {
func (t Tally) Level(repository string, number int, module, language, node string, la buildstep.LevelAnswer) (*meshdeliverychecker.VerdictsByGroupHeadsValue, float64, []Tested) {
a := t[la.Level]
if a == nil {
return failAt(node, fmt.Sprintf("the check of %s#%d answered the level %q, which is no level", repository, number, la.Level)), 0, nil
@@ -158,7 +160,7 @@ func (t Tally) Level(repository string, number int, module, node string, la buil
}
results = append(results, res)
elapsed += res.Elapsed + res.Setup + res.Lint
if module == nodeBuildModule {
if language == LanguageTypeScript {
if !res.NoTests {
a.Run++ // a Node package is counted whole: its build module counts no tests (ADR 0484 clause 10)
}
@@ -176,7 +178,7 @@ func (t Tally) Level(repository string, number int, module, node string, la buil
}
}
if len(failed) > 0 {
return LevelFailure(repository, module, la.Level, failed...), elapsed, results
return LevelFailure(repository, module, language, la.Level, failed...), elapsed, results
}
return nil, elapsed, results
}
@@ -200,7 +202,7 @@ func Judged(f Facts) Answer {
case s.Failed != nil:
failures = append(failures, &meshdeliverychecker.VerdictsByGroupHeadsValue{State: meshdeliverycheckerclient.VerdictFail, Failed: s.Failed})
case s.Level != nil:
if v, _, _ := tally.Level(s.Repository, s.Number, s.Module, s.Node, *s.Level); v != nil {
if v, _, _ := tally.Level(s.Repository, s.Number, s.Module, s.Language, s.Node, *s.Level); v != nil {
failures = append(failures, v)
}
case s.Programs != nil:
@@ -208,7 +210,7 @@ func Judged(f Facts) Answer {
failures = append(failures, &meshdeliverychecker.VerdictsByGroupHeadsValue{State: meshdeliverycheckerclient.VerdictFail, Failed: ff})
}
case s.Graph != nil:
ff, said := JudgeGraph(*s.Graph)
ff, said := JudgeGraph(*s.Graph, s.Module)
notes = append(notes, said...)
if ff != nil {
failures = append(failures, &meshdeliverychecker.VerdictsByGroupHeadsValue{State: meshdeliverycheckerclient.VerdictFail, Failed: ff})
@@ -224,7 +226,7 @@ func Judged(f Facts) Answer {
failures = append(failures, &meshdeliverychecker.VerdictsByGroupHeadsValue{State: meshdeliverycheckerclient.VerdictFail, Failed: &ff})
}
case s.Manifests != nil:
ff, said := JudgeManifests(*s.Manifests)
ff, said := JudgeManifests(*s.Manifests, s.Module)
notes = append(notes, said...)
if ff != nil {
failures = append(failures, &meshdeliverychecker.VerdictsByGroupHeadsValue{State: meshdeliverycheckerclient.VerdictFail, Failed: ff})
+2 -2
View File
@@ -44,7 +44,7 @@ type Graph struct {
// JudgeGraph is the cycle rule over the graph's facts: the failure of a cycle the change makes, or the note of no cycle
// or of a cycle the trunk already has.
func JudgeGraph(g Graph) (*FailedFirst, []string) {
func JudgeGraph(g Graph, machine string) (*FailedFirst, []string) {
cycle := firstCycle(g.Edges, func(e Edge) bool { return e.Only != EdgeTrunk || !g.Trunk })
if cycle == nil {
return nil, []string{"the change's module graph has no cycle"}
@@ -58,7 +58,7 @@ func JudgeGraph(g Graph) (*FailedFirst, []string) {
return nil, []string{"the module graph has the cycle " + words + ", which the trunk already has among the same modules (ADR 0603 §2)"}
}
}
return &FailedFirst{Machine: goBuildModule, Words: fmt.Sprintf("the change's module graph has the cycle %s: these modules would "+
return &FailedFirst{Machine: machine, Words: fmt.Sprintf("the change's module graph has the cycle %s: these modules would "+
"each wait for the other for good, through their bases, the provisions and bus state they await, or the verbs they invoke (ADR 0603 §2, ADR 0614 §6)", words)}, nil
}
@@ -19,9 +19,6 @@ import (
// The facts the manifest validator, the fetch's programs and the composer answered, and the rules that judge them
// (milestone 47/B0, D1b): the running checker asks and records; these rules give the verdict.
// manifestsMachine is the machine a manifest's failure names: the build seat that ran the manifest validator.
const manifestsMachine = goBuildModule
// CatalogSeat is the seat the indexer holds: the module whose manifest claims it is the indexer, found by the claim and
// never by its name (novox/hq ADR 0633 clause 6, ADR 0601 §4).
const CatalogSeat = "mesh-catalog"
@@ -83,7 +80,7 @@ type Composed struct {
// validator refusing, holding or setting aside the new indexer's manifest fails first (ADR 0633 clause 6); else a new or
// changed manifest is judged in full, a held one is refused, and of one the change leaves as it is a problem the trunk
// already had is a warning while one the change causes fails the check (ADR 0579 §2).
func JudgeManifests(m Manifests) (*FailedFirst, []string) {
func JudgeManifests(m Manifests, machine string) (*FailedFirst, []string) {
var notes []string
if m.Indexer != "" {
var problems []string
@@ -103,7 +100,7 @@ func JudgeManifests(m Manifests) (*FailedFirst, []string) {
problems = append(problems, "the manifest validator does not know its fields "+iv.SetAside+", so it waits")
}
if len(problems) > 0 {
return &FailedFirst{Machine: manifestsMachine, Words: fmt.Sprintf("the indexer %s, the holder of the seat %s, would never run: the trunk's manifest validator at %s does not accept its manifest: %s",
return &FailedFirst{Machine: machine, Words: fmt.Sprintf("the indexer %s, the holder of the seat %s, would never run: the trunk's manifest validator at %s does not accept its manifest: %s",
m.Indexer, CatalogSeat, m.IndexerAt, strings.Join(problems, "; "))}, nil
}
notes = append(notes, fmt.Sprintf("the new indexer's manifest of %s is accepted by the trunk's manifest validator at %s", m.Indexer, m.IndexerAt))
@@ -147,7 +144,7 @@ func JudgeManifests(m Manifests) (*FailedFirst, []string) {
notes = append(notes, "warnings of the indexer's judge on manifests the change leaves as they are (ADR 0579 §2): "+strings.Join(warned, " | "))
}
if len(refused) > 0 {
return &FailedFirst{Machine: manifestsMachine, Words: strings.Join(refused, "\n")}, notes
return &FailedFirst{Machine: machine, Words: strings.Join(refused, "\n")}, notes
}
return nil, append(notes, fmt.Sprintf("the manifests of %s are accepted by the indexer's judge at %s", strings.Join(m.Touched, ", "), m.At))
}
@@ -0,0 +1,8 @@
package rules
// LanguageTypeScript is the language of the toolchain whose build module counts a package whole, not its tests (ADR 0484 clause
// 10): the `on` of a build seat's acting rule, never the name of a build module (design 47 G A-82).
const LanguageTypeScript = "typescript"
// LanguageGo is the language of the toolchain of the build module that judges the Go repositories and the manifests.
const LanguageGo = "go"
+2 -2
View File
@@ -168,7 +168,7 @@ const MaxFailureLine = 300
// LevelFailure turns a level that did not pass into a verdict: Failed names what failed first with its words whole,
// and Failures every failed test of the level's results with its first line, at most meshdeliverycheckerclient.MaxFailures and an entry
// saying how many more (issue 1921).
func LevelFailure(repository, module, level string, results ...Tested) *meshdeliverychecker.VerdictsByGroupHeadsValue {
func LevelFailure(repository, module, language, level string, results ...Tested) *meshdeliverychecker.VerdictsByGroupHeadsValue {
v := meshdeliverychecker.VerdictsByGroupHeadsValue{State: meshdeliverycheckerclient.VerdictFail}
var all []FailedFirst
for _, res := range results {
@@ -182,7 +182,7 @@ func LevelFailure(repository, module, level string, results ...Tested) *meshdeli
}
for _, f := range res.Failed {
name := FailedName(f)
if module == nodeBuildModule && f.Package == "" {
if language == LanguageTypeScript && f.Package == "" {
name = res.Module
}
all = append(all, FailedFirst{Test: level + ": " + name, Words: FailureWordsAfter(f, HidWords(module, res.By, f))})