Files
mesh-delivery/modules/mesh-delivery-checker/rules/judged.go
T
jschoubben 84bcdb687a BL18: build modules found through their build seats' holders on the seat list
The builder finds a build module as the indexer's seed does: the build seat whose acting rule
`build` has an `on` matching the artifact's language or kind:image, and that seat's holder, read
from the seat list's definitions and the claims of the catalog half's manifests. Task.Module, every
lane key, the unpublished modules, the toolchain sources, the ends watched, the sayer and
builder_stuck (through `<seat>.steps`) derive from it; the floor's sources are the holders of the
seats git and mesh-catalog. buildrun.GoBuildModule, NodeBuildModule, LanguageOf,
buildstep.BuildModuleOf use, build.ImageBuildModule and StuckBuildModules are gone from the builder
(the checker keeps its own names in two small files until its part reads them from the list).

Builds: design 47 G §7, §428, §704, §204, A-82
Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
2026-11-12 00:04:34 +01:00

292 lines
13 KiB
Go

package rules
import (
"fmt"
nec "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
ne "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
"maps"
"slices"
"sort"
"strings"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshwants"
"git.novox.be/novox/mesh-delivery/sdk/buildstep"
"git.novox.be/novox/mesh-delivery/sdk/composer"
)
// 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"
// BetweenManifests is the manifest validator's name for what it judges between manifests (the catalogue as a whole).
const BetweenManifests = "(between manifests)"
// Manifests are the manifest validator's answers on the manifests a check touches (ADR 0578 §2, ADR 0579 §2, ADR 0633
// clause 6): its change and trunk cases where it judged (At), and, when a touched module claims the seat mesh-catalog,
// that indexer and the trunk validator's change case on it (IndexerAt names where it judged).
type Manifests struct {
Touched []string `json:"touched"`
At string `json:"at"`
Change composer.CaseVerdict `json:"change"`
Trunk composer.CaseVerdict `json:"trunk"`
Indexer string `json:"indexer,omitempty"`
IndexerAt string `json:"indexer-at,omitempty"`
OfIndexer *composer.CaseVerdict `json:"of-indexer,omitempty"`
}
// Programs are what one member's fetch read of the change's compiled programs (issue 1577): Files nil when the build
// module reported none (an older module: not refused, the running checker warns), else each changed file that is one.
type Programs struct {
Key string `json:"key"`
Files *[]ProgramFile `json:"files"`
}
// ProgramFile is a changed file that is a compiled program, its format (ELF, Mach-O or PE) and, when it was added in an
// earlier commit of the branch and removed later, that commit: the build step's own (buildstep.Program).
type ProgramFile = buildstep.Program
// Composition is how the check's composition went (ADR 0593 §7, §9; ADR 0603 §10; ADR 0548 §5): kept from the check it
// carries, not run (NotRun says why), or composed by the engines; BuildsFailed are the candidate's builds that failed
// before it composed, worded by the running checker; Waits are what it composed without waiting on, as warnings.
type Composition struct {
Kept *meshdeliverychecker.VerdictsByGroupHeadsValueComposition `json:"kept,omitempty"`
NotRun string `json:"not-run,omitempty"`
BuildsFailed []FailedFirst `json:"builds-failed,omitempty"`
Waits []string `json:"waits,omitempty"`
Composed *Composed `json:"composed,omitempty"`
FPSet string `json:"fp-set,omitempty"`
}
// Composed are the engines' answers to one composition: the nodes placed, each answering node's answer, each silent
// node with why (for the journal only), what composed (the node-engine's commit), the composer's own warnings and how
// long it took in milliseconds.
type Composed struct {
Group string `json:"group"`
Heads string `json:"heads"`
Composer string `json:"composer"`
Nodes []string `json:"nodes"`
Answers map[string]ne.ComposeCandidateAnswer `json:"answers,omitempty"`
Silent map[string]string `json:"silent,omitempty"`
Warnings []string `json:"warnings,omitempty"`
Took int64 `json:"took,omitempty"`
}
// JudgeManifests is the failure of the manifests' facts (nil when they pass) and the notes a reader gets: the trunk
// 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) {
var notes []string
if m.Indexer != "" {
var problems []string
ok := false
var iv composer.ManifestRead
if m.OfIndexer != nil {
iv, ok = m.OfIndexer.Manifests[m.Indexer]
}
switch {
case !ok:
problems = append(problems, "the manifest validator gave no verdict")
case iv.Refused != "":
problems = append(problems, strings.Split(iv.Refused, "\n")...)
case !iv.Accepted:
problems = append(problems, "the manifest validator holds it, with no verdict yet")
case iv.SetAside != "":
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",
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))
}
var refused, warned []string
for _, module := range slices.Sorted(maps.Keys(m.Change.Manifests)) {
r := m.Change.Manifests[module]
if r.Accepted {
continue
}
if r.Refused == "" { // held: a verdict not given is not one given (ADR 0633 clause 6)
if slices.Contains(m.Touched, module) {
refused = append(refused, fmt.Sprintf("the indexer's judge at %s holds the manifest of %s: it gave no verdict", m.At, module))
}
continue
}
had := map[string]bool{}
for _, p := range strings.Split(m.Trunk.Manifests[module].Refused, "\n") {
had[p] = true
}
var caused, old []string
for _, p := range strings.Split(r.Refused, "\n") {
if slices.Contains(m.Touched, module) || !had[p] {
caused = append(caused, p)
} else {
old = append(old, p)
}
}
if len(caused) > 0 {
what := "the manifest of " + module
if module == BetweenManifests {
what = "the catalogue"
}
refused = append(refused, fmt.Sprintf("the indexer's judge at %s refuses %s: %s", m.At, what, strings.Join(caused, "; ")))
}
if len(old) > 0 {
warned = append(warned, module+": "+strings.Join(old, "; "))
}
}
if len(warned) > 0 {
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 nil, append(notes, fmt.Sprintf("the manifests of %s are accepted by the indexer's judge at %s", strings.Join(m.Touched, ", "), m.At))
}
// JudgePrograms fails a member whose change adds or modifies a compiled program, naming each file (issue 1577); a fetch
// that reported none, or found none, passes.
func JudgePrograms(machine string, p Programs) *FailedFirst {
if p.Files == nil {
return nil
}
var found []string
for _, f := range *p.Files {
if f.Commit != "" {
found = append(found, fmt.Sprintf("%s: a compiled program added in %s and removed later: it stays in history; squash the branch (issue 1577)", f.Path, short12(f.Commit)))
continue
}
found = append(found, f.Path+": a compiled program is build output: remove it and add it to .gitignore (issue 1577)")
}
if len(found) == 0 {
return nil
}
return &FailedFirst{Machine: machine, Words: fmt.Sprintf("%s: %s", p.Key, strings.Join(found, "; "))}
}
func short12(s string) string {
if len(s) > 12 {
return s[:12]
}
return s
}
// JudgeEngines is the check's composition verdict from the engines' answers (ADR 0593 §7, §9): failed when any node's
// answer is refused, naming each node and each module refused with its words (the first refusing node is the machine);
// passed otherwise. Every placed node without an answer is named with nec.NoAnswer as a warning.
func JudgeEngines(c Composed) meshwants.CompositionsByGroupHeadsValue {
v := meshwants.CompositionsByGroupHeadsValue{Group: c.Group, Heads: c.Heads, State: meshdeliverycheckerclient.CompositionPass, Sdk: c.Composer, Took: c.Took}
sorted := append([]string(nil), c.Nodes...)
sort.Strings(sorted)
var fails []string
for _, node := range sorted {
a, answered := c.Answers[node]
if !answered {
v.Warnings = append(v.Warnings, node+": "+nec.NoAnswer)
continue
}
if a.Verdict == nec.NotJudged {
// Composed in the sandbox and not judged (ADR 0593 §9): named as no answer, with the composer's words; decided
// by the verdict, never by the words (ADR 0601 §4's kind).
v.Warnings = append(v.Warnings, node+": "+nec.NoAnswer+" ("+a.Refused+")")
continue
}
v.Machines = append(v.Machines, node)
if a.Verdict != nec.Refused {
continue
}
if v.Machine == "" {
v.Machine = node
}
var words []string
if a.Refused != "" {
words = append(words, a.Refused)
}
for _, m := range append(append([]ne.Module(nil), a.Modules...), a.Own) {
if m.State == nec.ModuleRefused {
words = append(words, m.Module+": "+m.Words)
}
}
if len(words) == 0 {
words = append(words, "refused, naming no module")
}
fails = append(fails, "not composed on "+node+": "+strings.Join(words, "; "))
}
if len(fails) > 0 {
v.State, v.Words = meshdeliverycheckerclient.CompositionFail, strings.Join(fails, "; ")
}
if len(sorted) == 0 {
v.Warnings = append(v.Warnings, "no node is placed, so the change was composed on none")
}
v.Warnings = append(v.Warnings, c.Warnings...)
return v
}
// JudgeComposition takes the composition's facts into the verdict: a failed build of the candidate fails it, named after
// the tests' failures; a failed composition fails it, naming its machine; a passed one with warnings says them.
func JudgeComposition(v *meshdeliverychecker.VerdictsByGroupHeadsValue, c Composition) {
if c.Kept != nil {
kept := *c.Kept
v.Composition = &kept
return
}
for _, f := range c.BuildsFailed {
if v.State != meshdeliverycheckerclient.VerdictFail {
v.State, v.Failed = meshdeliverycheckerclient.VerdictFail, &f
continue
}
AddFailure(v, f)
}
if c.NotRun != "" {
v.Composition = &meshdeliverychecker.VerdictsByGroupHeadsValueComposition{State: meshdeliverycheckerclient.CompositionNotRun, Words: c.NotRun}
return
}
if c.Composed == nil {
return
}
comp := JudgeEngines(*c.Composed)
comp.Warnings = append(comp.Warnings, c.Waits...) // ADR 0603 §10
v.Composition = compositionOf(comp)
if v.Took == nil {
v.Took = &meshdeliverychecker.VerdictsByGroupHeadsValueTook{}
}
v.Took.Composition = comp.Took
if comp.State == meshdeliverycheckerclient.CompositionPass && len(comp.Warnings) > 0 && v.Composition != nil {
v.Composition.Words = strings.Join(comp.Warnings, "; ") // passed with a warning (ADR 0593 §9)
}
if comp.State == meshdeliverycheckerclient.CompositionFail {
machine := comp.Machine
if machine == "" {
machine = "the composition"
}
f := FailedFirst{Machine: machine, Words: comp.Words}
if v.State == meshdeliverycheckerclient.VerdictFail {
AddFailure(v, f) // the tests failed too: the composition is named after them (the operator, 2026-11-02)
} else {
v.State, v.Failed = meshdeliverycheckerclient.VerdictFail, &f
}
}
if v.Composition != nil && v.Composition.State != meshdeliverycheckerclient.CompositionNotRun {
v.Composition.Fpset = c.FPSet // a carry keeps it while the fingerprints stay the same (ADR 0548 §5)
}
}
// compositionWaits is the state of the assigner's composition answer that waits for what it composes from.
const compositionWaits = "waits"
// compositionOf is what the verdict carries of a composition answer c: nil while c waits (the check is still running).
func compositionOf(c meshwants.CompositionsByGroupHeadsValue) *meshdeliverychecker.VerdictsByGroupHeadsValueComposition {
if c.State == compositionWaits {
return nil
}
return &meshdeliverychecker.VerdictsByGroupHeadsValueComposition{State: c.State, Words: c.Words, Sdk: c.Sdk, Machines: c.Machines}
}