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
This commit is contained in:
2026-11-04 22:47:19 +01:00
parent ae31b66a9e
commit 2cb3198049
2 changed files with 133 additions and 20 deletions
@@ -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)
@@ -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 {