From 5c4016c9601c59aea0334822d4a9e1e884ff488d Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 4 Nov 2026 15:30:11 +0100 Subject: [PATCH] Join item 5: the judge's packages rule takes the known packages by their ecosystem names Design 32 names a package in a manifest's packages by its name in its own language, so the judge (DLV-I14) now refuses a name no known package carries instead of any name that is not a declared /. The indexer hands it its packages state's names, kept before the merge's judge pass; the checker hands it its followed packages picture plus, for a package new in the group, the name read at the member's head. Both read a name with one reader, sdk/packagename.Of, and the judge request carries the names in its Input (packagename.Input). The series-form check stays. Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig --- .../cmd/mesh-delivery-checker/judge.go | 2 +- .../cmd/mesh-delivery-checker/judge_test.go | 10 +++ .../cmd/mesh-delivery-checker/packages.go | 31 +++++++- .../internal/check/examiner_test.go | 20 +++++ .../internal/check/judge.go | 6 ++ .../internal/check/manifests_judge.go | 66 ++++++++++++++++ .../internal/index/candidates.go | 9 ++- .../internal/index/control_records_test.go | 4 + .../internal/index/series.go | 53 +------------ .../internal/index/verdicts.go | 31 +++++++- .../judge/busstate_test.go | 57 +++++++++++--- modules/mesh-delivery-indexer/judge/judge.go | 32 +++++--- .../mesh-delivery-indexer/judge/judge_test.go | 2 +- .../mesh-delivery-indexer/judge/packages.go | 55 ++++++++++---- modules/mesh-delivery-indexer/judge/rules.go | 4 +- sdk/packagename/packagename.go | 76 +++++++++++++++++++ 16 files changed, 361 insertions(+), 97 deletions(-) create mode 100644 sdk/packagename/packagename.go 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 0aa34c2e..fd0d494b 100644 --- a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/judge.go +++ b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/judge.go @@ -220,7 +220,7 @@ func newJudge(d judgeDeps) check.Judge { nodes = d.nodes } ex := &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, Published: d.published, Forge: d.forge, Runs: d.repos, Builds: d.builds, BuildEnds: d.ends, BuildWait: d.buildWait, Announce: d.announce, Trunks: d.trunks, Manifests: d.repos, Read: d.repos, Dirs: d.repos, Predefined: d.predefined, Tags: d.tags, Renames: d.renames, SeatDefinitions: d.seatDefs, Logf: d.logf} + Kept: check.Kept{Store: d.kept}, Previews: d.previews, Modules: d.repos, Packages: d.repos, Published: d.published, KnownPackages: d.published, Forge: d.forge, Runs: d.repos, Builds: d.builds, BuildEnds: d.ends, BuildWait: d.buildWait, Announce: d.announce, Trunks: d.trunks, Manifests: d.repos, Read: d.repos, Dirs: d.repos, Predefined: d.predefined, Tags: d.tags, Renames: d.renames, SeatDefinitions: d.seatDefs, Logf: d.logf} if d.nodes != nil && d.builds != nil && d.repos != nil { ex.Serving = runningServes{have: d.nodes, builds: d.builds, read: d.repos, logf: d.logf} } diff --git a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/judge_test.go b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/judge_test.go index f02d7ed3..8d89a5a6 100644 --- a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/judge_test.go +++ b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/judge_test.go @@ -146,6 +146,16 @@ func TestThePublishedVersionsAreTheIndexersPackagesState(t *testing.T) { if got := p.LatestPackages(); len(got) != 1 || got[sdk] == "" { t.Errorf("after a delete: %v", got) } + // A series key names its package by its ecosystem name, the judge's known packages (DLV-I14, design 32), and is no + // module's published version. + series, err := delivery.EncodeSeriesPackage(delivery.SeriesPackage{Name: sdk, Package: "mesh-sdk/go", Series: "0.1", At: time.Now().UTC()}) + if err != nil { + t.Fatal(err) + } + p.Put(delivery.SeriesPackageKey(sdk, "0.1"), series, false) + if got := p.PackageNames(); !reflect.DeepEqual(got, map[string]string{sdk: "mesh-sdk/go"}) || len(p.LatestPackages()) != 1 { + t.Errorf("the known packages = %v, the latest %v", got, p.LatestPackages()) + } t.Run("a pull request's unparsed seat definitions are its entries of the indexer's seat-definitions", func(t *testing.T) { state := map[string]string{ "web-api.trunk": `{"seat":"web-api","source":"trunk","digest":"d"}`, diff --git a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/packages.go b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/packages.go index 165397ec..9e8df54e 100644 --- a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/packages.go +++ b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/packages.go @@ -10,9 +10,14 @@ import ( // module, the latest published commit of its repository the indexer read its packages at. The indexer alone computes a // package's version; the checker reads it, so a Go check builds every mesh package at the version the mesh publishes // (check.Examiner.Published) and computes none. +// +// Its series keys (delivery.SeriesPackage, decoded with delivery.DecodeSeriesPackage) name each package by its name in its +// own ecosystem and the `/` it is built from (design 32): the known packages the judge takes +// (PackageNames). type publishedPackages struct { mu sync.Mutex byKey map[string]delivery.PublishedPackages + series map[string]delivery.SeriesPackage unread map[string]string } @@ -22,13 +27,23 @@ func (p *publishedPackages) Put(key string, raw []byte, deleted bool) { p.mu.Lock() defer p.mu.Unlock() if p.byKey == nil { - p.byKey, p.unread = map[string]delivery.PublishedPackages{}, map[string]string{} + p.byKey, p.series, p.unread = map[string]delivery.PublishedPackages{}, map[string]delivery.SeriesPackage{}, map[string]string{} } delete(p.byKey, key) + delete(p.series, key) delete(p.unread, key) if deleted { return } + if delivery.IsSeriesPackageKey(key) { + s, err := delivery.DecodeSeriesPackage(raw) + if err != nil { + p.unread[key] = err.Error() + return + } + p.series[key] = s + return + } v, err := delivery.DecodePublishedPackages(raw) if err != nil { p.unread[key] = err.Error() @@ -51,3 +66,17 @@ func (p *publishedPackages) LatestPackages() delivery.PackageVersions { } return out } + +// PackageNames are the packages the state's series keys name, by the name in its own ecosystem: the `/` +// each is built from (empty when the key does not say). Never nil, so the judge judges every name against it. +func (p *publishedPackages) PackageNames() map[string]string { + p.mu.Lock() + defer p.mu.Unlock() + out := map[string]string{} + for _, s := range p.series { + if out[s.Name] == "" { + out[s.Name] = s.Package + } + } + return out +} diff --git a/modules/mesh-delivery-checker/internal/check/examiner_test.go b/modules/mesh-delivery-checker/internal/check/examiner_test.go index 658d8945..f93feda3 100644 --- a/modules/mesh-delivery-checker/internal/check/examiner_test.go +++ b/modules/mesh-delivery-checker/internal/check/examiner_test.go @@ -21,6 +21,7 @@ import ( gobuild "git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep" "git.novox.be/novox/mesh-contracts/go/seats/meshconfig" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun" + "git.novox.be/novox/mesh-delivery/sdk/packagename" "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/composer" "git.novox.be/novox/mesh-sdk/go/delivery" @@ -1043,6 +1044,9 @@ func TestTheIndexersJudgeJudgesTheChangesManifests(t *testing.T) { renames, seat string // unparsed is the indexer's words of why the pull request's head's seat definition of web-api does not parse. unparsed string + // known are the judge's known packages the request hands it (DLV-I14, design 32): the packages state's names and, + // of web's package new in the group, the name its go.mod says at the member's head. + known []string }{ {name: "a seat definition that does not parse", unparsed: "seats/web-api/seat.json: unexpected end of JSON input", fails: "novox/mesh-delivery#7: the seat definition of web-api (module web-contracts, go) does not parse: seats/web-api/seat.json: unexpected end of JSON input"}, @@ -1057,6 +1061,7 @@ func TestTheIndexersJudgeJudgesTheChangesManifests(t *testing.T) { {name: "accepted"}, {name: "a package series raised", series: "0.2", seriesWords: "web/pkg series 0.1 → 0.2"}, {name: "a package series kept", series: "0.1"}, + {name: "a package new in the group", series: "0.1", known: []string{"git.novox.be/novox/mesh-sdk/go", "git.novox.be/novox/web/go"}}, {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"}, @@ -1104,6 +1109,12 @@ func TestTheIndexersJudgeJudgesTheChangesManifests(t *testing.T) { b.ex.Read = fixedManifests{strings.Repeat("a", 64): {Module: "mail"}, strings.Repeat("b", 64): {Module: "web", Build: pkg("")}, web: {Module: "web", Build: pkg(row.series)}} } + if row.known != nil { + b.ex.KnownPackages = knownNames{"git.novox.be/novox/mesh-sdk/go": "mesh-sdk/go"} + b.ex.Read = fixedManifests{strings.Repeat("a", 64): {Module: "mail"}, strings.Repeat("b", 64): {Module: "web"}, web: {Module: "web", + Build: &catalogue.Build{Artifacts: []catalogue.Artifact{{Name: "pkg", Kind: catalogue.ArtifactPackage, Language: "go", Paths: []string{"go"}}}}}} + b.ex.Forge = headFiles{repoA + " modules/web/go/go.mod " + head1: "module git.novox.be/novox/web/go\n\ngo 1.26\n"} + } if row.renames != "" { renamed := map[string]catalogue.SettingDeclaration{"new-key": {Type: catalogue.ConfigString, RenamedFrom: "old-key"}} plain := map[string]catalogue.SettingDeclaration{"new-key": {Type: catalogue.ConfigString}} @@ -1170,6 +1181,10 @@ func TestTheIndexersJudgeJudgesTheChangesManifests(t *testing.T) { req.Cases[0].Manifests["mail"].Digest != strings.Repeat("a", 64) { t.Fatalf("%s: the request %+v: %v", row.name, req, err) } + if in := (packagename.Input{}); row.known != nil && (json.Unmarshal(req.Input, &in) != nil || !reflect.DeepEqual(in.Packages, row.known)) || + row.known == nil && len(req.Input) != 0 && string(req.Input) != "null" { + t.Errorf("%s: the judge's input %s, want the known packages %v", row.name, req.Input, row.known) + } switch { case row.fails == "" && v.State != delivery.VerdictPass: t.Errorf("%s: %+v", row.name, v) @@ -1184,6 +1199,11 @@ func TestTheIndexersJudgeJudgesTheChangesManifests(t *testing.T) { } } +// knownNames are the packages state's names as a test gives them, each with the / it is built from. +type knownNames map[string]string + +func (k knownNames) PackageNames() map[string]string { return k } + // fakeRenames is the seat mesh-config's verb renames as its definition answers it. type fakeRenames struct { answer meshconfig.RenamesAnswer diff --git a/modules/mesh-delivery-checker/internal/check/judge.go b/modules/mesh-delivery-checker/internal/check/judge.go index 00a14638..ada57672 100644 --- a/modules/mesh-delivery-checker/internal/check/judge.go +++ b/modules/mesh-delivery-checker/internal/check/judge.go @@ -99,6 +99,12 @@ type Examiner struct { Published interface { LatestPackages() delivery.PackageVersions } + // KnownPackages names the packages the indexer's packages state knows (its series keys, delivery.SeriesPackage), by the + // name in its own ecosystem, each with the `/` it is built from: the names a manifest's `packages` is + // judged against (manifests_judge.go, design 32). Nil hands the judge no names, and it judges none. + KnownPackages interface { + PackageNames() map[string]string + } // Builds says how the build of a fingerprint the candidate carries stands (the builder's builds bucket); nil says nothing // of the builds. Builds BuildStates diff --git a/modules/mesh-delivery-checker/internal/check/manifests_judge.go b/modules/mesh-delivery-checker/internal/check/manifests_judge.go index c3114fd3..42bb640e 100644 --- a/modules/mesh-delivery-checker/internal/check/manifests_judge.go +++ b/modules/mesh-delivery-checker/internal/check/manifests_judge.go @@ -3,6 +3,7 @@ package check import ( "context" "encoding/json" + "errors" "fmt" "maps" "slices" @@ -12,6 +13,8 @@ import ( "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/composer" "git.novox.be/novox/mesh-sdk/go/delivery" + + "git.novox.be/novox/mesh-delivery/sdk/packagename" ) // A change's manifests are judged by the indexer's judge (novox/hq ADR 0578 §2): the checker asks the Go build seat to @@ -120,6 +123,13 @@ 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) } + if known := st.knownPackages(ctx); known != nil { + input, err := json.Marshal(packagename.Input{Packages: known}) + if err != nil { + return fail(goModule, err.Error()) + } + req.Input = input + } raw, err := json.Marshal(req) if err != nil { return fail(goModule, err.Error()) @@ -257,3 +267,59 @@ func (st *state) judgedByMember(p Pull, merged []Fetched) { } } } + +// knownPackages are the packages a manifest's `packages` may name, sorted (DLV-I14, design 32): every name the indexer's +// packages state holds, and, of each package a member of the case declares that the state does not know by its +// `/`, the name read at that member's head through the forge (packagename.Of, the indexer's own reader +// at a merge). A name that cannot be read is said and left out, so a manifest naming it is refused naming it. Nil, which +// judges no name, when the checker follows no packages state. +func (st *state) knownPackages(ctx context.Context) []string { + if st.j.KnownPackages == nil { + return nil + } + names := st.j.KnownPackages.PackageNames() + built := map[string]bool{} + for _, b := range names { + built[b] = true + } + known := slices.Collect(maps.Keys(names)) + declared, err := st.declaredPackages(ctx) + if err != nil { + st.c.note("the packages the group's members declare are not known, so only the packages state's names are known to the judge: " + err.Error()) + } + for _, d := range declared { + if built[d.module+"/"+d.name] { + continue + } + if st.j.Forge == nil { + st.c.note(fmt.Sprintf("the name of the package %s/%s new in the group is not read: this checker reads no forge", d.module, d.name)) + continue + } + read := func(ctx context.Context, repository, commit, file string) ([]byte, error) { + text, err := st.j.Forge.File(ctx, repository, file, commit) + if errors.Is(err, ErrNotFound) { + return nil, nil + } + return []byte(text), err + } + dirs := []string{d.dir} + if d.dir == "" { + dirs = []string{"modules/" + d.module, delivery.RootPath} + } + var why []string + for _, dir := range dirs { + a := catalogue.Artifact{Name: d.name, Kind: catalogue.ArtifactPackage, Language: d.language, Paths: d.paths} + name, err := packagename.Of(ctx, read, d.pull.Repository, d.pull.Head, dir, a) + if err == nil { + known, why = append(known, name), nil + break + } + why = append(why, err.Error()) + } + if len(why) > 0 { + st.c.note(fmt.Sprintf("the name of the package %s/%s new in the group is not read at the head of %s: %s", d.module, d.name, pullKey(d.pull), strings.Join(why, "; "))) + } + } + slices.Sort(known) + return slices.Compact(known) +} diff --git a/modules/mesh-delivery-indexer/internal/index/candidates.go b/modules/mesh-delivery-indexer/internal/index/candidates.go index 2ba2e085..53ce5221 100644 --- a/modules/mesh-delivery-indexer/internal/index/candidates.go +++ b/modules/mesh-delivery-indexer/internal/index/candidates.go @@ -185,6 +185,11 @@ func (ix *Indexer) ApplyMessage(ctx context.Context, f delivery.Forge) (Applied, if err := ix.configCandidatesAfterMerge(ctx, f, writtenModules(w.written)); err != nil { return applied, err } + // Each package's name in its own ecosystem, which the builder numbers it under (design 32), kept before the + // manifests are judged: a manifest of this merge naming a package this merge brings is judged knowing its name. + if err := ix.KeepPackageNames(ctx, f); err != nil { + return applied, err + } // Every manifest judged again: a rule between manifests can turn on the one this merge moved (ADR 0578 §1). // What this take-in newly wrote is fresh: judged in full (ADR 0579 §1). if _, err := ix.JudgeAll(ctx, w.fresh); err != nil { @@ -199,10 +204,6 @@ func (ix *Indexer) ApplyMessage(ctx context.Context, f delivery.Forge) (Applied, if err := ix.ApplySeatDefinitions(ctx, f); err != nil { return applied, err } - // Each package's name in its own ecosystem, which the builder numbers it under (design 32). - if err := ix.KeepPackageNames(ctx, f); err != nil { - return applied, err - } } var digests map[string]string var held map[string]delivery.Module diff --git a/modules/mesh-delivery-indexer/internal/index/control_records_test.go b/modules/mesh-delivery-indexer/internal/index/control_records_test.go index 61da5616..5e1a4809 100644 --- a/modules/mesh-delivery-indexer/internal/index/control_records_test.go +++ b/modules/mesh-delivery-indexer/internal/index/control_records_test.go @@ -317,6 +317,10 @@ func TestTheControlRecords(t *testing.T) { ts.Name != "@novox/mesh-sdk" || ts.Package != "mesh-sdk/ts" || ts.GoModule != "" || len(named) != 2 { t.Errorf("names at the merge = %+v", named) } + // The judge's known packages (DLV-I14) are those names, never the builder's /. + if known := ix.knownPackages(ctx); len(known) != 2 || !known[sdkGo] || !known["@novox/mesh-sdk"] { + t.Errorf("the judge's known packages = %v", known) + } delete(ix.Packages.(*memStore).vals, SeriesKey("@novox/mesh-sdk", "0.3")) for _, h := range []HeardVersion{ {Package: sdkGo, Seat: "go-package-registry", Version: "0.1.15", Commit: "c15"}, diff --git a/modules/mesh-delivery-indexer/internal/index/series.go b/modules/mesh-delivery-indexer/internal/index/series.go index b18dca58..493e778d 100644 --- a/modules/mesh-delivery-indexer/internal/index/series.go +++ b/modules/mesh-delivery-indexer/internal/index/series.go @@ -6,12 +6,13 @@ import ( "errors" "fmt" "maps" - "path" "slices" "strings" "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/delivery" + + "git.novox.be/novox/mesh-delivery/sdk/packagename" ) // The packages state per series (DLV-I5; milestone § 2 item 1; design 51 *The versions*, ADR 0606 §1, ADR 0609 §1): beside @@ -200,7 +201,7 @@ func (ix *Indexer) KeepPackageNames(ctx context.Context, f delivery.Forge) error if a.Kind != catalogue.ArtifactPackage { continue } - name, err := ix.packageName(ctx, f.Repository, f.Merge, dir, a) + name, err := packagename.Of(ctx, packagename.ReadFile(ix.ReadFile), f.Repository, f.Merge, dir, a) if err != nil { ix.logf("packages: the name of %s/%s at %.12s is not read, so it is numbered under no name until the next merge: %v", m.Module, a.Name, f.Merge, err) continue @@ -226,51 +227,3 @@ func (ix *Indexer) KeepPackageNames(ctx context.Context, f delivery.Forge) error } return errors.Join(errs...) } - -// packageName is a package artifact's name in its ecosystem, read at commit from the first of its paths (or the module's -// directory) that holds its go.mod (Go) or package.json (TypeScript). -func (ix *Indexer) packageName(ctx context.Context, repository, commit, dir string, a catalogue.Artifact) (string, error) { - file := "package.json" - if a.Language == "go" { - file = "go.mod" - } - base := dir - if base == delivery.RootPath { - base = "" - } - roots := a.Paths - if len(roots) == 0 { - roots = []string{"."} - } - for _, r := range roots { - p := path.Join(base, r, file) - if strings.HasSuffix(r, file) { - p = path.Join(base, r) - } - raw, err := ix.ReadFile(ctx, repository, commit, p) - if err != nil { - return "", fmt.Errorf("%s: %w", p, err) - } - if raw == nil { - continue - } - if a.Language == "go" { - for _, line := range strings.Split(string(raw), "\n") { - if rest, ok := strings.CutPrefix(strings.TrimSpace(line), "module "); ok { - if name := strings.Trim(strings.TrimSpace(rest), `"`); name != "" { - return name, nil - } - } - } - return "", fmt.Errorf("%s names no module", p) - } - var pj struct { - Name string `json:"name"` - } - if err := json.Unmarshal(raw, &pj); err != nil || pj.Name == "" { - return "", fmt.Errorf("%s names no name (%v)", p, err) - } - return pj.Name, nil - } - return "", fmt.Errorf("no %s under %v", file, roots) -} diff --git a/modules/mesh-delivery-indexer/internal/index/verdicts.go b/modules/mesh-delivery-indexer/internal/index/verdicts.go index 83f84a7c..88d9a58a 100644 --- a/modules/mesh-delivery-indexer/internal/index/verdicts.go +++ b/modules/mesh-delivery-indexer/internal/index/verdicts.go @@ -114,7 +114,7 @@ func (ix *Indexer) judgeOver(ctx context.Context, fresh map[string]bool, over ma if ix.OldSections != nil { counts = ix.heldOldSections(ctx) } - refused, waits, unnamed := judge.ShelfWaiting(raws, predefined, 0, counts) + refused, waits, unnamed := judge.ShelfWaiting(raws, predefined, 0, counts, ix.knownPackages(ctx)) // A change's renamed-from is judged against the trunk's manifest of the same module (DLV-I15, design 32). for module, digest := range trunkOf { if _, waiting := waits[module]; waiting || raws[module] == nil { @@ -306,6 +306,35 @@ func (ix *Indexer) judgeStale(ctx context.Context) { } // kickJudge judges again a start pass that failed, when the indexer hears something: never on a timer. +// knownPackages are the packages a manifest's `packages` may name (DLV-I14, design 32): every name the packages state's +// series keys hold, as KeepPackageNames reads it at each merge. Nil, which judges no name, where the indexer keeps no +// packages state or it cannot be listed (said): a state the judge cannot read refuses no manifest. +func (ix *Indexer) knownPackages(ctx context.Context) judge.KnownPackages { + if ix.Packages == nil { + return nil + } + keys, err := ix.Packages.Keys(ctx) + if err != nil { + ix.logf("judge: the packages state cannot be listed, so the names of packages are not judged: %v", err) + return nil + } + known := judge.KnownPackages{} + for _, key := range keys { + if !IsSeriesKey(key) { + continue + } + value, _, found, err := ix.Packages.Get(ctx, key) + if err != nil { + ix.logf("judge: packages.%s cannot be read, so the names of packages are not judged: %v", key, err) + return nil + } + if s, err := delivery.DecodeSeriesPackage(value); found && err == nil { + known[s.Name] = true + } + } + return known +} + func (ix *Indexer) kickJudge(ctx context.Context) { if !ix.judgePending.Load() { return diff --git a/modules/mesh-delivery-indexer/judge/busstate_test.go b/modules/mesh-delivery-indexer/judge/busstate_test.go index b9817b32..ad7d16b7 100644 --- a/modules/mesh-delivery-indexer/judge/busstate_test.go +++ b/modules/mesh-delivery-indexer/judge/busstate_test.go @@ -1,10 +1,14 @@ package judge import ( + "encoding/json" + "maps" + "slices" "strings" "testing" "git.novox.be/novox/mesh-sdk/go/catalogue" + "git.novox.be/novox/mesh-sdk/go/composer" ) // A manifest's bus state, asked for in `requires` by template, is judged once and each refusal names the module, the entry @@ -88,6 +92,8 @@ func TestBusStateIsJudgedOnce(t *testing.T) { // Between the manifests: two modules' names on the bus alike, and a read of bus state that moved (ADR 0596 §2). sdk := catalogue.Manifest{Module: "mesh-sdk", Build: &catalogue.Build{Artifacts: []catalogue.Artifact{ {Name: "go", Kind: catalogue.ArtifactPackage, Language: "go"}, {Name: "img", Kind: catalogue.ArtifactImage}}}} + sdkGo := "git.novox.be/novox/mesh-sdk/go" + known := KnownPackages{sdkGo: true} entry := func(name string) []catalogue.BusStateEntry { return []catalogue.BusStateEntry{{Name: name, Storage: catalogue.StorageKeyValue, Template: catalogue.TemplateState}} } @@ -101,18 +107,51 @@ func TestBusStateIsJudgedOnce(t *testing.T) { "reader": {Module: "reader", Reads: []string{"owner.cache"}}}, "reader reads \"owner.cache\", which owner has moved"}, {"a read of state still kept", catalogue.Shelf{"owner": {Module: "owner", State: []catalogue.StateDeclaration{{Name: "cache"}}}, "reader": {Module: "reader", Reads: []string{"owner.cache"}}}, ""}, - // DLV-I14 (design 32): the packages section names a declared package, by the builder's name, in a well-formed series. - {"packages naming a declared package", catalogue.Shelf{"mesh-sdk": sdk, "user": {Module: "user", Packages: map[string]string{"mesh-sdk/go": "0.2"}}}, ""}, - {"packages naming no declared package", catalogue.Shelf{"mesh-sdk": sdk, "user": {Module: "user", Packages: map[string]string{"mesh-sdk": "0.1"}}}, - `user's packages name "mesh-sdk", which no manifest declares`}, - {"packages naming an image", catalogue.Shelf{"mesh-sdk": sdk, "user": {Module: "user", Packages: map[string]string{"mesh-sdk/img": "0.1"}}}, - `name "mesh-sdk/img", which no manifest declares`}, - {"packages in an ill-formed series", catalogue.Shelf{"mesh-sdk": sdk, "user": {Module: "user", Packages: map[string]string{"mesh-sdk/go": "v0.2"}}}, - `user's packages accept "mesh-sdk/go" in the series "v0.2"`}, + // DLV-I14 (design 32): the packages section names a known package, by its name in its own language (the packages + // state's names, and a group member's at its head: known), in a well-formed series; through a judge request too. + {"packages naming a known package", catalogue.Shelf{"mesh-sdk": sdk, "user": {Module: "user", Packages: map[string]string{sdkGo: "0.2"}}}, ""}, + {"packages naming a known package, by request", catalogue.Shelf{"user": {Module: "user", Packages: map[string]string{sdkGo: "0.2"}}}, ""}, + {"packages naming a package by the builder's name", catalogue.Shelf{"mesh-sdk": sdk, "user": {Module: "user", Packages: map[string]string{"mesh-sdk/go": "0.1"}}}, + `user's packages name "mesh-sdk/go", which no package of the mesh is named`}, + {"packages naming an unknown package, by request", catalogue.Shelf{"user": {Module: "user", Packages: map[string]string{"@novox/ui": "0.1"}}}, + `user's packages name "@novox/ui", which no package of the mesh is named`}, + {"packages in an ill-formed series", catalogue.Shelf{"mesh-sdk": sdk, "user": {Module: "user", Packages: map[string]string{sdkGo: "v0.2"}}}, + `user's packages accept "` + sdkGo + `" in the series "v0.2"`}, } { - got := strings.Join(append(BusStateCatalogueProblems(c.shelf), PackagesCatalogueProblems(c.shelf)...), "\n") + got := strings.Join(append(BusStateCatalogueProblems(c.shelf), PackagesCatalogueProblems(c.shelf, known)...), "\n") + if strings.Contains(c.name, "by request") { + got = judgedByRequest(t, c.shelf, known) + } if (c.want == "") != (got == "") || !strings.Contains(got, c.want) { t.Errorf("%s: between the manifests: %q, want %q", c.name, got, c.want) } } } + +// judgedByRequest is every refusal the judge's request (mesh-judge's) answers of shelf's manifests, its Input naming known. +func judgedByRequest(t *testing.T, shelf catalogue.Shelf, known KnownPackages) string { + t.Helper() + input, err := json.Marshal(Input{Packages: slices.Sorted(maps.Keys(known))}) + if err != nil { + t.Fatal(err) + } + c := composer.Case{Name: "change", Manifests: map[string]composer.Manifest{}} + for name, m := range shelf { + raw, err := json.Marshal(m) + if err != nil { + t.Fatal(err) + } + c.Manifests[name] = composer.Manifest{Raw: raw} + } + v, err := Request(composer.Request{V: composer.Version, Input: input, Cases: []composer.Case{c}}) + if err != nil { + t.Fatal(err) + } + var refused []string + for _, name := range slices.Sorted(maps.Keys(v.Cases[0].Manifests)) { + if r := v.Cases[0].Manifests[name].Refused; r != "" { + refused = append(refused, r) + } + } + return strings.Join(refused, "\n") +} diff --git a/modules/mesh-delivery-indexer/judge/judge.go b/modules/mesh-delivery-indexer/judge/judge.go index b41c6ba0..83b8bb90 100644 --- a/modules/mesh-delivery-indexer/judge/judge.go +++ b/modules/mesh-delivery-indexer/judge/judge.go @@ -69,15 +69,16 @@ func problemsOf(err error) []string { // whose removal from the catalogue ends it (a rule two modules break together refuses both); one no removal ends is in // unnamed, for the caller to say. A module not in the answer is accepted. func Shelf(raws map[string][]byte, predefined catalogue.PredefinedList, longestMachine int) (refused map[string][]string, unnamed []string) { - return ShelfCounting(raws, predefined, longestMachine, nil) + return ShelfCounting(raws, predefined, longestMachine, nil, nil) } // ShelfCounting is Shelf also refusing a manifest of a module outside the foundation that uses an old section whose count // is 0 (OldSectionProblems; issue 1966 D4); nil counts refuse none. A field this judge does not know is refused, strictly, as // the checker judges a change: the judge that runs there is the one built at the change's head, which knows the fields the -// change brings, so a field it does not know is a mistake. -func ShelfCounting(raws map[string][]byte, predefined catalogue.PredefinedList, longestMachine int, counts OldSectionCounts) (refused map[string][]string, unnamed []string) { - refused, waits, unnamed := ShelfWaiting(raws, predefined, longestMachine, counts) +// change brings, so a field it does not know is a mistake. Known are the packages a manifest's `packages` may name +// (KnownPackages; nil judges no name). +func ShelfCounting(raws map[string][]byte, predefined catalogue.PredefinedList, longestMachine int, counts OldSectionCounts, known KnownPackages) (refused map[string][]string, unnamed []string) { + refused, waits, unnamed := ShelfWaiting(raws, predefined, longestMachine, counts, known) for name := range waits { _, refused[name] = Manifest(raws[name], predefined) } @@ -88,7 +89,7 @@ func ShelfCounting(raws map[string][]byte, predefined catalogue.PredefinedList, // refused: it is in waits, by module, naming each field, and judged no further, because a newer judge may know the field // (novox/hq ADR 0578 §1, amended). The indexer judges so; the checker, which runs the judge built at the head of the change, // judges with Shelf. -func ShelfWaiting(raws map[string][]byte, predefined catalogue.PredefinedList, longestMachine int, counts OldSectionCounts) (refused, waits map[string][]string, unnamed []string) { +func ShelfWaiting(raws map[string][]byte, predefined catalogue.PredefinedList, longestMachine int, counts OldSectionCounts, known KnownPackages) (refused, waits map[string][]string, unnamed []string) { refused, waits = map[string][]string{}, map[string][]string{} shelf := catalogue.Shelf{} for name, raw := range raws { @@ -111,14 +112,14 @@ func ShelfWaiting(raws map[string][]byte, predefined catalogue.PredefinedList, l } shelf[name] = m } - between := Between(shelf, longestMachine) + between := Between(shelf, longestMachine, known) without := map[string]map[string]bool{} // by module: the problems between the rest, once asked ends := func(module, problem string) bool { if without[module] == nil { rest := maps.Clone(shelf) delete(rest, module) without[module] = map[string]bool{} - for _, p := range Between(rest, longestMachine) { + for _, p := range Between(rest, longestMachine, known) { without[module][p] = true } } @@ -154,12 +155,14 @@ func ShelfWaiting(raws map[string][]byte, predefined catalogue.PredefinedList, l } // Between is every problem between the manifests of shelf, sorted: catalogue.CatalogueProblems, IdentityProblems over -// the longest machine name (catalogue.DefaultLongestMachine when not above 0) and DataProblems. -func Between(shelf catalogue.Shelf, longestMachine int) []string { +// the longest machine name (catalogue.DefaultLongestMachine when not above 0), DataProblems, and +// PackagesCatalogueProblems against the known packages (nil judges no name). +func Between(shelf catalogue.Shelf, longestMachine int, known KnownPackages) []string { if longestMachine <= 0 { longestMachine = catalogue.DefaultLongestMachine } between := CatalogueProblems(shelf) + between = append(between, PackagesCatalogueProblems(shelf, known)...) between = append(between, IdentityProblems(shelf, longestMachine)...) between = append(between, DataProblems(shelf)...) sort.Strings(between) @@ -202,12 +205,17 @@ const Unnamed = "(between manifests)" // Request judges every case of a composer request (mesh-sdk composer.Request), what cmd/mesh-judge answers the checker: // each case's manifests as one catalogue (Shelf), every manifest accepted or refused with its problems, one per line; the -// problems no module's removal ends under Between. The verdict's SDK names this judge (Identity). A manifest left as a -// digest, or a case named twice, is a request it cannot run. +// problems no module's removal ends under Between. The request's Input is the judge's (Input): the known packages every +// case's `packages` are judged against. The verdict's SDK names this judge (Identity). A manifest left as a digest, a case +// named twice, or an Input not of its shape is a request it cannot run. func Request(r composer.Request) (composer.Verdict, error) { v := composer.Verdict{V: composer.Version, SDK: Identity()} seen := map[string]bool{} counts := trunkOldSections(r) + known, err := inputOf(r.Input) + if err != nil { + return composer.Verdict{}, err + } for _, c := range r.Cases { if seen[c.Name] { return composer.Verdict{}, fmt.Errorf("the case %q is named twice", c.Name) @@ -220,7 +228,7 @@ func Request(r composer.Request) (composer.Verdict, error) { } raws[name] = m.Raw } - refused, unnamed := ShelfCounting(raws, r.Predefined, 0, counts) + refused, unnamed := ShelfCounting(raws, r.Predefined, 0, counts, known) cv := composer.CaseVerdict{Name: c.Name, Manifests: map[string]composer.ManifestRead{}} for name := range raws { if problems := refused[name]; len(problems) > 0 { diff --git a/modules/mesh-delivery-indexer/judge/judge_test.go b/modules/mesh-delivery-indexer/judge/judge_test.go index 7732c11c..92b9e9a9 100644 --- a/modules/mesh-delivery-indexer/judge/judge_test.go +++ b/modules/mesh-delivery-indexer/judge/judge_test.go @@ -69,7 +69,7 @@ func TestTheJudge(t *testing.T) { "bad": []byte(`{"module":"bad","version":"1","provides":[{"name":"amqp"}]}`), "ok": []byte(`{"module":"ok","version":"1"}`), } - refused, waits, _ := ShelfWaiting(waitRaws, nil, 0, nil) + refused, waits, _ := ShelfWaiting(waitRaws, nil, 0, nil, nil) if !reflect.DeepEqual(waits, map[string][]string{"later": {"later-field", "other"}}) || len(refused) != 2 || refused["renamed"] == nil || refused["bad"] == nil { t.Errorf("ShelfWaiting: waits %v, refused %v", waits, refused) } diff --git a/modules/mesh-delivery-indexer/judge/packages.go b/modules/mesh-delivery-indexer/judge/packages.go index 61dabb15..307c9133 100644 --- a/modules/mesh-delivery-indexer/judge/packages.go +++ b/modules/mesh-delivery-indexer/judge/packages.go @@ -1,35 +1,58 @@ package judge import ( + "encoding/json" "fmt" "maps" "slices" "strings" "git.novox.be/novox/mesh-sdk/go/catalogue" + + "git.novox.be/novox/mesh-delivery/sdk/packagename" ) -// PackagesCatalogueProblems are the rules of a manifest's `packages` no single manifest can be judged against (DLV-I14; -// ADR 0606 §4, design 32 *A package, its series, and the series its users accept*): each name is a package some manifest -// of the shelf declares, named as the builder names it (`/`), and each series is `.`. -func PackagesCatalogueProblems(shelf catalogue.Shelf) []string { - declared := map[string]bool{} - for _, module := range shelfOrder(shelf) { - if b := shelf[module].Build; b != nil { - for _, a := range b.Artifacts { - if a.Kind == catalogue.ArtifactPackage { - declared[module+"/"+a.Name] = true - } - } - } +// KnownPackages are the names of the packages the mesh knows, each by its name in its own ecosystem (design 32: a Go +// package's go.mod module path, a TypeScript package's package.json name): the names the indexer's packages state holds +// (its series keys, delivery.SeriesPackage's Name), read at each trunk merge, and, at the merge check, the names a member +// of the change's delivery group names at its head for a package new in the group. Both read a name with mesh-delivery's +// sdk/packagename. Nil is no list at hand: the names are then not judged, only their series. +type KnownPackages map[string]bool + +// Input is the judge's own input in a judge request (mesh-delivery's sdk/packagename.Input, which the checker hands it). +type Input = packagename.Input + +// inputOf reads the known packages a request's Input names; nil when it names none (no list, not an empty one). +func inputOf(raw json.RawMessage) (KnownPackages, error) { + if len(raw) == 0 || string(raw) == "null" { + return nil, nil } + var in Input + if err := json.Unmarshal(raw, &in); err != nil { + return nil, fmt.Errorf("the judge's input is not JSON of its shape: %w", err) + } + if in.Packages == nil { + return nil, nil + } + known := KnownPackages{} + for _, name := range in.Packages { + known[name] = true + } + return known, nil +} + +// PackagesCatalogueProblems are the rules of a manifest's `packages` no single manifest can be judged against (DLV-I14; +// ADR 0606 §4, design 32 *A package, its series, and the series its users accept*): each name is a known package's name in +// its own ecosystem (known; not judged when nil), and each series is `.`. +func PackagesCatalogueProblems(shelf catalogue.Shelf, known KnownPackages) []string { var problems []string for _, module := range shelfOrder(shelf) { accepts := shelf[module].Packages for _, name := range slices.Sorted(maps.Keys(accepts)) { - if !declared[name] { - problems = append(problems, fmt.Sprintf("%s's packages name %q, which no manifest declares as a package "+ - "(a package is named /; design 32)", module, name)) + if known != nil && !known[name] { + problems = append(problems, fmt.Sprintf("%s's packages name %q, which no package of the mesh is named "+ + "(a package is named in its own language: a Go package by its go.mod's module path, a TypeScript "+ + "package by its package.json's name; design 32)", module, name)) } if s := strings.TrimSpace(accepts[name]); !seriesForm.MatchString(s) { problems = append(problems, fmt.Sprintf("%s's packages accept %q in the series %q, which is no . "+ diff --git a/modules/mesh-delivery-indexer/judge/rules.go b/modules/mesh-delivery-indexer/judge/rules.go index 87520fc2..1a981af2 100644 --- a/modules/mesh-delivery-indexer/judge/rules.go +++ b/modules/mesh-delivery-indexer/judge/rules.go @@ -4717,8 +4717,8 @@ func CatalogueProblems(shelf catalogue.Shelf) []string { problems = append(problems, ObjectsCatalogueProblems(shelf)...) // Bus state in `requires`: a name on the bus has one owner, and the new form has no readers (ADR 0595, ADR 0596 §2). problems = append(problems, BusStateCatalogueProblems(shelf)...) - // A manifest's packages name a declared package in a well-formed series (design 32, DLV-I14). - problems = append(problems, PackagesCatalogueProblems(shelf)...) + // A manifest's packages name a known package in a well-formed series (design 32, DLV-I14): Between, which is handed the + // known names. // A module that hears a restricted stream runs as an account of its own (novox/hq ADR 0376 §2). problems = append(problems, RestrictedHearingProblems(shelf)...) // A work-queue stream is never asked for by another module's consumes (novox/hq ADR 0436 §6). diff --git a/sdk/packagename/packagename.go b/sdk/packagename/packagename.go new file mode 100644 index 00000000..51b34986 --- /dev/null +++ b/sdk/packagename/packagename.go @@ -0,0 +1,76 @@ +// Package packagename reads a package artifact's name in its own ecosystem (design 32, a package named by its name in its +// own language): a Go package by the module line of its go.mod, a TypeScript package by the name in its package.json, read +// at a commit of the repository that builds it. The indexer reads it so at each trunk merge (its packages state's series +// keys), and the checker so at a delivery group member's head for a package new in the group, so the judge is handed one +// name for each package whoever reads it. +package packagename + +import ( + "context" + "encoding/json" + "fmt" + "path" + "strings" + + "git.novox.be/novox/mesh-sdk/go/catalogue" + "git.novox.be/novox/mesh-sdk/go/delivery" +) + +// ReadFile reads one file of a repository at a commit; a file the commit does not hold is nil with no error. +type ReadFile func(ctx context.Context, repository, commit, file string) ([]byte, error) + +// Of is a package artifact's name in its ecosystem, read at commit from the first of its paths (or the module's directory +// dir, delivery.RootPath or empty for the repository's root) that holds its go.mod (Go) or package.json (TypeScript). A +// file read that fails, a go.mod that names no module and a package.json that names no name are errors, said. +func Of(ctx context.Context, read ReadFile, repository, commit, dir string, a catalogue.Artifact) (string, error) { + file := "package.json" + if a.Language == "go" { + file = "go.mod" + } + base := dir + if base == delivery.RootPath { + base = "" + } + roots := a.Paths + if len(roots) == 0 { + roots = []string{"."} + } + for _, r := range roots { + p := path.Join(base, r, file) + if strings.HasSuffix(r, file) { + p = path.Join(base, r) + } + raw, err := read(ctx, repository, commit, p) + if err != nil { + return "", fmt.Errorf("%s: %w", p, err) + } + if raw == nil { + continue + } + if a.Language == "go" { + for _, line := range strings.Split(string(raw), "\n") { + if rest, ok := strings.CutPrefix(strings.TrimSpace(line), "module "); ok { + if name := strings.Trim(strings.TrimSpace(rest), `"`); name != "" { + return name, nil + } + } + } + return "", fmt.Errorf("%s names no module", p) + } + var pj struct { + Name string `json:"name"` + } + if err := json.Unmarshal(raw, &pj); err != nil || pj.Name == "" { + return "", fmt.Errorf("%s names no name (%v)", p, err) + } + return pj.Name, nil + } + return "", fmt.Errorf("no %s under %v", file, roots) +} + +// Input is the indexer's judge's own input in a judge request (mesh-sdk composer.Request's Input, which the composer reads +// as the composition's input and the judge as this), as the checker hands it: the known packages, each by its name in its +// own ecosystem, that a manifest's `packages` may name (DLV-I14). A request without one judges no name. +type Input struct { + Packages []string `json:"packages"` +}