From efef7183efbdb74745ca058c50cd11296d5ce775 Mon Sep 17 00:00:00 2001 From: jochen Date: Sun, 1 Nov 2026 02:15:54 +0100 Subject: [PATCH] =?UTF-8?q?Let=20grants,=20builder=20and=20checker=20decod?= =?UTF-8?q?e=20manifests,=20and=20the=20checker=20judge=20a=20change=20wit?= =?UTF-8?q?h=20the=20indexer's=20judge=20(ADR=200578=20=C2=A72,=20=C2=A73)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Issue 1585, S12: grants, builder and checker read with catalogue.Decode and grants holds a module the indexer refused; the checker asks the Go build seat to run mesh-judge, built at mesh-delivery's trunk or at the group's mesh-delivery head when it changes the judge, over the catalogue as the change leaves it, and fails on a refusal. Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL --- .../cmd/mesh-delivery-builder/bus.go | 3 +- modules/mesh-delivery-builder/go.mod | 2 +- modules/mesh-delivery-builder/go.sum | 2 + .../cmd/mesh-delivery-checker/bus.go | 8 +- .../cmd/mesh-delivery-checker/judge.go | 16 +- modules/mesh-delivery-checker/go.mod | 2 +- modules/mesh-delivery-checker/go.sum | 2 + .../internal/check/examiner_test.go | 62 +++++++ .../internal/check/judge.go | 11 ++ .../internal/check/manifests_judge.go | 169 ++++++++++++++++++ modules/mesh-grants/go.mod | 2 +- modules/mesh-grants/go.sum | 2 + modules/mesh-grants/grants_test.go | 10 +- modules/mesh-grants/internal/grants/inputs.go | 16 +- modules/mesh-grants/leasecheck_test.go | 6 +- 15 files changed, 296 insertions(+), 17 deletions(-) create mode 100644 modules/mesh-delivery-checker/internal/check/manifests_judge.go diff --git a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/bus.go b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/bus.go index 2f32c84..4023faf 100644 --- a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/bus.go +++ b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/bus.go @@ -123,5 +123,6 @@ func (m manifestReader) Manifest(ctx context.Context, digest string) (catalogue. if err != nil { return catalogue.Manifest{}, err } - return catalogue.ParseManifestForInputs([]byte(kept.JSON)) + // Decoded, never judged (novox/hq ADR 0578 §3): what a build needs of it is read, a field it does not know set aside. + return catalogue.Decode([]byte(kept.JSON)) } diff --git a/modules/mesh-delivery-builder/go.mod b/modules/mesh-delivery-builder/go.mod index 5bd2168..8b35c26 100644 --- a/modules/mesh-delivery-builder/go.mod +++ b/modules/mesh-delivery-builder/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-builder go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031084708-acbf80ad910a + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101005736-7b09de1e5502 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-builder/go.sum b/modules/mesh-delivery-builder/go.sum index 8e0c33a..e0ce2ef 100644 --- a/modules/mesh-delivery-builder/go.sum +++ b/modules/mesh-delivery-builder/go.sum @@ -1,5 +1,7 @@ git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031084708-acbf80ad910a h1:DcSvfEmdPBKqCg5OmXRNb269EkkPe7BsEp+RykEhkgE= git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031084708-acbf80ad910a/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101005736-7b09de1e5502 h1:XndvkzyAFNde/821lEfYObqMTURzmve/w9Ks+QrBBJ8= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101005736-7b09de1e5502/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/bus.go b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/bus.go index a5453a6..62eccf0 100644 --- a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/bus.go +++ b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/bus.go @@ -125,11 +125,11 @@ func (m manifestReader) Manifest(ctx context.Context, digest string) (catalogue. return parseManifest(kept.JSON) } -// parseManifest reads a manifest the indexer kept as the indexer and the assigner read it (issues 1751, 1906): a config value that -// falls back to a predefined key is judged by the merge gate in the catalogue, against the predefined.json at its root that this -// process never holds, so it is not unreadable here (novox/hq ADR 0348 §7). +// parseManifest decodes a manifest the indexer kept, as every reader does (novox/hq ADR 0578 §3): judged by nothing here, +// a field it does not know set aside. The checker judges a change's manifests with the indexer's judge, run as a step +// (judge.go). func parseManifest(raw string) (catalogue.Manifest, error) { - return catalogue.ParseManifestForInputs([]byte(raw)) + return catalogue.Decode([]byte(raw)) } // awaitedKV is a bucket that may appear after the start (process.AwaitBucket) as a check.KV: a call while it is not on the diff --git a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/judge.go b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/judge.go index 8576332..8442c7d 100644 --- a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/judge.go +++ b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/judge.go @@ -69,7 +69,7 @@ func newJudge(d judgeDeps) check.Judge { nodes = d.nodes } return &check.Examiner{Nodes: nodes, Compositions: d.compositions, Asks: d.asks, Toolchains: d.tools, - Kept: check.Kept{Store: d.kept}, Previews: d.previews, Modules: d.repos, Packages: d.repos, Forge: d.forge, Runs: d.repos, Builds: d.builds, Announce: d.announce, Trunks: d.trunks, Logf: d.logf} + Kept: check.Kept{Store: d.kept}, Previews: d.previews, Modules: d.repos, Packages: d.repos, Forge: d.forge, Runs: d.repos, Builds: d.builds, Announce: d.announce, Trunks: d.trunks, Manifests: d.repos, Logf: d.logf} } // manifestKV reads a manifest by its digest. @@ -221,6 +221,20 @@ func (r buildReader) Build(ctx context.Context, module, fp2 string) (delivery.Bu return b, err == nil, err } +// ManifestDigests is the manifest digest of every module the indexer holds, by module: the catalogue the indexer's judge +// judges a change's manifests in (novox/hq ADR 0578 §2). +func (r *repoModules) ManifestDigests() map[string]string { + r.mu.Lock() + defer r.mu.Unlock() + out := map[string]string{} + for module, digest := range r.typed { + if digest != "" { + out[module] = digest + } + } + return out +} + // CommitOf is the commit the indexer's record of the module names: the commit its fingerprint first appeared at, so the source // of the build the mesh runs (a later merge that leaves the module alone writes no record, the operator 2026-10-25). func (r *repoModules) CommitOf(module string) string { diff --git a/modules/mesh-delivery-checker/go.mod b/modules/mesh-delivery-checker/go.mod index eccbc07..a36c677 100644 --- a/modules/mesh-delivery-checker/go.mod +++ b/modules/mesh-delivery-checker/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-checker go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031084708-acbf80ad910a + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101005736-7b09de1e5502 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-checker/go.sum b/modules/mesh-delivery-checker/go.sum index 8e0c33a..e0ce2ef 100644 --- a/modules/mesh-delivery-checker/go.sum +++ b/modules/mesh-delivery-checker/go.sum @@ -1,5 +1,7 @@ git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031084708-acbf80ad910a h1:DcSvfEmdPBKqCg5OmXRNb269EkkPe7BsEp+RykEhkgE= git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031084708-acbf80ad910a/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101005736-7b09de1e5502 h1:XndvkzyAFNde/821lEfYObqMTURzmve/w9Ks+QrBBJ8= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101005736-7b09de1e5502/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-checker/internal/check/examiner_test.go b/modules/mesh-delivery-checker/internal/check/examiner_test.go index d80dd7e..bad6d18 100644 --- a/modules/mesh-delivery-checker/internal/check/examiner_test.go +++ b/modules/mesh-delivery-checker/internal/check/examiner_test.go @@ -17,6 +17,7 @@ import ( "time" "git.novox.be/novox/mesh-sdk/go/buildstep" + "git.novox.be/novox/mesh-sdk/go/composer" "git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/delivery/buildrun" ) @@ -928,3 +929,64 @@ func TestASecondCheckOfTheSameHeadsKeepsWhatTheFirstPassed(t *testing.T) { t.Errorf("%d test steps ran on the second check", n) } } + +// fixedDigests are the indexer's manifest digests as a test gives them. +type fixedDigests map[string]string + +func (f fixedDigests) ManifestDigests() map[string]string { return f } + +// ADR 0578 §2: a change's manifests are judged by the indexer's judge, built at mesh-delivery's trunk and run as the +// composer is (a compose ask of its command); a refusal fails the check naming the manifest and each problem; a group +// whose mesh-delivery member changes the judge is judged by that member's head. +func TestTheIndexersJudgeJudgesTheChangesManifests(t *testing.T) { + web, trunk := strings.Repeat("e", 64), strings.Repeat("d", 40) + for _, row := range []struct { + name string + refused string + fails string + }{ + {name: "accepted"}, + {name: "refused", refused: "web requires \"amqp\"\nweb has no id", fails: "the indexer's judge at mesh-delivery's trunk " + strings.Repeat("d", 40) + ` refuses the manifest of web: web requires "amqp"; web has no id`}, + } { + b := newBench(t) + b.ex.Manifests = fixedDigests{"mail": strings.Repeat("a", 64), "web": strings.Repeat("b", 64)} + b.ex.Trunks = fixedTrunks{heads: map[string]string{DeliveryRepository: trunk}} + var judged []delivery.ComposeJob + answer := b.asks.Answer + b.asks.Answer = func(a delivery.StepAsk) delivery.Step { + if a.Compose == nil { + return answer(a) + } + judged = append(judged, *a.Compose) + read := composer.ManifestRead{Accepted: row.refused == "", Refused: row.refused} + 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: true}}}}}) + result, _ := json.Marshal(delivery.ComposeAnswer{Key: "k", Verdict: verdict}) + return delivery.Step{ID: a.ID, State: delivery.StepPassed, Result: result} + } + p := pullOf(repoA, 7, head1) + 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 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) + } + var req composer.Request + if err := json.Unmarshal(judged[0].Request, &req); err != nil || req.Cases[0].Manifests["web"].Digest != web || + req.Cases[0].Manifests["mail"].Digest != strings.Repeat("a", 64) { + t.Fatalf("%s: the request %+v: %v", row.name, req, err) + } + switch { + case row.fails == "" && v.State != delivery.VerdictPass: + t.Errorf("%s: %+v", row.name, v) + case row.fails != "" && (v.State != delivery.VerdictFail || !strings.Contains(v.Failed.Words, row.fails)): + t.Errorf("%s: %+v, want a fail saying %q", row.name, v.Failed, row.fails) + } + } + if !changesJudge(DeliveryRepository, []Fetched{{Changed: []string{JudgeDir + "/judge/judge.go"}}}) || + changesJudge(DeliveryRepository, []Fetched{{Changed: []string{JudgeDir + "/internal/index/x.go"}}}) || + changesJudge("novox/mesh-catalog", []Fetched{{Changed: []string{JudgeDir + "/judge/judge.go"}}}) { + t.Error("which merge changes the judge") + } +} diff --git a/modules/mesh-delivery-checker/internal/check/judge.go b/modules/mesh-delivery-checker/internal/check/judge.go index c0fa364..02affea 100644 --- a/modules/mesh-delivery-checker/internal/check/judge.go +++ b/modules/mesh-delivery-checker/internal/check/judge.go @@ -106,6 +106,9 @@ type Examiner struct { } // Trunks are the trunk heads of every repository, for the siblings a repository's tests read. Trunks interface{ Heads() map[string]string } + // Manifests are the indexer's manifest digests by module: the catalogue a change's manifests are judged in by the + // indexer's judge (manifests_judge.go, novox/hq ADR 0578 §2); nil judges no manifest. + Manifests TrunkManifests // Compositions answers the assigner's composition check of a check by the check's key (delivery.CompositionKey), // waiting until the assigner wrote a verdict that does not wait, or ctx ends (ADR 0528 clause 5, issue 1297); nil // leaves the composition not run. @@ -214,6 +217,9 @@ type state struct { slow []string // beside are the siblings read at their trunk's head, by repository (ADR 0548 §6). beside map[string]string + // judgeHead is the group's mesh-delivery member head when its merge changes the judge (ADR 0578 §2); empty judges + // with mesh-delivery's trunk. + judgeHead string } // 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 @@ -224,6 +230,10 @@ func (j *Examiner) Judge(ctx context.Context, c *Case) (Outcome, error) { if v := st.judgeAll(ctx); v != nil { return st.finish(ctx, *v), nil } + // Every manifest the change touches, judged by the indexer's judge (ADR 0578 §2): a refusal fails the check. + if v := st.judgeManifests(ctx); v != nil { + return st.finish(ctx, *v), nil + } return st.finish(ctx, delivery.Verdict{State: delivery.VerdictPass}), nil } @@ -383,6 +393,7 @@ func (st *state) judgeAll(ctx context.Context) *delivery.Verdict { if st.c.Try == nil { // a try owes the indexer no preview: no pull request holds its head st.j.writePreview(ctx, p, merged) } + st.judgedByMember(p, merged) if p.Repository == SDKRepository && len(st.c.Pulls) > 1 && st.j.changesPackage(p, merged) { groupPackage = map[string]any{"repository": p.Repository, "commit": p.Head, "ref": refOf(p.Number)} } diff --git a/modules/mesh-delivery-checker/internal/check/manifests_judge.go b/modules/mesh-delivery-checker/internal/check/manifests_judge.go new file mode 100644 index 0000000..0568b29 --- /dev/null +++ b/modules/mesh-delivery-checker/internal/check/manifests_judge.go @@ -0,0 +1,169 @@ +package check + +import ( + "context" + "encoding/json" + "fmt" + "maps" + "slices" + "strings" + + "git.novox.be/novox/mesh-sdk/go/buildstep" + "git.novox.be/novox/mesh-sdk/go/composer" + "git.novox.be/novox/mesh-sdk/go/delivery" +) + +// A change's manifests are judged by the indexer's judge (novox/hq ADR 0578 §2): the checker asks the Go build seat to +// build the judge's command (mesh-delivery's modules/mesh-delivery-indexer, cmd/mesh-judge) at mesh-delivery's trunk, or +// at the group's mesh-delivery member head when that member changes the judge, and to run it in its sandbox over the +// catalogue as the change leaves it, exactly as the composer is run (delivery.ComposeJob, ADR 0482 §4). A manifest the +// change touches that the judge refuses fails the check, naming the manifest and each problem. The checker links no +// judge of its own. + +// The judge the checker runs. +const ( + DeliveryRepository = "novox/mesh-delivery" + JudgeDir = "modules/mesh-delivery-indexer" + JudgePackage = "mesh-delivery-indexer/cmd/mesh-judge" + // JudgeCase is the one case a judge request holds: the catalogue as the change leaves it. + JudgeCase = "change" + // judgeBetween is how the judge names the problems between manifests that no one module's removal ends. + judgeBetween = "(between manifests)" +) + +// judgePaths are the paths of mesh-delivery whose change changes the judge: a group whose mesh-delivery member changes +// one is judged by that member's head. +var judgePaths = []string{JudgeDir + "/judge/", JudgeDir + "/cmd/mesh-judge/", JudgeDir + "/go.mod", JudgeDir + "/go.sum"} + +// changesJudge says whether a merge of mesh-delivery with these changed paths changes the judge. +func changesJudge(repository string, merged []Fetched) bool { + if repository != DeliveryRepository { + return false + } + for _, f := range merged { + for _, p := range f.Changed { + for _, j := range judgePaths { + if p == j || strings.HasPrefix(p, j) { + return true + } + } + } + } + return false +} + +// TrunkManifests says the manifest digest of every module the indexer holds (its modules state), by module. +type TrunkManifests interface { + ManifestDigests() map[string]string +} + +// judgeRequest is the judge's request over the catalogue as the case leaves it: every trunk manifest by its digest, each +// one a pull request touches replaced by the digest its candidate names; touched are those modules, sorted. +func judgeRequest(trunk map[string]string, pulls []Pull) (req composer.Request, touched []string) { + manifests := map[string]composer.Manifest{} + for module, digest := range trunk { + if digest != "" { + manifests[module] = composer.Manifest{Digest: digest} + } + } + seen := map[string]bool{} + for _, p := range pulls { + for module, cm := range p.Fps { + if cm.Manifest == "" { + continue + } + manifests[module] = composer.Manifest{Digest: cm.Manifest} + seen[module] = true + } + } + return composer.Request{V: composer.Version, Cases: []composer.Case{{Name: JudgeCase, Manifests: manifests}}}, + slices.Sorted(maps.Keys(seen)) +} + +// judgeManifests runs the indexer's judge over the case's manifests and fails the check for each touched manifest it +// refuses. A case that touches no manifest, or an examiner that reads no trunk manifests, judges nothing; a judge that +// cannot be asked or does not answer fails the check, said, never passes it. +func (st *state) judgeManifests(ctx context.Context) *delivery.Verdict { + if st.j.Manifests == nil || st.c.Try != nil { + return nil + } + req, touched := judgeRequest(st.j.Manifests.ManifestDigests(), st.c.Pulls) + if len(touched) == 0 { + return nil + } + commit, at := st.judgeHead, "the group's mesh-delivery head "+st.judgeHead + if commit == "" { + if st.j.Trunks != nil { + commit = st.j.Trunks.Heads()[DeliveryRepository] + } + 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") + } + raw, err := json.Marshal(req) + if err != nil { + return 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 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, 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)) + } + var refused []string + for _, module := range touched { + if r, ok := cv.Manifests[module]; ok && !r.Accepted { + refused = append(refused, fmt.Sprintf("the indexer's judge at %s refuses the manifest of %s: %s", at, module, + strings.ReplaceAll(r.Refused, "\n", "; "))) + } + } + if between := cv.Manifests[judgeBetween]; between.Refused != "" { + st.c.note("the indexer's judge names problems between manifests that no one module's removal ends: " + + strings.ReplaceAll(between.Refused, "\n", "; ")) + } + if len(refused) > 0 { + return 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 +} + +// judgeAnswer reads the judge's case from its step, or why it cannot. +func judgeAnswer(step delivery.Step) (composer.CaseVerdict, string) { + if step.State != delivery.StepPassed { + return composer.CaseVerdict{}, fmt.Sprintf("its step %s: %s", step.State, step.Reason) + } + var a delivery.ComposeAnswer + if err := json.Unmarshal(step.Result, &a); err != nil { + return composer.CaseVerdict{}, "its step's answer cannot be read: " + err.Error() + } + var v composer.Verdict + if err := json.Unmarshal(a.Verdict, &v); err != nil || v.V != composer.Version { + return composer.CaseVerdict{}, fmt.Sprintf("its verdict cannot be read (version %d): %v", v.V, err) + } + for _, c := range v.Cases { + if c.Name == JudgeCase { + return c, "" + } + } + return composer.CaseVerdict{}, "its verdict holds no case " + JudgeCase +} + +// judgedByMember notes the group's mesh-delivery member head as the judge's when its merge changes the judge. +func (st *state) judgedByMember(p Pull, merged []Fetched) { + if changesJudge(p.Repository, merged) { + st.judgeHead = p.Head + } +} diff --git a/modules/mesh-grants/go.mod b/modules/mesh-grants/go.mod index eddf813..80599b0 100644 --- a/modules/mesh-grants/go.mod +++ b/modules/mesh-grants/go.mod @@ -3,7 +3,7 @@ module mesh-grants go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031230538-c28c645ca406 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101005736-7b09de1e5502 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-grants/go.sum b/modules/mesh-grants/go.sum index 08bf0b2..b9876d1 100644 --- a/modules/mesh-grants/go.sum +++ b/modules/mesh-grants/go.sum @@ -1,5 +1,7 @@ git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031230538-c28c645ca406 h1:Z4RcH5Ft4OAAG3F538NXIHP9F3pkyk3j0LtP9mqu1qQ= git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031230538-c28c645ca406/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101005736-7b09de1e5502 h1:XndvkzyAFNde/821lEfYObqMTURzmve/w9Ks+QrBBJ8= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101005736-7b09de1e5502/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-grants/grants_test.go b/modules/mesh-grants/grants_test.go index cc1c886..21cb4f7 100644 --- a/modules/mesh-grants/grants_test.go +++ b/modules/mesh-grants/grants_test.go @@ -307,6 +307,13 @@ func (b *fakeBus) manifestOf(module, raw string) { b.put(grants.ReadModules, module, delivery.Module{V: delivery.Version, FP2: "src2:" + strings.Repeat("ab", 32), Manifest: digest, Repository: "novox/x", Commit: "c"}) } +// refuse marks the indexer's refusal of module's manifest in its value (novox/hq ADR 0578 §1). +func (b *fakeBus) refuse(module, raw, problem string) { + b.put(grants.ReadModules, module, delivery.Module{V: delivery.Version, FP2: "src2:" + strings.Repeat("ab", 32), + Manifest: delivery.ManifestDigest([]byte(raw)), Repository: "novox/x", Commit: "c", Verdict: delivery.VerdictRefused, + Problems: []string{problem}}) +} + func (b *fakeBus) assign(node, module string) { b.put(grants.ReadAssignments, node+"."+module, map[string]any{"node": node, "module": module, "assigned": "2026-10-25T20:00:00Z"}) } @@ -728,13 +735,14 @@ func TestGrants(t *testing.T) { t.Run("a predefined-key fallback is read with no list; every refused manifest listed", func(t *testing.T) { // mod-p's value falls back to a predefined key, as claude-code's managed_settings does, and no list is on the bus - // (issue 1945); mod-z's manifest carries a key mesh-sdk's reader refuses, and mod-z is assigned nowhere. + // (issue 1945); mod-z's manifest the indexer's judge refused (ADR 0578 §3), and mod-z is assigned nowhere. const pManifest = `{"module":"mod-p","version":"1","own-secrets":{"broker":"/var/lib/p/broker"},` + `"config":{"size":{"type":"number","description":"The size text shows in, in points.","predefined":"font-size","default":12,"runtime":true}}}` const zManifest = `{"module":"mod-z","version":"1","no-such-key":true}` b := fixture() b.manifestOf("mod-p", pManifest) b.manifestOf("mod-z", zManifest) + b.refuse("mod-z", zManifest, "it carries no-such-key") b.assign("b", "mod-p") // mesh-wireguard is provided by the mesh, not read from a repository: no manifest in the indexer, none refused, // no gap, and its manifest is the one mesh-sdk provides (issue 1945). diff --git a/modules/mesh-grants/internal/grants/inputs.go b/modules/mesh-grants/internal/grants/inputs.go index 2f71b4c..5ed4184 100644 --- a/modules/mesh-grants/internal/grants/inputs.go +++ b/modules/mesh-grants/internal/grants/inputs.go @@ -10,6 +10,7 @@ package grants import ( "crypto/sha256" "encoding/json" + "errors" "fmt" "slices" "sort" @@ -206,12 +207,11 @@ func (c *Manifests) get(r Reader, digest string) (catalogue.Manifest, error) { if err != nil { return catalogue.Manifest{}, err } - // Read for its inputs, as every delivery part reads a manifest (issue 1751): the merge gate judged its config values, - // and the part reads its seats, buckets, logs and streams, never a value; so a value falling back to a predefined key - // is taken as it declares itself, where ParseManifest refused it with no list and placed its module nowhere (issue 1945). - m, err = catalogue.ParseManifestForInputs([]byte(v.JSON)) + // Decoded, never judged (novox/hq ADR 0578 §3): the indexer's judge judged it, and its verdict is taken where the + // module's value is read (Inputs); a field this part does not know is set aside. + m, err = catalogue.Decode([]byte(v.JSON)) if err != nil { - return catalogue.Manifest{}, fmt.Errorf("the manifest %s is refused by mesh-sdk's reader: %w", digest, err) + return catalogue.Manifest{}, fmt.Errorf("the manifest %s does not decode: %w", digest, err) } c.mu.Lock() defer c.mu.Unlock() @@ -384,6 +384,12 @@ func ReadInputs(r Reader, cache *Manifests) Inputs { in.NoFingerprint[module] = why } var m catalogue.Manifest + if err == nil { + // The indexer's refusal, carried in its value (ADR 0578 §3): held and named as a refused manifest. + if verdict := v.VerdictOf(module); verdict != nil && verdict.Refused() { + err = errors.New(verdict.Why()) + } + } if err == nil { m, err = cache.get(r, v.Manifest) } diff --git a/modules/mesh-grants/leasecheck_test.go b/modules/mesh-grants/leasecheck_test.go index b0d084a..ec05baa 100644 --- a/modules/mesh-grants/leasecheck_test.go +++ b/modules/mesh-grants/leasecheck_test.go @@ -70,9 +70,11 @@ func TestTheLeaseCheckJudgesEveryDependencyOnTheController(t *testing.T) { controllerHolds, want{reason: "manifest of mod-m could not be read", bothPhases: true}, func(b *fakeBus) { delete(b.states[grants.ReadManifests], delivery.ManifestDigest([]byte(mManifest))) }}, - {"a manifest mesh-sdk refuses fails both phases, named", + {"a manifest the indexer's judge refuses fails both phases, named", map[string]string{"mod-l": `{"module":"mod-l","version":"1"}`, "mod-r": `{"module":"mod-r","version":"1","waits":["mesh-controller.checked"]}`}, - controllerHolds, want{reason: "manifest of mod-r could not be read", bothPhases: true}, nil}, + controllerHolds, want{reason: "manifest of mod-r could not be read", bothPhases: true}, func(b *fakeBus) { + b.refuse("mod-r", `{"module":"mod-r","version":"1","waits":["mesh-controller.checked"]}`, "waits on what no record keeps") + }}, {"a lease already handed away leaves nothing to write", map[string]string{}, handedAway, want{handedAway: true, reason: "there is nothing to write: the lease is handed to the operator's hand-over"}, nil},