Files
mesh-delivery/modules/mesh-delivery-checker/rules/judged.go
T
jschoubben 2159375873 Merge the trunk: B0's facts and checking program carry the registry's steps (issue 1921, milestone 47/G D3-D11)
B0's shape wins: the running checker asks and records facts, the checking program
decides. The registry's types win: the rules and the program read the generated
checker contract and the domain SDK, and no mesh-sdk delivery import is left.
The registry's series, renames and unparsed seat definitions become facts with
their refusals as rules; the tagged versions ride on the case. The program reads
the heads' seat definitions through sdk/seatdefs and the running checker lays them
over its reader's list by seat, reading no file of the heads: the reader's list
is larger than a run input's bound.

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-07 02:57:21 +01:00

293 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/modules/mesh-delivery-builder/buildrun"
"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 = buildrun.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}
}