Every failure the asking meets is a fact of the check, and the verdict is the rules' of the facts (issue 1921)

The running checker records each failure where it is met and the node of each step it reads; the verdict is
rules.Judge of those facts, so the checking program, given the same facts, gives the same verdict. Its own
second outcome and its node set are gone.

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
This commit is contained in:
2026-11-06 22:20:42 +01:00
parent 61aa338887
commit cd201a79f1
6 changed files with 50 additions and 37 deletions
@@ -36,7 +36,7 @@ func (st *state) checkBinariesOf(t *target) verdictPtr {
found = append(found, p.Path+": a compiled program is build output: remove it and add it to .gitignore (issue 1577)")
}
if len(found) > 0 {
return failed(t.node, fmt.Sprintf("%s: %s", key, strings.Join(found, "; ")))
return st.fail(t.node, fmt.Sprintf("%s: %s", key, strings.Join(found, "; ")))
}
return nil
}
@@ -23,7 +23,7 @@ func failingResult(t *testing.T, tests int, failed ...Failure) json.RawMessage {
func judgeLevel(t *testing.T, tests ...json.RawMessage) (*delivery.Verdict, *state) {
t.Helper()
st := &state{j: &Examiner{}, c: &Case{}, levels: rules.NewTally(), nodes: map[string]bool{}}
st := &state{j: &Examiner{}, c: &Case{}, levels: rules.NewTally()}
v, _ := st.level(t.Context(), &target{pull: Pull{Repository: "novox/x"}}, delivery.LevelAnswer{Level: delivery.LevelIntegration, Tests: tests})
v = rules.Settled(rules.JoinFailures(v))
// 1921 (B0, D1a): the facts the running checker gathered, judged by the rules, give the same failures and counts.
@@ -550,6 +550,10 @@ func TestAHeadThatDoesNotMergeEndsFailedNamingTheFilesAndRunsNoTest(t *testing.T
if n := len(b.steps.tool("digest")) + len(b.steps.tool("test")); n != 0 {
t.Errorf("%d digest and test steps ran after a conflict", n)
}
// 1921/D1c: the node that ran the step is a fact, so the verdict judged from the facts names its machine.
if m := out.Verdict.Machines; len(m) != 1 || m[0] != "shanks" {
t.Errorf("machines = %v, want the node that ran the step", m)
}
}
func TestALevelWhoseDigestIsNotNamedRunsWholeAndKeepsNothing(t *testing.T) { // 1287/AC4
@@ -51,7 +51,7 @@ func (st *state) refuseCycle(ctx context.Context) *delivery.Verdict {
}
change, err := read(req.Cases[0].Manifests)
if err != nil {
return fail(goModule, "the change's module graph cannot be worked out, so no cycle can be ruled out: "+err.Error())
return st.fail(goModule, "the change's module graph cannot be worked out, so no cycle can be ruled out: "+err.Error())
}
cycle := graph.OfManifests(change).Cycle()
if cycle == nil {
@@ -71,6 +71,6 @@ func (st *state) refuseCycle(ctx context.Context) *delivery.Verdict {
return nil
}
}
return fail(goModule, fmt.Sprintf("the change's module graph has the cycle %s: these modules would each wait for the other for "+
return st.fail(goModule, fmt.Sprintf("the change's module graph has the cycle %s: these modules would each wait for the other for "+
"good, through their bases, the provisions and bus state they await, or the verbs they invoke (ADR 0603 §2, ADR 0614 §6)", words))
}
@@ -50,8 +50,6 @@ const (
// verdictPtr is a verdict of failure a step of the check ends with.
type verdictPtr = *delivery.Verdict
func failed(machine, words string) verdictPtr { return fail(machine, words) }
// Asks puts a check's ask on its build seat's stream and waits for its step (buildrun.Asker; ADR 0525 §1, §6): the
// checker chooses no node, starts no step and holds none; whichever build node pulls the ask runs the check.
type Asks interface {
@@ -236,7 +234,6 @@ type state struct {
levels rules.Tally
// facts are the facts of the check met so far, in order: what the checking program judges (milestone 47/B0).
facts []rules.Step
nodes map[string]bool
clone time.Duration
slow []string
// beside are the siblings read at their trunk's head, by repository (ADR 0548 §6).
@@ -298,16 +295,16 @@ func (st *state) takeTryCandidate(ctx context.Context) {
// Judge runs the case (it is the Judge of a Checker). A failure of the change, of a merge or of a step is a verdict; an error is only a case that cannot
// be judged at all.
func (j *Examiner) Judge(ctx context.Context, c *Case) (Outcome, error) {
st := &state{j: j, c: c, levels: rules.NewTally(), nodes: map[string]bool{},
st := &state{j: j, c: c, levels: rules.NewTally(),
beside: map[string]string{}}
// Every member and level runs to its end, and every manifest the change touches is judged by the indexer's judge (ADR
// 0578 §2), also after a failure: the verdict names every failure, not only the first (the operator, 2026-11-02).
tests := st.judgeAll(ctx)
// Each failure is a fact recorded where it is met; the verdict is the judging rules' of the facts.
st.judgeAll(ctx)
st.takeTryCandidate(ctx)
if v := rules.Settled(rules.JoinFailures(tests, st.judgeManifests(ctx), st.refuseCycle(ctx))); v != nil {
return st.finish(ctx, *v), nil
}
return st.finish(ctx, delivery.Verdict{State: delivery.VerdictPass}), nil
st.judgeManifests(ctx)
st.refuseCycle(ctx)
return st.finish(ctx, rules.Judge(st.factsOf())), nil
}
// finish is the outcome of the verdict with the state of the builds the candidate's modules carry and the assigner's
@@ -361,9 +358,8 @@ func (st *state) composed(ctx context.Context, v *delivery.Verdict) {
}
}
// outcome fills the verdict's levels, times, machines and composition from what ran.
// outcome is the verdict judged from the facts with the slowest tests, the siblings read and what judged a build module.
func (st *state) outcome(v delivery.Verdict) Outcome {
rules.Outcome(&v, st.levels, st.clone.Milliseconds(), st.nodes)
slow := rules.Slowest(st.levels)
out := Outcome{Verdict: v, Slowest: slow}
if len(st.beside) > 0 {
@@ -380,6 +376,19 @@ func (st *state) outcome(v delivery.Verdict) Outcome {
return out
}
// fail records a failure the asking met as a fact of the check, where it is met, and answers it as a verdict of failure
// at a machine: the verdict is the judging rules' of the facts (rules.Judge), so a failure only returned is no failure
// (milestone 47/B0).
func (st *state) fail(machine, words string) *delivery.Verdict {
st.facts = append(st.facts, rules.Step{Failed: &delivery.FailedFirst{Machine: machine, Words: words}})
return fail(machine, words)
}
// factsOf are the check's facts as met: what the checking program judges.
func (st *state) factsOf() rules.Facts {
return rules.Facts{V: rules.FactsVersion, Steps: st.facts, CloneMS: st.clone.Milliseconds(), Notes: st.c.Notes()}
}
// fail is a verdict of failure at a machine.
func fail(machine, words string) *delivery.Verdict {
v := failure(machine, words)
@@ -416,7 +425,7 @@ func (st *state) judgeAll(ctx context.Context) *delivery.Verdict {
for _, p := range pulls {
if ctx.Err() != nil {
if len(failures) == 0 { // an ask that ended with the context said it already
failures = append(failures, fail(delivery.CheckerModule, ctx.Err().Error()))
failures = append(failures, st.fail(delivery.CheckerModule, ctx.Err().Error()))
}
break
}
@@ -435,7 +444,7 @@ func (st *state) judgeAll(ctx context.Context) *delivery.Verdict {
if bm == buildrun.GoBuildModule {
beside, err := st.j.besideFor(ctx, st.c, p)
if err != nil {
failures = append(failures, fail(forgeMachine(err, bm), err.Error()+unheardWords(err)))
failures = append(failures, st.fail(forgeMachine(err, bm), err.Error()+unheardWords(err)))
continue
}
if len(beside) > 0 {
@@ -495,7 +504,7 @@ func (st *state) askNeeding(ctx context.Context, t *target, merged *[]Fetched, l
err = fmt.Errorf("no node record is on the bus")
}
if err != nil {
return fail(t.module, fmt.Sprintf("the check of %s#%d is not asked: the nodes' names the names check needs cannot be read: %v", p.Repository, p.Number, err))
return st.fail(t.module, fmt.Sprintf("the check of %s#%d is not asked: the nodes' names the names check needs cannot be read: %v", p.Repository, p.Number, err))
}
job.NodeNames = names
}
@@ -524,7 +533,7 @@ func (st *state) askNeeding(ctx context.Context, t *target, merged *[]Fetched, l
st.c.Progress.Set(what, "")
st.c.Asked(a.ID)
if err := st.j.Asks.Put(ctx, a); err != nil {
return fail(t.module, fmt.Sprintf("the check of %s#%d could not be asked: %v", p.Repository, p.Number, err))
return st.fail(t.module, fmt.Sprintf("the check of %s#%d could not be asked: %v", p.Repository, p.Number, err))
}
waiting := ctx
if watch := st.c.Watch; watch != nil {
@@ -536,9 +545,9 @@ func (st *state) askNeeding(ctx context.Context, t *target, merged *[]Fetched, l
step, err = st.j.Asks.Wait(waiting, a)
if err != nil {
if ctx.Err() != nil {
return fail(t.module, ctx.Err().Error())
return st.fail(t.module, ctx.Err().Error())
}
return fail(t.module, fmt.Sprintf("the check of %s#%d: %v", p.Repository, p.Number, err))
return st.fail(t.module, fmt.Sprintf("the check of %s#%d: %v", p.Repository, p.Number, err))
}
d, declined := delivery.DeclinedOf(step)
if !declined {
@@ -559,7 +568,7 @@ func (st *state) askNeeding(ctx context.Context, t *target, merged *[]Fetched, l
if lacks == "" {
lacks = strings.Join(d.Lacks, ", ") // a node off the agreed toolchain declined it (ADR 0584)
}
return fail(step.Node, fmt.Sprintf("the check of %s#%d needs a node with %s, and none took it in %s: every node that pulled it lacks it (last %s)",
return st.fail(step.Node, fmt.Sprintf("the check of %s#%d needs a node with %s, and none took it in %s: every node that pulled it lacks it (last %s)",
p.Repository, p.Number, lacks, bound, step.Node))
}
st.j.logf("%s#%d: the ask %s was declined on %s, which lacks %s; asked again", p.Repository, p.Number, a.ID, step.Node, strings.Join(d.Lacks, ", "))
@@ -572,23 +581,23 @@ func (st *state) askNeeding(ctx context.Context, t *target, merged *[]Fetched, l
took = step.Ended.Sub(step.Started)
}
t.node = step.Node
st.nodes[step.Node] = true
st.facts = append(st.facts, rules.Step{Repository: p.Repository, Number: p.Number, Module: t.module, Node: step.Node})
st.c.Progress.Set(fmt.Sprintf("judge the check of %s#%d", p.Repository, p.Number), step.Node)
if step.State != delivery.StepPassed {
return fail(step.Node, fmt.Sprintf("the check of %s#%d %s: %s", p.Repository, p.Number, step.State, step.Reason))
return st.fail(step.Node, fmt.Sprintf("the check of %s#%d %s: %s", p.Repository, p.Number, step.State, step.Reason))
}
var answer delivery.CheckAnswer
if err := json.Unmarshal(step.Result, &answer); err != nil {
return fail(step.Node, fmt.Sprintf("the check of %s#%d answered what is not a check: %s", p.Repository, p.Number, step.Result))
return st.fail(step.Node, fmt.Sprintf("the check of %s#%d answered what is not a check: %s", p.Repository, p.Number, step.Result))
}
if err := json.Unmarshal(answer.Fetch, &t.fetched); err != nil || t.fetched.Workspace == "" {
return fail(step.Node, fmt.Sprintf("the fetch of %s#%d answered no workspace: %s", p.Repository, p.Number, answer.Fetch))
return st.fail(step.Node, fmt.Sprintf("the fetch of %s#%d answered no workspace: %s", p.Repository, p.Number, answer.Fetch))
}
if len(t.fetched.Conflicts) > 0 {
if !st.narrowTry() {
st.j.writePreview(ctx, p, append(*merged, t.fetched))
}
return fail(step.Node, fmt.Sprintf("%s#%d %s%s", p.Repository, p.Number, ConflictWords, strings.Join(t.fetched.Conflicts, ", ")))
return st.fail(step.Node, fmt.Sprintf("%s#%d %s%s", p.Repository, p.Number, ConflictWords, strings.Join(t.fetched.Conflicts, ", ")))
}
t.changed = t.fetched.Changed
if !changesBuildGo(p.Repository, t.changed) {
@@ -605,7 +614,7 @@ func (st *state) askNeeding(ctx context.Context, t *target, merged *[]Fetched, l
if placed {
if len(la.Unmet) > 0 {
st.clone += max(0, took-time.Duration(ran*float64(time.Second)))
return fail(step.Node, fmt.Sprintf("the %s level of %s#%d left %s untested on %s, which was asked for a node with %s",
return st.fail(step.Node, fmt.Sprintf("the %s level of %s#%d left %s untested on %s, which was asked for a node with %s",
la.Level, p.Repository, p.Number, unmetNames(la.Unmet), step.Node, strings.Join(needs, ", ")))
}
la.Kept = nil // it names its packages and takes no other's kept verdict; this is only a guard against an older module
@@ -112,7 +112,7 @@ func (st *state) judgeManifests(ctx context.Context) *delivery.Verdict {
at = "mesh-delivery's trunk " + commit
}
if commit == "" {
return fail(goModule, "the change's manifests cannot be judged: the trunk head of "+DeliveryRepository+" is not known")
return st.fail(goModule, "the change's manifests cannot be judged: the trunk head of "+DeliveryRepository+" is not known")
}
// The list of predefined keys the manifests are judged against (ADR 0578 §2): the catalogue member's predefined.json
// when the change alters it, else the one the indexer keeps.
@@ -169,7 +169,7 @@ func (st *state) judgeManifests(ctx context.Context) *delivery.Verdict {
st.c.note("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 fail(goModule, strings.Join(refused, "\n"))
return st.fail(goModule, strings.Join(refused, "\n"))
}
st.c.note(fmt.Sprintf("the manifests of %s are accepted by the indexer's judge at %s", strings.Join(touched, ", "), at))
return nil
@@ -180,7 +180,7 @@ func (st *state) judgeManifests(ctx context.Context) *delivery.Verdict {
func (st *state) runJudge(ctx context.Context, req composer.Request, touched []string, commit, at string, versions delivery.PackageVersions) (change, trunk composer.CaseVerdict, v *delivery.Verdict) {
raw, err := json.Marshal(req)
if err != nil {
return change, trunk, fail(goModule, err.Error())
return change, trunk, st.fail(goModule, err.Error())
}
job := delivery.ComposeJob{Fetch: withPackages(map[string]any{"repository": DeliveryRepository, "commit": commit}, versions), Dir: JudgeDir,
Package: JudgePackage, Request: raw, Reader: "the indexer's judge at " + at}
@@ -189,15 +189,15 @@ func (st *state) runJudge(ctx context.Context, req composer.Request, touched []s
st.c.Progress.Set(fmt.Sprintf("the judge ask %s of the manifests of %s", a.ID, strings.Join(touched, ", ")), "")
st.c.Asked(a.ID)
if err := st.j.Asks.Put(ctx, a); err != nil {
return change, trunk, fail(goModule, "the judge of the change's manifests could not be asked: "+err.Error())
return change, trunk, st.fail(goModule, "the judge of the change's manifests could not be asked: "+err.Error())
}
step, err := st.j.Asks.Wait(ctx, a)
if err != nil {
return change, trunk, fail(goModule, "the judge of the change's manifests: "+err.Error())
return change, trunk, st.fail(goModule, "the judge of the change's manifests: "+err.Error())
}
change, trunk, why := judgeAnswer(step)
if why != "" {
return change, trunk, fail(goModule, fmt.Sprintf("the indexer's judge at %s did not judge the change's manifests: %s", at, why))
return change, trunk, st.fail(goModule, fmt.Sprintf("the indexer's judge at %s did not judge the change's manifests: %s", at, why))
}
return change, trunk, nil
}
@@ -221,7 +221,7 @@ func (st *state) refuseUnmovableIndexer(ctx context.Context, req composer.Reques
for _, module := range touched {
m, err := st.j.Read.Manifest(ctx, req.Cases[0].Manifests[module].Digest)
if err != nil {
return fail(goModule, fmt.Sprintf("the manifest of %s cannot be read, so the indexer cannot be ruled out: %v", module, err))
return st.fail(goModule, fmt.Sprintf("the manifest of %s cannot be read, so the indexer cannot be ruled out: %v", module, err))
}
for _, c := range m.Claims {
if c.Name == CatalogSeat {
@@ -241,7 +241,7 @@ func (st *state) refuseUnmovableIndexer(ctx context.Context, req composer.Reques
trunk = st.j.Trunks.Heads()[DeliveryRepository]
}
if trunk == "" {
return fail(goModule, "the new indexer cannot be judged by the trunk's manifest validator: the trunk head of "+DeliveryRepository+" is not known")
return st.fail(goModule, "the new indexer cannot be judged by the trunk's manifest validator: the trunk head of "+DeliveryRepository+" is not known")
}
var v *delivery.Verdict
judgedAt = "mesh-delivery's trunk " + trunk
@@ -262,7 +262,7 @@ func (st *state) refuseUnmovableIndexer(ctx context.Context, req composer.Reques
problems = append(problems, "the manifest validator does not know its fields "+r.SetAside+", so it waits")
}
if len(problems) > 0 {
return fail(goModule, 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",
return st.fail(goModule, 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",
indexer, CatalogSeat, judgedAt, strings.Join(problems, "; ")))
}
st.c.note(fmt.Sprintf("the new indexer's manifest of %s is accepted by the trunk's manifest validator at %s", indexer, judgedAt))