From 2cb3198049e5e5e311cee35901e59d0156ae55e5 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 4 Nov 2026 22:47:19 +0100 Subject: [PATCH] Checker: judge the new indexer's manifest with the trunk's judge, and fail a held verdict (issue 1585, ADR 0633 clause 6) Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn --- .../internal/check/examiner_test.go | 31 +++++ .../internal/check/manifests_judge.go | 122 +++++++++++++++--- 2 files changed, 133 insertions(+), 20 deletions(-) diff --git a/modules/mesh-delivery-checker/internal/check/examiner_test.go b/modules/mesh-delivery-checker/internal/check/examiner_test.go index b237c86f..e9f490d5 100644 --- a/modules/mesh-delivery-checker/internal/check/examiner_test.go +++ b/modules/mesh-delivery-checker/internal/check/examiner_test.go @@ -1035,8 +1035,19 @@ func TestTheIndexersJudgeJudgesTheChangesManifests(t *testing.T) { // mail's tool while mail awaits web's database: both left out, so neither is refused (ADR 0614 §3, §6). cycle string trunkCycle bool + // indexer makes web the holder of the seat mesh-catalog, and the trunk's judge answers for it: "held" with no verdict, + // "aside" with a field it does not know; ownJudge has the group change the judge, so the manifest is also judged by the + // trunk's, in a second ask (ADR 0633 clause 6, 1585/T22, T23) + indexer string + ownJudge bool }{ {name: "accepted"}, + {name: "the indexer accepted by the trunk's judge", indexer: "ok"}, + {name: "the indexer held by the trunk's judge", indexer: "held", fails: "the indexer web, the holder of the seat mesh-catalog, would never run: the trunk's judge at mesh-delivery's trunk " + strings.Repeat("d", 40) + " does not accept its manifest: the judge holds it, with no verdict yet"}, + {name: "the indexer with a field the trunk's judge does not know", indexer: "aside", fails: "the judge does not know its fields new_field, so it waits"}, + {name: "the indexer refused by the trunk's judge", indexer: "refused", refused: "web has no id", fails: "does not accept its manifest: web has no id"}, + {name: "the indexer under a changed judge is judged by the trunk's as well", indexer: "aside", ownJudge: true, fails: "the judge does not know its fields new_field, so it waits"}, + {name: "a touched manifest the judge holds", indexer: "heldplain", fails: "holds the manifest of web: it gave no verdict"}, {name: "an untouched manifest's problem the trunk had", untouched: "mail says no credential form", trunkMail: "mail says no credential form"}, {name: "a problem the change causes in an untouched manifest", untouched: "mail reads a state web no longer keeps", fails: "refuses the manifest of mail: mail reads a state web no longer keeps"}, @@ -1077,6 +1088,10 @@ func TestTheIndexersJudgeJudgesTheChangesManifests(t *testing.T) { } b.ex.Read = fixedManifests{strings.Repeat("a", 64): mail, strings.Repeat("b", 64): trunkWeb, web: changed} } + if row.indexer != "" && row.indexer != "heldplain" { + b.ex.Read = fixedManifests{strings.Repeat("a", 64): {Module: "mail"}, strings.Repeat("b", 64): {Module: "web"}, + web: {Module: "web", Claims: []catalogue.Claim{{Name: CatalogSeat}}}} + } var judged []delivery.ComposeJob answer := b.asks.Answer b.asks.Answer = func(a delivery.StepAsk) delivery.Step { @@ -1085,6 +1100,12 @@ func TestTheIndexersJudgeJudgesTheChangesManifests(t *testing.T) { } judged = append(judged, *a.Compose) read := composer.ManifestRead{Accepted: row.refused == "", Refused: row.refused} + switch row.indexer { + case "held", "heldplain": + read = composer.ManifestRead{} + case "aside": + read.SetAside = "new_field" + } verdict, _ := json.Marshal(composer.Verdict{V: composer.Version, SDK: "exe:1", Cases: []composer.CaseVerdict{{Name: JudgeCase, Manifests: map[string]composer.ManifestRead{"web": read, "mail": {Accepted: row.untouched == "", Refused: row.untouched}}}, {Name: JudgeTrunk, Manifests: map[string]composer.ManifestRead{"web": {Accepted: true}, "mail": {Accepted: row.trunkMail == "", Refused: row.trunkMail}}}}}) @@ -1092,9 +1113,19 @@ func TestTheIndexersJudgeJudgesTheChangesManifests(t *testing.T) { return delivery.Step{ID: a.ID, State: delivery.StepPassed, Result: result} } p := pullOf(repoA, 7, head1) + if row.ownJudge { + p = pullOf(DeliveryRepository, 7, head1) + b.fetch.Changed = []string{JudgeDir + "/judge/judge.go"} + } p.Modules = []string{"web"} p.Fps = map[string]delivery.CandidateModule{"web": {FP2: "src2:" + strings.Repeat("f", 64), Manifest: web}} v := b.judge(t, caseOf(t, p)).Verdict + if row.ownJudge && (len(judged) != 2 || judged[0].Fetch["commit"] != head1 || judged[1].Fetch["commit"] != trunk) { + t.Fatalf("%s: judged %+v, want the head's judge and then the trunk's", row.name, judged) + } + if row.ownJudge { + judged = judged[1:] + } if len(judged) != 1 || judged[0].Fetch["repository"] != DeliveryRepository || judged[0].Fetch["commit"] != trunk || judged[0].Package != JudgePackage || judged[0].Dir != JudgeDir { t.Fatalf("%s: judged %+v (verdict %+v)", row.name, judged, v) diff --git a/modules/mesh-delivery-checker/internal/check/manifests_judge.go b/modules/mesh-delivery-checker/internal/check/manifests_judge.go index c3114fd3..c8320df3 100644 --- a/modules/mesh-delivery-checker/internal/check/manifests_judge.go +++ b/modules/mesh-delivery-checker/internal/check/manifests_judge.go @@ -120,26 +120,12 @@ func (st *state) judgeManifests(ctx context.Context) *delivery.Verdict { req.Predefined = list st.c.note("the manifests are judged against the predefined keys of " + from) } - raw, err := json.Marshal(req) - if err != nil { - return fail(goModule, err.Error()) + cv, trunkCase, v := st.runJudge(ctx, req, touched, commit, at) + if v != nil { + return v } - job := delivery.ComposeJob{Fetch: map[string]any{"repository": DeliveryRepository, "commit": commit}, Dir: JudgeDir, - Package: JudgePackage, Request: raw, Reader: "the indexer's judge at " + at} - a := delivery.StepAsk{ID: buildstep.NewID(), Kind: delivery.AskCheck, Language: delivery.AskGo, Asker: delivery.CheckerModule, - At: st.j.now().UTC(), Compose: &job} - 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 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 fail(goModule, "the judge of the change's manifests: "+err.Error()) - } - cv, trunkCase, why := judgeAnswer(step) - if why != "" { - return fail(goModule, fmt.Sprintf("the indexer's judge at %s did not judge the change's manifests: %s", at, why)) + if v := st.refuseUnmovableIndexer(ctx, req, touched, cv, at); v != nil { + return v } // A new or changed manifest is judged in full; of one the change leaves as it is, a problem the trunk already had is a // warning (it is not refused for a rule it already broke), and a problem the change causes in it, which the trunk case @@ -147,7 +133,13 @@ func (st *state) judgeManifests(ctx context.Context) *delivery.Verdict { var refused, warned []string for _, module := range slices.Sorted(maps.Keys(cv.Manifests)) { r := cv.Manifests[module] - if r.Accepted || r.Refused == "" { + if r.Accepted { + continue + } + if r.Refused == "" { // held: a verdict not given is not one given (ADR 0633 clause 6) + if slices.Contains(touched, module) { + refused = append(refused, fmt.Sprintf("the indexer's judge at %s holds the manifest of %s: it gave no verdict", at, module)) + } continue } had := map[string]bool{} @@ -183,6 +175,96 @@ func (st *state) judgeManifests(ctx context.Context) *delivery.Verdict { return nil } +// runJudge asks the Go build seat to build the judge at the commit and run it over the request, and reads its change and +// trunk cases; a failed verdict when it cannot be asked or does not answer. +func (st *state) runJudge(ctx context.Context, req composer.Request, touched []string, commit, at string) (change, trunk composer.CaseVerdict, v *delivery.Verdict) { + raw, err := json.Marshal(req) + if err != nil { + return change, trunk, fail(goModule, err.Error()) + } + job := delivery.ComposeJob{Fetch: map[string]any{"repository": DeliveryRepository, "commit": commit}, Dir: JudgeDir, + Package: JudgePackage, Request: raw, Reader: "the indexer's judge at " + at} + a := delivery.StepAsk{ID: buildstep.NewID(), Kind: delivery.AskCheck, Language: delivery.AskGo, Asker: delivery.CheckerModule, + At: st.j.now().UTC(), Compose: &job} + 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()) + } + 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()) + } + 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, nil +} + +// 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" + +// refuseUnmovableIndexer fails the check when the trunk's judge, the one the running parts hold, would refuse or hold the +// change's new indexer manifest: such an indexer would never run, and every revision behind it would wait for ever +// (ADR 0633 clause 6). The indexer is the touched module whose manifest claims the seat mesh-catalog. When the change +// judges with its own head's judge (it changes the judge), the manifest is judged by the trunk's as well. The verdicts +// read are the trunk's judge's: its change case, where refused, held (not accepted with no reason) or with a field set +// aside (a field the running judge does not know waits, ADR 0601 §3) each fail it, naming the module and the problems. +func (st *state) refuseUnmovableIndexer(ctx context.Context, req composer.Request, touched []string, cv composer.CaseVerdict, judgedAt string) *delivery.Verdict { + if st.j.Read == nil { + return nil + } + var indexer string + 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)) + } + for _, c := range m.Claims { + if c.Name == CatalogSeat { + indexer = module + } + } + } + if indexer == "" { + return nil + } + if st.judgeHead != "" { + trunk := "" + if st.j.Trunks != nil { + trunk = st.j.Trunks.Heads()[DeliveryRepository] + } + if trunk == "" { + return fail(goModule, "the new indexer cannot be judged by the trunk's judge: the trunk head of "+DeliveryRepository+" is not known") + } + var v *delivery.Verdict + judgedAt = "mesh-delivery's trunk " + trunk + if cv, _, v = st.runJudge(ctx, req, touched, trunk, judgedAt); v != nil { + return v + } + } + r, ok := cv.Manifests[indexer] + var problems []string + switch { + case !ok: + problems = append(problems, "the judge gave no verdict") + case r.Refused != "": + problems = append(problems, strings.Split(r.Refused, "\n")...) + case !r.Accepted: + problems = append(problems, "the judge holds it, with no verdict yet") + case r.SetAside != "": + problems = append(problems, "the judge 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 judge 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 judge at %s", indexer, judgedAt)) + return nil +} + // judgeAnswer reads the judge's change and trunk cases from its step, or why it cannot. func judgeAnswer(step delivery.Step) (change, trunk composer.CaseVerdict, why string) { if step.State != delivery.StepPassed {