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
293 lines
13 KiB
Go
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}
|
|
}
|