From df56af07657b39612c0bebc30f139ac779d2e5a5 Mon Sep 17 00:00:00 2001 From: jochen Date: Sat, 7 Nov 2026 03:00:35 +0100 Subject: [PATCH] Every check composes with the composer built at the node-engine member's head, else at its trunk, and a change's manifests carry the check's own judge's verdicts (issue 1921, milestone 47/G D12, D13) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The parked composition-at-running path is folded as the lead's ruling Q3: one path, so no check composes with the running engines' own code (ADR 0656 §5). The sandbox request no longer drops a change's manifest verdict: it carries the check's judge's, or none, which the composer reads as accepted (ADR 0578 §3). Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i --- .../cmd/mesh-delivery-checker/main.go | 2 + .../internal/check/beside.go | 5 +- .../internal/check/case.go | 3 + .../internal/check/engines.go | 35 ++++++++--- .../internal/check/engines_test.go | 61 ++++++++++++++++++- .../internal/check/manifests_judge.go | 28 +++++++++ 6 files changed, 125 insertions(+), 9 deletions(-) diff --git a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/main.go b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/main.go index 93353127..7a149d19 100644 --- a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/main.go +++ b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/main.go @@ -260,6 +260,8 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro Bound: answerBound, Sandbox: seatSandbox{asker: buildrun.Asker{Publisher: asks, Waiter: waiter, Bound: stepBound}}, Verdicts: verdicts.all, + // The node-engine's trunk head: a group with no node-engine member composes with the composer built there (ADR 0656 §5). + Composer: func() string { return trunks.Heads()[check.NodeEngineRepository] }, } // The tries (ADR 0501 §5) are judged by the same judge, held by the same deliveries. tryer := &check.Tryer{Tries: kvStore{buckets[check.TriesBucket]}, Bound: checkBound, NotTaken: stepBound, Logf: logger.Printf, diff --git a/modules/mesh-delivery-checker/internal/check/beside.go b/modules/mesh-delivery-checker/internal/check/beside.go index 6b80fc73..215711b4 100644 --- a/modules/mesh-delivery-checker/internal/check/beside.go +++ b/modules/mesh-delivery-checker/internal/check/beside.go @@ -31,7 +31,7 @@ const NodeEngineModule = nodeEngineModule const ( catalogueRepository = "novox/mesh-catalog" - nodeEngineRepo = "novox/mesh-host" + nodeEngineRepo = NodeEngineRepository nodeEngineModule = "mesh-host" // networkingRepository holds route-proxy, whose manifest the node-engine's proofs of a composition read. networkingRepository = "novox/mesh-networking" @@ -213,3 +213,6 @@ func besideAtTrunk(c *Case, refs []BesideRef, into map[string]string) { } } } + +// NodeEngineRepository is the node-engine's repository, whose trunk head a check without its member composes at. +const NodeEngineRepository = "novox/mesh-host" diff --git a/modules/mesh-delivery-checker/internal/check/case.go b/modules/mesh-delivery-checker/internal/check/case.go index ca2e52bd..6557b8f6 100644 --- a/modules/mesh-delivery-checker/internal/check/case.go +++ b/modules/mesh-delivery-checker/internal/check/case.go @@ -50,6 +50,9 @@ type Case struct { Seats []json.RawMessage // Try narrows a try's check (its level, its tests); nil for a pull request's. Try *TryOptions + // Judged are the check's own judge's verdicts on the manifests the change brings, by module, as the seat mesh-catalog's + // verdict value (judgeManifests): the composer's request carries them in place of the running indexer's (ADR 0578 §2). + Judged map[string]json.RawMessage // Tagged are the group's tagged versions of its Go packages the check heard (design 51 *Tagged versions*), nil for none: // a composer built at the node-engine member's head is built against them, as the check's other Go builds are. Tagged buildstep.PackageVersions diff --git a/modules/mesh-delivery-checker/internal/check/engines.go b/modules/mesh-delivery-checker/internal/check/engines.go index ec361c44..f4fe5ea9 100644 --- a/modules/mesh-delivery-checker/internal/check/engines.go +++ b/modules/mesh-delivery-checker/internal/check/engines.go @@ -50,7 +50,10 @@ type EngineCompositions struct { Placed func() ([]string, error) // Bound is the answer bound (the setting compose-answer-bound); zero is nec.AnswerBound. Bound time.Duration - // Composer is the commit of the node-engine the mesh runs, named as what composed (ADR 0548 §6's running commit). + // Composer is the node-engine's trunk head: a check whose group has no node-engine member is composed by the composer + // built there, so every check composes with an acting part built from the change's heads or their trunk, never with + // the running engines' own code (ADR 0656 §5; the lead's ruling Q3 on milestone 47/G). Nil, or an empty head, leaves + // such a check composed by the running engines, said. Composer func() string // Sandbox runs the composer of a node-engine member's head; nil leaves such a group judged by the running engines, // said in a warning. @@ -67,6 +70,14 @@ type EngineCompositions struct { } // composerHead is the node-engine member's head of a case, or empty. +// atWord names where a composer was built: the member's head or the node-engine's trunk. +func atWord(member bool) string { + if member { + return "head" + } + return "trunk" +} + func composerHead(c *Case) string { if m := c.member(nodeEngineRepo); m != nil { return m.Head @@ -92,7 +103,11 @@ func (e EngineCompositions) Compose(ctx context.Context, c *Case) (rules.Compose if bound <= 0 { bound = nec.AnswerBound } - head := composerHead(c) + // One composition path (ADR 0656 §5): the composer built at the node-engine member's head, else at its trunk. + head, member := composerHead(c), true + if head == "" && e.Composer != nil { + head, member = e.Composer(), false + } sandboxed := head != "" && e.Sandbox != nil answers := map[string]ne.ComposeCandidateAnswer{} silent := map[string]string{} @@ -111,7 +126,7 @@ func (e EngineCompositions) Compose(ctx context.Context, c *Case) (rules.Compose // the head. if composed, serr := e.inSandbox(ctx, c, head, node, a); serr != nil { a = ne.ComposeCandidateAnswer{V: a.V, Node: node, Candidate: a.Candidate, Verdict: nec.Refused, - Refused: "the composer at the node-engine's head " + head + " could not compose it: " + serr.Error()} + Refused: "the composer at the node-engine's " + atWord(member) + " " + head + " could not compose it: " + serr.Error()} } else { a = composed } @@ -133,12 +148,13 @@ func (e EngineCompositions) Compose(ctx context.Context, c *Case) (rules.Compose composerName := "node-engine" if sandboxed { composerName = head - } else if e.Composer != nil && e.Composer() != "" { - composerName = e.Composer() } v := rules.Composed{Group: c.Group, Heads: c.HeadsHash, Composer: composerName, Nodes: nodes, Answers: answers, Silent: silent} - if head != "" && e.Sandbox == nil { + switch { + case member && head != "" && e.Sandbox == nil: v.Warnings = append(v.Warnings, "the group changes the node-engine's composition code and was composed by the running engines") + case !sandboxed && e.Sandbox != nil && len(nodes) > 0: + v.Warnings = append(v.Warnings, "composed by the running engines: no composer at the node-engine's trunk is known to build") } v.Took = time.Since(began).Milliseconds() return v, nil @@ -174,9 +190,14 @@ func (e EngineCompositions) inSandbox(ctx context.Context, c *Case, head, node s verdicts[name] = raw } } + // A manifest the change brings is the check's own judge's (ADR 0578 §2, §3): its verdict there when the judge gave one, + // else none, which the composer reads as accepted; never the running indexer's (F5 of milestone 47/G). for _, p := range c.Pulls { for name := range p.Fps { - delete(verdicts, name) // a manifest the change brings is the checker's own judge's (ADR 0578 §2) + delete(verdicts, name) + if raw, ok := c.Judged[name]; ok { + verdicts[name] = raw + } } } // The answer carries its input as the object the seat's contract names, decoded with its numbers as written diff --git a/modules/mesh-delivery-checker/internal/check/engines_test.go b/modules/mesh-delivery-checker/internal/check/engines_test.go index a06a455a..76eb06dc 100644 --- a/modules/mesh-delivery-checker/internal/check/engines_test.go +++ b/modules/mesh-delivery-checker/internal/check/engines_test.go @@ -131,7 +131,8 @@ func TestTheCompositionIsMadeFromTheEnginesAnswers(t *testing.T) { if took := time.Since(began); took > 2*time.Second { t.Errorf("the nodes were not asked in parallel within the bound: %s", took) } - if v.State != meshdeliverycheckerclient.CompositionPass || v.Sdk != strings.Repeat("7", 40) || len(v.Machines) != 1 { + // No sandbox: the running engines composed, named so, though the node-engine's trunk head is known. + if v.State != meshdeliverycheckerclient.CompositionPass || v.Sdk != "node-engine" || len(v.Machines) != 1 { t.Fatalf("verdict = %+v", v) } for _, node := range []string{"ace", "g14", "laptop"} { @@ -229,6 +230,64 @@ func TestTheCompositionIsMadeFromTheEnginesAnswers(t *testing.T) { } }) + // ADR 0578 §2, §3, ADR 0656 §5 (milestone 47/G D12, D13): with no node-engine member, every node is composed by the + // composer built at the node-engine's trunk over the input its engine answers, and a manifest the change brings carries + // the check's own judge's verdict, not the running indexer's: one the running indexer refused and the judge accepted + // composes, and the verdict names the trunk's composer. + t.Run("a change's manifest the check's judge accepted is composed by the composer at the node-engine's trunk", func(t *testing.T) { + p := pullOf(repoA, 7, head1) + p.Modules = []string{"mesh-build-go"} + p.Fps = map[string]meshcatalog.CandidatesByGroupValuePullsItemFpsValue{"mesh-build-go": {Fp2: "src2:" + strings.Repeat("f", 64), Manifest: strings.Repeat("d", 64)}} + brings := caseOf(t, p) + brings.Judged = map[string]json.RawMessage{"mesh-build-go": json.RawMessage(`{"module":"mesh-build-go","state":"accepted"}`)} + trunkHead := strings.Repeat("7", 40) + withInput := composed("anchor") + withInput.Input = map[string]any{"nodes": []any{map[string]any{"name": "anchor"}}} + e := &fakeEngines{answers: map[string]ne.ComposeCandidateAnswer{"anchor": withInput}} + sb := &fakeSandbox{v: func(node string) composer.Verdict { + return composer.Verdict{V: composer.Version, Cases: []composer.CaseVerdict{{Name: "change", + Machines: map[string]composer.Machine{node: {Composes: true}}}}} + }} + indexer := map[string]meshcatalog.VerdictsByDigestValue{"mesh-build-go": {Module: "mesh-build-go", State: "refused", Problems: []string{"serves no verb input"}}, "mail": {Module: "mail", State: "accepted"}} + ec := EngineCompositions{Asker: e, Placed: func() ([]string, error) { return []string{"anchor"}, nil }, Bound: time.Second, + Sandbox: sb, Composer: func() string { return trunkHead }, Published: published{}, + Verdicts: func() map[string]meshcatalog.VerdictsByDigestValue { return indexer }} + raw, err := ec.Compose(context.Background(), brings) + if err != nil { + t.Fatal(err) + } + if v := rules.JudgeEngines(raw); v.State != meshdeliverycheckerclient.CompositionPass || v.Sdk != trunkHead { + t.Fatalf("verdict = %+v, want a pass composed at the node-engine's trunk %s", v, trunkHead) + } + if len(sb.jobs) != 1 || sb.jobs[0].Fetch.Commit != trunkHead || sb.jobs[0].Fetch.Repository != nodeEngineRepo { + t.Fatalf("jobs %+v: want one job at the node-engine's trunk", sb.jobs) + } + var req composer.Request + _ = json.Unmarshal(sb.jobs[0].Request, &req) + if got := string(req.Cases[0].Verdicts["mesh-build-go"]); got != string(brings.Judged["mesh-build-go"]) || len(req.Cases[0].Verdicts) != 2 { + t.Errorf("the request's verdicts %s: want the check's judge's for mesh-build-go and the indexer's for mail", req.Cases[0].Verdicts) + } + // A manifest the judge gave no verdict on carries none, which the composer reads as accepted (ADR 0578 §3). + brings.Judged = nil + if _, err := ec.Compose(context.Background(), brings); err != nil { + t.Fatal(err) + } + var none composer.Request + _ = json.Unmarshal(sb.jobs[len(sb.jobs)-1].Request, &none) + if _, kept := none.Cases[0].Verdicts["mesh-build-go"]; kept { + t.Errorf("the running indexer's verdict on the change's manifest was kept: %s", none.Cases[0].Verdicts) + } + // A refusal the trunk's composer makes stays the node-engine's own. + sb.v = func(node string) composer.Verdict { + return composer.Verdict{V: composer.Version, Cases: []composer.CaseVerdict{{Name: "change", + Machines: map[string]composer.Machine{node: {Composes: false, Reason: "a required value is unset"}}}}} + } + raw, _ = ec.Compose(context.Background(), brings) + if v := rules.JudgeEngines(raw); v.State != meshdeliverycheckerclient.CompositionFail || !strings.Contains(v.Words, "a required value is unset") { + t.Errorf("a refusal the trunk's composer makes: %+v", v) + } + }) + // Review finding 4: assignments that cannot be read is no mesh with no node: the composition is not made, said. t.Run("an unreadable assignments makes no composition", func(t *testing.T) { ec := EngineCompositions{Asker: &fakeEngines{}, Placed: func() ([]string, error) { return nil, errors.New("no such bucket") }} diff --git a/modules/mesh-delivery-checker/internal/check/manifests_judge.go b/modules/mesh-delivery-checker/internal/check/manifests_judge.go index 38ebfe6b..b2880e4e 100644 --- a/modules/mesh-delivery-checker/internal/check/manifests_judge.go +++ b/modules/mesh-delivery-checker/internal/check/manifests_judge.go @@ -11,6 +11,7 @@ import ( "strings" "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" + "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" "git.novox.be/novox/mesh-delivery/sdk/buildstep" "git.novox.be/novox/mesh-delivery/sdk/composer" "git.novox.be/novox/mesh-sdk/go/catalogue" @@ -140,6 +141,7 @@ func (st *state) judgeManifests(ctx context.Context) { if !ok { return } + st.judgedVerdicts(cv, at) m := rules.Manifests{Touched: touched, At: at, Change: cv, Trunk: trunkCase} if !st.indexerOf(ctx, req, touched, cv, at, &m) { return @@ -368,3 +370,29 @@ func (st *state) knownPackages(ctx context.Context) []string { func (st *state) trunkJudgeVersions() buildstep.PackageVersions { return packagesFor(st.j.Published, &Case{}, nil, DeliveryRepository) } + +// judgedVerdicts puts the check's own judge's verdict on each manifest the change brings on the case, as the seat +// mesh-catalog's verdict value, for the composer's request (ADR 0578 §2, §3; F5 of milestone 47/G): accepted, or refused +// with its words; a manifest the judge gave no verdict on carries none, which the composer reads as accepted. +func (st *state) judgedVerdicts(cv composer.CaseVerdict, at string) { + for _, p := range st.c.Pulls { + for name, fp := range p.Fps { + read, ok := cv.Manifests[name] + if fp.Manifest == "" || !ok || (!read.Accepted && read.Refused == "") { + continue + } + v := meshcatalog.VerdictsByDigestValue{Module: name, State: meshcatalogclient.VerdictAccepted, Judge: at} + if !read.Accepted { + v.State, v.Problems = meshcatalogclient.VerdictRefused, []string{read.Refused} + } + raw, err := json.Marshal(v) + if err != nil { + continue + } + if st.c.Judged == nil { + st.c.Judged = map[string]json.RawMessage{} + } + st.c.Judged[name] = raw + } + } +}