diff --git a/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer-tools/views.go b/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer-tools/views.go index 4259a2e3..2568de88 100644 --- a/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer-tools/views.go +++ b/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer-tools/views.go @@ -136,7 +136,7 @@ func packageUsersTool(state func(string) views.Source) stdio.Tool { "packages, or the default), that series' newest published version and the version its fingerprint is pinned to, so a " + "user left on an older series is seen. Give package (such as mesh-sdk/go) for one.", Input: map[string]any{"type": "object", "properties": map[string]any{ - "package": map[string]any{"type": "string", "description": "one package by the builder's name, /; all when absent"}}}, + "package": map[string]any{"type": "string", "description": "one package by its name in its own ecosystem (a Go module path, an npm package name); all when absent"}}}, Run: func(args map[string]any) (any, error) { only, _ := args["package"].(string) got, err := index.PackageUsersOf(state("modules"), state("manifests"), state(delivery.PackagesState), only) diff --git a/modules/mesh-delivery-indexer/internal/index/candidates.go b/modules/mesh-delivery-indexer/internal/index/candidates.go index 5ad825a6..123fa52c 100644 --- a/modules/mesh-delivery-indexer/internal/index/candidates.go +++ b/modules/mesh-delivery-indexer/internal/index/candidates.go @@ -199,6 +199,10 @@ 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 d0f73bf7..61da5616 100644 --- a/modules/mesh-delivery-indexer/internal/index/control_records_test.go +++ b/modules/mesh-delivery-indexer/internal/index/control_records_test.go @@ -296,24 +296,44 @@ func TestTheControlRecords(t *testing.T) { }) t.Run("packages per series", func(t *testing.T) { ctx := context.Background() - ix := &Indexer{Packages: newMem()} sdkGo := "git.novox.be/novox/mesh-sdk/go" + // The lead's design limit (design 32): a merge names each package by its name in its own ecosystem, read at the + // merge commit: the Go package by its go.mod's module path, the TypeScript one by its package.json name. + files := map[string]string{"go/go.mod": "module " + sdkGo + "\n\ngo 1.26\n", "package.json": `{"name":"@novox/mesh-sdk","version":"0.0.0"}`} + ix := &Indexer{Packages: newMem(), Logf: t.Logf, ReadFile: func(_ context.Context, r, sha, p string) ([]byte, error) { + if v, ok := files[p]; ok && r == "novox/mesh-sdk" && sha == "m1" { + return []byte(v), nil + } + return nil, nil + }} + sdk := `{"module":"mesh-sdk","version":"1","build":{"artifacts":[{"name":"go","kind":"package","language":"go","paths":["go"]},` + + `{"name":"ts","kind":"package","language":"typescript","paths":["src","package.json"],"series":"0.3"}]}}` + if err := ix.KeepPackageNames(ctx, delivery.Forge{Kind: delivery.ForgeMerged, Repository: "novox/mesh-sdk", Merge: "m1", + Manifests: map[string]string{delivery.RootPath: sdk}}); err != nil { + t.Fatal(err) + } + named := ix.SeriesAll(ctx) + if g, ts := named[SeriesKey(sdkGo, "0.1")], named[SeriesKey("@novox/mesh-sdk", "0.3")]; g.Name != sdkGo || g.Package != "mesh-sdk/go" || g.GoModule != sdkGo || + ts.Name != "@novox/mesh-sdk" || ts.Package != "mesh-sdk/ts" || ts.GoModule != "" || len(named) != 2 { + t.Errorf("names at the merge = %+v", named) + } + delete(ix.Packages.(*memStore).vals, SeriesKey("@novox/mesh-sdk", "0.3")) for _, h := range []HeardVersion{ - {Package: "mesh-sdk/go", Seat: "go-package-registry", Version: "0.1.15", Commit: "c15"}, - {Package: "mesh-sdk/go", Seat: "go-package-registry", Version: "0.1.15", Commit: "c15", Digest: "h1:a", GoModule: sdkGo, Published: true}, - {Package: "mesh-sdk/go", Version: "0.1.16", Commit: "c16"}, - {Package: "mesh-sdk/go", Version: "0.1.14", Commit: "c14", Published: true}, - {Package: "mesh-sdk/go", Version: "0.1.17-feat-x.1", Tag: "feat-x", Published: true}, - {Package: "mesh-sdk/go", Version: "0.2.0", Commit: "c20", Published: true}, + {Package: sdkGo, Seat: "go-package-registry", Version: "0.1.15", Commit: "c15"}, + {Package: sdkGo, Seat: "go-package-registry", Version: "0.1.15", Commit: "c15", Digest: "h1:a", GoModule: sdkGo, Published: true}, + {Package: sdkGo, Version: "0.1.16", Commit: "c16"}, + {Package: sdkGo, Version: "0.1.14", Commit: "c14", Published: true}, + {Package: sdkGo, Version: "0.1.17-feat-x.1", Tag: "feat-x", Published: true}, + {Package: sdkGo, Version: "0.2.0", Commit: "c20", Published: true}, } { if _, err := ix.HearVersion(ctx, h); err != nil { t.Fatal(err) } } all := ix.SeriesAll(ctx) - one, two := all[SeriesKey("mesh-sdk/go", "0.1")], all[SeriesKey("mesh-sdk/go", "0.2")] + one, two := all[SeriesKey(sdkGo, "0.1")], all[SeriesKey(sdkGo, "0.2")] if one.Newest == nil || one.Newest.Version != "0.1.15" || one.Newest.Digest != "h1:a" || one.Numbered == nil || one.Numbered.Version != "0.1.16" || - one.GoModule != sdkGo || one.Seat != "go-package-registry" || len(all) != 2 || two.Newest == nil || two.Newest.Version != "0.2.0" { + one.GoModule != sdkGo || one.Name != sdkGo || one.Package != "mesh-sdk/go" || one.Seat != "go-package-registry" || len(all) != 2 || two.Newest == nil || two.Newest.Version != "0.2.0" { t.Errorf("series = %+v", all) } if got := ix.publishedAll(ctx); len(got) != 0 { @@ -334,15 +354,15 @@ func TestTheControlRecords(t *testing.T) { } s[key] = &views.Entry{Value: raw, Revision: 1} } - putJSON(packages, SeriesKey("mesh-sdk/go", "0.1"), SeriesPackage{V: 1, Package: "mesh-sdk/go", Series: "0.1", GoModule: sdkGo, + putJSON(packages, SeriesKey(sdkGo, "0.1"), SeriesPackage{V: 1, Name: sdkGo, Package: "mesh-sdk/go", Series: "0.1", GoModule: sdkGo, Newest: &SeriesVersion{Version: "0.1.15"}}) - putJSON(packages, SeriesKey("mesh-sdk/go", "0.2"), SeriesPackage{V: 1, Package: "mesh-sdk/go", Series: "0.2", GoModule: sdkGo}) + putJSON(packages, SeriesKey(sdkGo, "0.2"), SeriesPackage{V: 1, Name: sdkGo, Package: "mesh-sdk/go", Series: "0.2", GoModule: sdkGo}) for name, u := range map[string]struct { repo, manifest string pkgs, imports []string pinned string }{ - "new": {"novox/mesh-catalog", `{"module":"new","version":"1","packages":{"mesh-sdk/go":"0.2","web/ui":"1.0"}}`, nil, nil, ""}, + "new": {"novox/mesh-catalog", `{"module":"new","version":"1","packages":{"git.novox.be/novox/mesh-sdk/go":"0.2","@novox/ui":"1.0"}}`, nil, nil, ""}, "old": {"novox/mesh-catalog", `{"module":"old","version":"1"}`, []string{sdkGo}, nil, "v0.1.15"}, "imports": {"novox/mesh-tools", `{"module":"imports","version":"1"}`, nil, []string{sdkGo + "/views"}, ""}, "sibling": {"novox/mesh-sdk", `{"module":"sibling","version":"1"}`, []string{sdkGo}, nil, ""}, @@ -363,16 +383,16 @@ func TestTheControlRecords(t *testing.T) { } got, err := PackageUsersOf(modules, manifests, packages, "") want := []PackageUses{ - {Package: "mesh-sdk/go", GoModule: sdkGo, Series: map[string]string{"0.1": "0.1.15", "0.2": ""}, Users: []PackageUser{ + {Package: "@novox/ui", Series: map[string]string{}, Users: []PackageUser{{Module: "new", Accepts: "1.0", Named: true}}}, + {Package: sdkGo, GoModule: sdkGo, Series: map[string]string{"0.1": "0.1.15", "0.2": ""}, Users: []PackageUser{ {Module: "imports", Accepts: "0.1", Newest: "0.1.15"}, {Module: "new", Accepts: "0.2", Named: true}, {Module: "old", Accepts: "0.1", Newest: "0.1.15", Pinned: "v0.1.15"}}}, - {Package: "web/ui", Series: map[string]string{}, Users: []PackageUser{{Module: "new", Accepts: "1.0", Named: true}}}, } if err != nil || !reflect.DeepEqual(got, want) { t.Errorf("indexer_package_users = %+v (%v), want %+v", got, err, want) } - if one, err := PackageUsersOf(modules, manifests, packages, "web/ui"); err != nil || len(one) != 1 || one[0].Package != "web/ui" { + if one, err := PackageUsersOf(modules, manifests, packages, "@novox/ui"); err != nil || len(one) != 1 || one[0].Package != "@novox/ui" { t.Errorf("one package = %+v (%v)", one, err) } if _, err := PackageUsersOf(modules, manifests, packages, "nobody/x"); err == nil { diff --git a/modules/mesh-delivery-indexer/internal/index/package_users.go b/modules/mesh-delivery-indexer/internal/index/package_users.go index 0649e2ee..04adff0c 100644 --- a/modules/mesh-delivery-indexer/internal/index/package_users.go +++ b/modules/mesh-delivery-indexer/internal/index/package_users.go @@ -67,7 +67,7 @@ func PackageUsersOf(modules, manifests, packages views.Source, only string) ([]P if err := json.Unmarshal(e.Value, &s); err != nil { return nil, fmt.Errorf("packages.%s holds what is not a series: %w", key, err) } - p := get(s.Package) + p := get(s.Name) if s.GoModule != "" { p.GoModule = s.GoModule } diff --git a/modules/mesh-delivery-indexer/internal/index/series.go b/modules/mesh-delivery-indexer/internal/index/series.go index ec628a32..4daaf744 100644 --- a/modules/mesh-delivery-indexer/internal/index/series.go +++ b/modules/mesh-delivery-indexer/internal/index/series.go @@ -3,8 +3,10 @@ package index import ( "context" "encoding/json" + "errors" "fmt" "maps" + "path" "slices" "strings" "time" @@ -25,8 +27,12 @@ import ( // SeriesPackage is the value of a series key. type SeriesPackage struct { V int `json:"v"` - // Package is the builder's name of the package, `/`; Series its `.`. - Package string `json:"package"` + // Name is the package's name in its own ecosystem (design 32, the lead's design limit of 2026-11-04): a Go package's + // module path in its go.mod at the commit, an npm package's package.json name. The series key is named by it, and a + // user's `packages` names it. Package is the module and artifact it is built from, `/`, by which + // the builder finds the name to number under; Series its `.`. + Name string `json:"name"` + Package string `json:"package,omitempty"` Series string `json:"series"` // Seat is the registry seat it is published through; GoModule the Go module path a Go package is fetched under (from // go-package-registry's published), empty for any other. @@ -67,7 +73,7 @@ func SeriesOf(version string) (string, bool) { return fmt.Sprintf("%d.%d", p.core[0], p.core[1]), true } -// HeardVersion is what the indexer takes of a numbered or a published: the package's name, the seat, the version and its +// HeardVersion is what the indexer takes of a numbered or a published: the package's name in its ecosystem, the seat, the version and its // tag, the commit, digest and fingerprint, and the Go module path a published names. type HeardVersion struct { Package, Seat, Version, Tag, Commit, Digest, Fingerprint, GoModule string @@ -104,14 +110,17 @@ func (ix *Indexer) HearVersion(ctx context.Context, h HeardVersion) (bool, error } kept := false err := write(ctx, ix.Packages, SeriesKey(h.Package, series), func(value []byte, found bool) ([]byte, bool, error) { - s := SeriesPackage{V: delivery.Version, Package: h.Package, Series: series} + s := SeriesPackage{V: delivery.Version, Name: h.Package, Series: series} if found { if err := json.Unmarshal(value, &s); err != nil { ix.logf("packages.%s holds what is not a series, written anew: %v", SeriesKey(h.Package, series), err) - s = SeriesPackage{V: delivery.Version, Package: h.Package, Series: series} + s = SeriesPackage{V: delivery.Version, Name: h.Package, Series: series} } } kept = false + if s.Name == "" { + s.Name, kept = h.Package, true + } if newer(s.Numbered) { s.Numbered, kept = &numbered, true } @@ -175,7 +184,7 @@ func acceptedSeries(series map[string]SeriesPackage, m catalogue.Manifest, repos var pins delivery.PackageVersions for _, key := range slices.Sorted(maps.Keys(series)) { s := series[key] - if s.GoModule == "" || s.Newest == nil || s.Newest.Contents == nil || m.AcceptedSeries(s.Package) != s.Series { + if s.GoModule == "" || s.Newest == nil || s.Newest.Contents == nil || m.AcceptedSeries(s.Name) != s.Series { continue } if repo, _, ok := delivery.PackageRepository(s.GoModule); !ok || delivery.SameRepository(repo, repository) { @@ -194,3 +203,97 @@ func acceptedSeries(series map[string]SeriesPackage, m catalogue.Manifest, repos } return out, pins } + +// KeepPackageNames writes, at a trunk merge, each package artifact's name in its own ecosystem into its series key +// (design 32): a Go package's module path from the go.mod under its paths at the merge commit, a TypeScript package's +// package.json name, each read through the forge. The key keeps what it held and gains the name and the module and +// artifact it is built from, so the builder numbers it under that name before its first version. A name the forge does +// not answer is said and left for the next merge; a manifest that does not decode is the judge's to refuse. +func (ix *Indexer) KeepPackageNames(ctx context.Context, f delivery.Forge) error { + if ix.Packages == nil || ix.ReadFile == nil || f.Kind != delivery.ForgeMerged || f.Merge == "" { + return nil + } + var errs []error + for _, dir := range slices.Sorted(maps.Keys(f.Manifests)) { + m, err := catalogue.Decode([]byte(f.Manifests[dir])) + if err != nil || m.Build == nil { + continue + } + for _, a := range m.Build.Artifacts { + if a.Kind != catalogue.ArtifactPackage { + continue + } + name, err := ix.packageName(ctx, 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 + } + series, built := a.SeriesOf(), m.Module+"/"+a.Name + errs = append(errs, write(ctx, ix.Packages, SeriesKey(name, series), func(value []byte, found bool) ([]byte, bool, error) { + s := SeriesPackage{V: delivery.Version, Series: series} + if found && json.Unmarshal(value, &s) != nil { + s = SeriesPackage{V: delivery.Version, Series: series} + } + if s.Name == name && s.Package == built && (a.Language != "go" || s.GoModule == name) { + return nil, false, nil + } + s.Name, s.Package = name, built + if a.Language == "go" { + s.GoModule = name + } + s.At = ix.now() + next, err := json.Marshal(s) + return next, false, err + })) + } + } + 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) +}