The check names a changed build module that invokes a verb no running build serves as one it cannot run (ADR 0614 §7; issue 774)

What the running builds serve is read when a changed build module is met: the node-engine's haves, the builder's
build of each, its manifest. The modules it builds are composed from their manifests, as for any changed build
module, and the composition's warnings name the verb.

Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
This commit is contained in:
2026-11-03 23:26:08 +01:00
parent c45bbfc391
commit 1d9f9cfa2c
4 changed files with 116 additions and 4 deletions
@@ -17,6 +17,8 @@ import (
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/process"
"git.novox.be/novox/mesh-delivery/sdk/graph"
"mesh-delivery-checker/internal/check"
)
@@ -95,6 +97,58 @@ func (n nodeNames) Names(ctx context.Context) ([]string, error) {
return out, nil
}
// runningServes says what the running builds serve (novox/hq ADR 0614 §7), read when a check asks it: every module's have
// in the node-engine's have that runs healthy, the builder's build of its fingerprint, and the verbs its manifest serves
// (graph.Served). A build whose manifest cannot be read serves nothing here, said in the log.
type runningServes struct {
have *nodeNames
builds check.BuildStates
read check.ManifestReader
logf func(format string, args ...any)
}
func (r runningServes) Served(ctx context.Context) (func(string) bool, error) {
if r.have == nil || r.have.kv == nil || r.builds == nil || r.read == nil {
return nil, fmt.Errorf("the node-engine's have, the builder's builds or the indexer's manifests cannot be read")
}
lister, err := r.have.kv.ListKeys(ctx)
if err != nil {
return nil, err
}
haves := map[string]map[string]delivery.Have{}
for key := range lister.Keys() {
node, module, isNode, err := delivery.ParseHaveKey(key)
if err != nil || isNode {
continue
}
e, err := r.have.kv.Get(ctx, key)
if err != nil {
continue
}
if h, err := delivery.DecodeHave(e.Value()); err == nil {
if haves[node] == nil {
haves[node] = map[string]delivery.Have{}
}
haves[node][module] = h
}
}
served, unread := graph.Served(haves, func(module, fp string) ([]string, bool) {
b, found, err := r.builds.Build(ctx, module, fp)
if err != nil || !found || b.Manifest == "" {
return nil, false
}
m, err := r.read.Manifest(ctx, b.Manifest)
if err != nil {
return nil, false
}
return graph.ServesOf(m), true
}, nil)
if len(unread) > 0 && r.logf != nil {
r.logf("the manifest of the running build of %v cannot be read, so it serves nothing the check counts (ADR 0614 §7)", unread)
}
return served, nil
}
func newJudge(d judgeDeps) check.Judge {
var nodes interface {
Names(ctx context.Context) ([]string, error)
@@ -102,8 +156,12 @@ func newJudge(d judgeDeps) check.Judge {
if d.nodes != nil {
nodes = d.nodes
}
return &check.Examiner{Nodes: nodes, Compositions: d.compositions, Asks: d.asks, Toolchains: d.tools,
ex := &check.Examiner{Nodes: nodes, Compositions: d.compositions, Asks: d.asks, Toolchains: d.tools,
Kept: check.Kept{Store: d.kept}, Previews: d.previews, Modules: d.repos, Packages: d.repos, Published: d.published, 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, Predefined: d.predefined, 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}
}
return ex
}
// manifestKV reads a manifest by its digest.
@@ -28,6 +28,9 @@ import (
// trunk's base; the base's own build is waited on.
//
// A module the trunk holds no record of yet is not built by the builder until it is merged, so it is not waited on either.
//
// A changed build module whose manifest invokes a seat verb or module tool no running build serves is one the check cannot
// run (ADR 0614 §7): the modules it builds are composed from their manifests, as above, and the words name the verb.
// buildsOfTheCheck are the builds the check waits on before it composes (module to fingerprint) and the words for the
// changed modules it composes without waiting on their build, one line each, sorted. A manifest that cannot be read leaves
@@ -88,6 +91,14 @@ func (st *state) buildsOfTheCheck(ctx context.Context) (map[string]string, []str
words = append(words, fmt.Sprintf("the change alters the build module %s: %s, which it builds, are built by the running %s, not the changed one, which the check cannot run, so the check does not wait on their builds; they are built again once the agreed toolchain moves (ADR 0603 §3, §10)",
bm, strings.Join(built, ", "), bm))
}
if unserved := st.unservedVerbs(ctx, changed[bm]); len(unserved) > 0 {
what := "nothing it builds is in the change"
if len(built) > 0 {
what = strings.Join(built, ", ") + ", which it builds, are composed from their manifests"
}
words = append(words, fmt.Sprintf("the changed build module %s invokes %s, which no running build serves, so the check cannot run it: %s (ADR 0614 §7)",
bm, strings.Join(unserved, ", "), what))
}
}
g := graph.OfManifests(changed)
for _, base := range slices.Sorted(maps.Keys(changed)) {
@@ -109,3 +120,23 @@ func buildsWith(m catalogue.Manifest, language string) bool {
return a.Kind == catalogue.ArtifactBundle && a.Language == language
})
}
// unservedVerbs are the seat verbs and module tools m invokes (graph.InvokesOf) that no running build serves, sorted; none
// when nothing reads what runs, or what runs cannot be read (said in the log: nothing is said that was not read).
func (st *state) unservedVerbs(ctx context.Context, m catalogue.Manifest) []string {
if st.j.Serving == nil {
return nil
}
served, err := st.j.Serving.Served(ctx)
if err != nil {
st.j.logf("%s: what the running builds serve cannot be read, so no build module is named for a verb it invokes: %v", st.c.Key, err)
return nil
}
var out []string
for _, v := range graph.InvokesOf(m) {
if !served(v) {
out = append(out, v)
}
}
return out
}
@@ -127,6 +127,13 @@ func (f *fakeEnds) Ended(_ context.Context, builds map[string]string) (map[strin
func (f *fakeEnds) Done(context.Context, map[string]string) bool { return false }
// fixedServing are the verbs the running builds serve.
type fixedServing []string
func (f fixedServing) Served(context.Context) (func(string) bool, error) {
return func(v string) bool { return slices.Contains(f, v) }, nil
}
// knownRuns are the modules the trunk holds a record of (the indexer's modules): any commit.
type knownRuns []string
@@ -154,10 +161,17 @@ func aChangeToABuildModuleOrBaseNamesTheModulesBuiltOnIt(t *testing.T) {
builds map[string]delivery.BuilderBuild // by module, at fp; nil leaves the check without the builds' ends
runs knownRuns // nil: the trunk holds every module
want []string
waited string // the modules waited on, sorted
failed string // the failure's test, when the check fails on a build
tests bool // the tests fail: the check fails before it would wait
waited string // the modules waited on, sorted
failed string // the failure's test, when the check fails on a build
tests bool // the tests fail: the check fails before it would wait
served []string // what the running builds serve (ADR 0614 §7); nil reads nothing of what runs
}{
{name: "the Go build module invokes a verb no running build serves",
manifest: map[string]catalogue.Manifest{"mesh-build-go": {Module: "mesh-build-go", Build: goBuild,
Invokes: []string{"seat:package-registry.publish", "seat:artifact-store.put"}}, "web": {Module: "web", Build: goBuild}},
builds: map[string]delivery.BuilderBuild{"mesh-build-go": done}, served: []string{"seat:artifact-store.put"},
want: []string{"the changed build module mesh-build-go invokes seat:package-registry.publish, which no running build serves, " +
"so the check cannot run it: web, which it builds, are composed from their manifests (ADR 0614 §7)"}, waited: "mesh-build-go"},
{name: "the Go build module changes",
manifest: map[string]catalogue.Manifest{"mesh-build-go": {Module: "mesh-build-go", Build: goBuild}, "web": {Module: "web", Build: goBuild},
"mail": {Module: "mail", Build: goBuild}, "docs": {Module: "docs"}},
@@ -211,6 +225,9 @@ func aChangeToABuildModuleOrBaseNamesTheModulesBuiltOnIt(t *testing.T) {
if row.runs != nil {
b.ex.Runs = row.runs
}
if row.served != nil {
b.ex.Serving = fixedServing(row.served)
}
read, fps := fixedManifests{}, map[string]delivery.CandidateModule{}
for module, m := range row.manifest {
d := dig(string(rune('a' + len(read))))
@@ -122,6 +122,12 @@ type Examiner struct {
// Read reads a kept manifest by its digest: the change's module graph is worked out from them (graph_cycle.go, novox/hq
// ADR 0603 §2); nil works out no graph.
Read ManifestReader
// Serving says, from what runs, whether a running healthy build serves a seat verb or module tool (novox/hq ADR 0614
// §7): the node-engines' haves, the builder's build of each, its manifest (graph.Served). Nil reads nothing, and no
// changed build module is named for a verb it invokes.
Serving interface {
Served(ctx context.Context) (func(invoke string) bool, error)
}
// Predefined reads the catalogue's predefined keys the indexer keeps (manifests_judge.go); nil reads none.
Predefined PredefinedList
// Compositions makes a check's composition (ADR 0593 §7): the engines' answers (EngineCompositions); nil leaves the