Files
mesh-delivery/modules/mesh-delivery-checker/internal/check/build_change.go
T
jschoubben 1d9f9cfa2c 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
2026-11-03 23:26:08 +01:00

143 lines
6.4 KiB
Go

package check
import (
"context"
"fmt"
"maps"
"slices"
"strings"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-delivery/sdk/graph"
)
// What the check builds of a candidate before it composes (novox/hq ADR 0603 §10). The builder builds every fingerprint the
// indexer's candidate carries, as a key of its own (builds.<module>.<fingerprint>), with the build modules that run; the
// check adds no build path of its own and waits on those keys (awaitBuilds), so the node-engines compose the candidate's
// modules with their builds. It does not wait on two kinds of changed module, and names them in its composition's warnings:
//
// - one built by a build module the change alters (the recipe of mesh-build-go or mesh-build-node enters the fingerprint
// of every module it builds, ADR 0530 §1): its candidate build is the running build module's, and the check cannot run
// the candidate's build module to build it (it runs it only for its own digest and tests, ADR 0535 §1), so it is not
// composed with a build of the changed build module; it is built again once the agreed toolchain moves (ADR 0603 §3).
// The build module's own build is waited on, as any other: the running build module builds its new source.
// - one that stands on a module the change alters (build.on): its candidate fingerprint names the base's trunk
// fingerprint, and no build module builds on another module's artifact (ADR 0487 §1), so its build stands on the
// 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
// every build waited on and nothing said of build modules or bases (nothing is said that was not read).
func (st *state) buildsOfTheCheck(ctx context.Context) (map[string]string, []string) {
wait := map[string]string{}
changed := map[string]catalogue.Manifest{}
readable := true
for _, p := range st.c.Pulls {
for module, cm := range p.Fps {
if cm.FP2 == "" || cm.Manifest == "" {
continue // the builder builds no fingerprint of it (candidates.go: none of the new form, or no manifest)
}
wait[module] = cm.FP2
if !readable || st.j.Read == nil {
readable = false
continue
}
m, err := st.j.Read.Manifest(ctx, cm.Manifest)
if err != nil {
st.j.logf("%s: the manifest of %s cannot be read, so the modules built with a changed build module or base are not named and every build is waited on: %v", st.c.Key, module, err)
readable = false
continue
}
changed[module] = m
}
}
var words []string
if st.j.Runs != nil {
var unheld []string
for _, module := range slices.Sorted(maps.Keys(wait)) {
if st.j.Runs.CommitOf(module) == "" {
unheld = append(unheld, module)
delete(wait, module)
}
}
if len(unheld) > 0 {
words = append(words, fmt.Sprintf("%s: the trunk holds no record of it yet, so the builder builds it once it is merged and the check composes it without a build (ADR 0603 §10)",
strings.Join(unheld, ", ")))
}
}
if !readable {
return wait, words
}
for _, bm := range slices.Sorted(maps.Keys(buildrun.LanguageOf)) {
if _, alters := changed[bm]; !alters {
continue
}
var built []string
for module, m := range changed {
if _, isBuildModule := buildrun.LanguageOf[module]; !isBuildModule && buildsWith(m, buildrun.LanguageOf[bm]) {
built = append(built, module)
delete(wait, module)
}
}
slices.Sort(built)
if len(built) > 0 {
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)) {
if on := g.Dependents(base); len(on) > 0 {
for _, module := range on {
delete(wait, module)
}
words = append(words, fmt.Sprintf("the change alters %s, a base of %s: their builds stand on the trunk's %s, since no build module builds on a candidate's base, so the check waits on the build of %s alone (ADR 0603 §10)",
base, strings.Join(on, ", "), base, base))
}
}
return wait, words
}
// buildsWith says whether a manifest has a bundle in the language: the artifacts whose fingerprint names that language's
// toolchain and its build module's recipe (an archive is packed with no toolchain, ADR 0478 clause 1).
func buildsWith(m catalogue.Manifest, language string) bool {
return m.Build != nil && slices.ContainsFunc(m.Build.Artifacts, func(a catalogue.Artifact) 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
}