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)

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
This commit is contained in:
2026-11-07 03:00:35 +01:00
parent 2159375873
commit df56af0765
6 changed files with 125 additions and 9 deletions
@@ -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,
@@ -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"
@@ -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
@@ -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
@@ -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") }}
@@ -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
}
}
}