diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/seat.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/seat.go index 803eaf8..0e90670 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/seat.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/seat.go @@ -664,12 +664,25 @@ func (p *passer) compute(prop delivery.PlacementProposal, current map[string]del } get := kvGetter(p.kvs[delivery.ManifestsBucket]) r := worldReader{w: p.w, manifests: get} + // A read that fails is said, never dropped silently (novox/hq ADR 0578 §3); a refused manifest no longer fails it, and + // its module is held, named in the input's Refused. var now, with *compose.Input if in, err := frombus.Input(context.Background(), r, frombus.Mesh{}, frombus.Scope{Every: true}); err == nil { now = &in + } else if p.logf != nil { + p.logf("proposal %s: the current composition input cannot be read, so what it changes is judged without it: %v", prop.ID, err) } if in, err := frombus.Input(context.Background(), r, frombus.Mesh{}, frombus.Scope{Every: true, Placements: laid}); err == nil { with = &in + } else if p.logf != nil { + p.logf("proposal %s: the composition input with the proposal cannot be read, so what it changes is judged without it: %v", prop.ID, err) + } + if now != nil && len(now.Refused) > 0 && p.logf != nil { + var held []string + for _, m := range slices.Sorted(maps.Keys(now.Refused)) { + held = append(held, m+" ("+now.Refused[m]+")") + } + p.logf("proposal %s: held as refused by the indexer's judge: %s", prop.ID, strings.Join(held, "; ")) } nodes := map[string]bool{} for n := range current { diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/wants_tools.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/wants_tools.go index 945a959..1be3cb3 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/wants_tools.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/wants_tools.go @@ -102,7 +102,8 @@ func (s *wantsTools) manifests() (map[string]catalogue.Manifest, map[string]deli if err != nil { continue } - if man, err := catalogue.ParseManifest([]byte(kept.JSON)); err == nil { + // Decoded, never judged (ADR 0578 §3); one that does not decode is left out of the answer, as before. + if man, err := catalogue.Decode([]byte(kept.JSON)); err == nil { out[name] = man } } diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/world.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/world.go index 34df7f1..542311e 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/world.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/world.go @@ -197,10 +197,10 @@ func (w *world) manifest(ctx context.Context, get getter, digest string) parsed m, err := delivery.DecodeManifest(value) if err != nil { p = parsed{err: err.Error()} - } else if man, err := catalogue.ParseManifestForInputs([]byte(m.JSON)); err != nil { - // Read as the indexer reads it (issue 1751): a value falling 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 (ADR 0348 §7). - p = parsed{module: m.Module, err: err.Error()} + } else if man, err := catalogue.Decode([]byte(m.JSON)); err != nil { + // Decoded, never judged (novox/hq ADR 0578 §3): the indexer's judge judged it, and its verdict is taken from the + // module's value where it is read (world.mesh). + p = parsed{module: m.Module, err: "it does not decode: " + err.Error()} } else { p = parsed{module: m.Module, manifest: man} } @@ -313,6 +313,18 @@ func (w *world) mesh(ctx context.Context, manifests getter) assign.Mesh { continue } m.Digests[name] = mod.Manifest + // The indexer's verdict on it, carried in its value (ADR 0578 §3, §5): a refused module is held and named as one + // that cannot be read, and its verdict goes into the composer's input. + if v := mod.VerdictOf(name); v != nil { + if refused, _ := delivery.TakeManifestVerdict(v); refused { + if m.Verdicts == nil { + m.Verdicts = map[string]delivery.ManifestVerdict{} + } + m.Verdicts[name] = *v + m.Unreadable[name] = v.Why() + continue + } + } p := w.manifest(ctx, manifests, mod.Manifest) if p.err != "" { m.Unreadable[name] = p.err diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/world_test.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/world_test.go index 7ac1d63..158912b 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/world_test.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/world_test.go @@ -73,10 +73,11 @@ func TestTheChangeOfACandidate(t *testing.T) { } // 1297/B3b: a candidate whose module has no manifest digest yet, or whose manifest the indexer does not hold yet, waits, -// saying for what; an unreadable manifest is the change's, not a wait. +// saying for what; a manifest that does not decode is the change's, not a wait (one with a field the assigner does not +// know is decoded with it set aside, novox/hq ADR 0578 §6). func TestAChangeWaitsForItsManifests(t *testing.T) { f := fakeManifests{} - bad := f.add(t, "web", `{"module":"web","version":"2","scope":"x"}`) + bad := f.add(t, "web", `{"module":"web","version":"2","requires":"x"}`) for _, c := range []delivery.CandidatePull{ pull("novox/web", 1, map[string]delivery.CandidateModule{"web": {None2: "no preview yet"}}), pull("novox/web", 1, map[string]delivery.CandidateModule{"web": {FP2: fp('b'), Manifest: strings.Repeat("9", 64)}}), diff --git a/modules/mesh-delivery-assigner/go.mod b/modules/mesh-delivery-assigner/go.mod index ea06044..158687d 100644 --- a/modules/mesh-delivery-assigner/go.mod +++ b/modules/mesh-delivery-assigner/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-assigner 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-assigner/go.sum b/modules/mesh-delivery-assigner/go.sum index 8e0c33a..e0ce2ef 100644 --- a/modules/mesh-delivery-assigner/go.sum +++ b/modules/mesh-delivery-assigner/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-assigner/internal/assign/compose.go b/modules/mesh-delivery-assigner/internal/assign/compose.go index 35cb850..6c9eece 100644 --- a/modules/mesh-delivery-assigner/internal/assign/compose.go +++ b/modules/mesh-delivery-assigner/internal/assign/compose.go @@ -43,6 +43,9 @@ type Mesh struct { // InputUnread says why that input could not be read from the bus, when it could not: the check then composes the // nodes and their assignments alone, and says so in its warnings. InputUnread string + // Verdicts are the indexer's refusals of trunk manifests, by module, as their values carry them (novox/hq ADR 0578 + // §3): each such module is in Unreadable too, and the composer's input carries the verdict (§5). + Verdicts map[string]delivery.ManifestVerdict } // Change is a group's change as the indexer's candidate holds it: the manifest of every module it moves or adds, those diff --git a/modules/mesh-delivery-assigner/internal/assign/composer.go b/modules/mesh-delivery-assigner/internal/assign/composer.go index b2dac9e..5abbc2c 100644 --- a/modules/mesh-delivery-assigner/internal/assign/composer.go +++ b/modules/mesh-delivery-assigner/internal/assign/composer.go @@ -49,10 +49,18 @@ func HeadJob(m Mesh, c Change) (delivery.ComposeJob, error) { in.Catalogue = nil fromClaims = len(in.Holdings) == 0 } - changeCase := composer.Case{Name: CaseChange, Manifests: change} + // Each trunk manifest the indexer refused goes in with its verdict, so the composer holds and names it (ADR 0578 §5); + // a manifest the change brings has no verdict of the indexer yet, and the checker's judge judges it (§2). + changeVerdicts := map[string]delivery.ManifestVerdict{} + for name, v := range m.Verdicts { + if _, changed := c.Digests[name]; !changed { + changeVerdicts[name] = v + } + } + changeCase := composer.Case{Name: CaseChange, Manifests: change, Verdicts: changeVerdicts} req := composer.Request{V: composer.Version, Input: in, Depth: composer.DepthResolve, HoldingsFromClaims: fromClaims, Machines: Touched(m, c), - Cases: []composer.Case{{Name: CaseTrunk, Manifests: trunk}, changeCase}} + Cases: []composer.Case{{Name: CaseTrunk, Manifests: trunk, Verdicts: m.Verdicts}, changeCase}} return job(SDKRepository, c.SDKHead, SDKDir, req, "the mesh-sdk head of "+c.Group) } @@ -139,10 +147,10 @@ type Reader struct { // FromComposer is the composition check of a group with a mesh-sdk member, judged from the composer built at its head // (ADR 0482 clauses 3 to 5, issues 1622 and 1623): the same failures as Compose, read from the composer's two cases, the -// verdict naming the mesh-sdk the composer was built with; a trunk manifest the head's mesh-sdk refuses fails too (the -// group would deliver a reader that refuses it); and each reader that refuses a changed manifest the head reads fails -// it, naming the module and its words, unless the reader's mesh-sdk has no composer to ask (before issue 1621), which is -// said and judged nothing. +// verdict naming the mesh-sdk the composer was built with; a trunk manifest the head's mesh-sdk cannot decode fails too +// (the group would deliver a reader that cannot read it). A reader that cannot read a changed manifest is a warning, +// never a failure: it decodes leniently and sets a field it does not know aside (novox/hq ADR 0578 §6, which changes +// ADR 0482 §5). func FromComposer(m Mesh, c Change, head Ran, readers []Reader, took time.Duration) delivery.CompositionVerdict { v := delivery.CompositionVerdict{Group: c.Group, Heads: c.Heads, SDK: "mesh-sdk " + c.SDKHead, Took: took.Milliseconds()} machines := Touched(m, c) @@ -245,8 +253,10 @@ func judgeReaders(c Change, head composer.CaseVerdict, readers []Reader) (fails, if !head.Manifests[name].Accepted || read.Manifests[name].Accepted { continue } - fails = append(fails, fmt.Sprintf("the manifest of %s uses what %s (mesh-sdk %s) cannot read: %s; the group must move %s onto "+ - "mesh-sdk %s", name, at, r.Ran.Verdict.SDK, read.Manifests[name].Refused, r.Module, c.SDKHead)) + // ADR 0578 §6: no longer a failure; the reader sets what it does not know aside, and the part that needs it takes + // the newer package (ADR 0573 §2). + warnings = append(warnings, fmt.Sprintf("the manifest of %s uses what %s (mesh-sdk %s) cannot read: %s; it reads the rest "+ + "once it takes mesh-sdk %s", name, at, r.Ran.Verdict.SDK, read.Manifests[name].Refused, c.SDKHead)) } } return fails, warnings diff --git a/modules/mesh-delivery-assigner/internal/assign/composer_test.go b/modules/mesh-delivery-assigner/internal/assign/composer_test.go index 2bc3258..8a07f1f 100644 --- a/modules/mesh-delivery-assigner/internal/assign/composer_test.go +++ b/modules/mesh-delivery-assigner/internal/assign/composer_test.go @@ -141,10 +141,11 @@ func TestAFieldTheGroupAddsPassesWithTheHeadsComposer(t *testing.T) { } } -// 1623/AC1, AC2: the group of mesh-sdk and mesh-catalog adding and using a field, without a controller member, fails -// naming the field and mesh-controller, whose running build pins an older mesh-sdk; with a controller member pinned to -// the SDK head (its composer reads what the head reads) it passes. -func TestAFieldTheRunningControllerCannotReadFailsTheGroup(t *testing.T) { +// 1623/AC1, AC2 as ADR 0578 §6 changes them: the group of mesh-sdk and mesh-catalog adding and using a field, without a +// controller member, passes with a warning naming the field and mesh-controller, whose running build pins an older +// mesh-sdk that cannot read it: a reader sets a field it does not know aside, so it fails nothing. With a controller +// member pinned to the SDK head it passes with no such warning. +func TestAFieldTheRunningControllerCannotReadIsAWarning(t *testing.T) { c := webShares(nil) job, _ := HeadJob(digestedMesh(), c) head := run(t, job, asTheHeadReads) @@ -156,10 +157,12 @@ func TestAFieldTheRunningControllerCannotReadFailsTheGroup(t *testing.T) { if rjob.Fetch["commit"] != running || rjob.Dir != delivery.RootPath { t.Fatalf("reader job = %+v", rjob) } - old := run(t, rjob, nil) // the running controller's mesh-sdk reads web2 as the assigner's own does: refused + // The running controller's mesh-sdk, older than the lenient decoder, refuses web2 for its field. + old := Ran{State: delivery.StepPassed, Verdict: &composer.Verdict{V: composer.Version, SDK: "v0.1.13", Cases: []composer.CaseVerdict{ + {Name: CaseChange, Manifests: map[string]composer.ManifestRead{"web": {Refused: `json: unknown field "shares"`}}}}}} v := FromComposer(digestedMesh(), c, head, []Reader{{Module: "mesh-controller", Commit: running, Ran: old}}, 0) - if v.State != delivery.CompositionFail || !strings.Contains(v.Words, "shares") || !strings.Contains(v.Words, "move mesh-controller onto mesh-sdk "+sdkHead) { - t.Fatalf("without a controller member: %+v, want a fail naming the field and mesh-controller", v) + if v.State != delivery.CompositionPass || !hasWarning(v, "shares") || !hasWarning(v, "mesh-controller at "+running) { + t.Fatalf("without a controller member: %+v, want a pass warning of the field and mesh-controller", v) } member := "2222222222222222222222222222222222222222" pinned := run(t, rjob, asTheHeadReads) @@ -196,9 +199,26 @@ func TestTheHeadsComposerFailingOrRefusingTheTrunkFails(t *testing.T) { t.Fatalf("a head composer that did not run: %+v", v) } job, _ := HeadJob(digestedMesh(), c) - strict := map[string]string{"d-web2": asTheHeadReads["d-web2"], "d-mail": `{"module": "mail", "version": "1", "gone": true}`} + strict := map[string]string{"d-web2": asTheHeadReads["d-web2"], "d-mail": `{"module": "mail", "version": "1", "requires": "x"}`} v = FromComposer(digestedMesh(), c, run(t, job, strict), nil, 0) if v.State != delivery.CompositionFail || !strings.Contains(v.Words, "refuses the trunk's manifest of mail") { t.Fatalf("a head that refuses a trunk manifest: %+v", v) } + // ADR 0578 §5: a trunk manifest the indexer refused goes into the composer's input with its verdict, in both cases; + // the composer holds and names it, and the group is not blamed for it. + m := digestedMesh() + m.Verdicts = map[string]delivery.ManifestVerdict{"mail": {Module: "mail", State: delivery.VerdictRefused, Problems: []string{"it is wrong"}}} + m.Unreadable = map[string]string{"mail": m.Verdicts["mail"].Why()} + job, _ = HeadJob(m, c) + var req composer.Request + if err := json.Unmarshal(job.Request, &req); err != nil || !req.Cases[0].Verdicts["mail"].Refused() || !req.Cases[1].Verdicts["mail"].Refused() { + t.Fatalf("the request carries no verdict: %+v %v", req.Cases, err) + } + head := run(t, job, asTheHeadReads) + if trunk, _ := caseOf(head.Verdict, CaseTrunk); !strings.Contains(trunk.Manifests["mail"].Refused, "refused the manifest of mail: it is wrong") { + t.Fatalf("the composer did not name the refusal: %+v", trunk.Manifests["mail"]) + } + if v = FromComposer(m, c, head, nil, 0); v.State != delivery.CompositionPass { + t.Fatalf("a refusal on the trunk blamed the group: %+v", v) + } }