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 <module>/<artifact>. 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
This commit is contained in:
@@ -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}
|
||||
}
|
||||
|
||||
@@ -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"}`,
|
||||
|
||||
@@ -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 `<module>/<artifact>` 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 `<module>/<artifact>`
|
||||
// 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
|
||||
}
|
||||
|
||||
@@ -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 <module>/<artifact> 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
|
||||
|
||||
@@ -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 `<module>/<artifact>` 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
|
||||
|
||||
@@ -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
|
||||
// `<module>/<artifact>`, 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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 <module>/<artifact>.
|
||||
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"},
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 (`<module>/<artifact>`), and each series is `<major>.<minor>`.
|
||||
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 `<major>.<minor>`.
|
||||
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 <module>/<artifact>; 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 <major>.<minor> "+
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
Reference in New Issue
Block a user