diff --git a/modules/mesh-delivery-indexer/internal/index/catalog_graph.go b/modules/mesh-delivery-indexer/internal/index/catalog_graph.go new file mode 100644 index 00000000..c87d0084 --- /dev/null +++ b/modules/mesh-delivery-indexer/internal/index/catalog_graph.go @@ -0,0 +1,65 @@ +package index + +import ( + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "maps" + "slices" + + "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" + "git.novox.be/novox/mesh-delivery/sdk/graph" +) + +// The catalog graph (novox/hq ADR 0656; design 47, *Two graphs, one source of truth*): at each revision one entry per +// module, as its manifest declares it, in the seat mesh-catalog's layout (its definition's CatalogEntry), and the +// revision a list of its entries' digests, each entry kept once as an object named by its digest +// (catalog-revisions: trunk., candidate.., entry.). + +// CatalogEntryLayout is the layout version an entry and a revision are written in. +const CatalogEntryLayout = 1 + +// CatalogEntryOf is the catalog graph entry of one module: the module graph's node of it as a recompute computed it +// (its build, provision and verb edges, the verb edges left out on a cycle, ADR 0614), with where the trunk holds it and +// its manifest's digest. The verdict and the acting rules are the revision's to add (they are not the node's). +func CatalogEntryOf(module string, cur meshcatalog.ModulesByModuleValue, n graph.Node) meshcatalog.CatalogEntry { + e := meshcatalog.CatalogEntry{V: CatalogEntryLayout, Module: module, Repository: cur.Repository, Path: cur.Path, + Commit: cur.Commit, Manifest: cur.Manifest, Fp: n.FP, None: n.None, Bases: n.Bases, Packages: n.Packages, + Provides: n.Provides, Requires: n.Requires, Awaits: n.Awaits, Invokes: n.Invokes, Serves: n.Serves, LeftOut: n.LeftOut} + for language, t := range n.Toolchains { + if e.Toolchains == nil { + e.Toolchains = map[string]meshcatalog.BuildEdge{} + } + e.Toolchains[language] = meshcatalog.BuildEdge{Module: t.Module, Version: t.Version, Recipe: t.Recipe} + } + return e +} + +// EncodeEntry is an entry's bytes and their sha256 in hex, the name of its object: the same entry in two revisions is +// one object (design 47, *An unchanged entry is stored once*). +func EncodeEntry(e meshcatalog.CatalogEntry) (string, []byte, error) { + data, err := json.Marshal(e) + if err != nil { + return "", nil, fmt.Errorf("the entry of %s: %w", e.Module, err) + } + sum := sha256.Sum256(data) + return hex.EncodeToString(sum[:]), data, nil +} + +// CatalogRevisionOf is a revision of entries, listed by module name with each entry's digest, and the entry objects it +// lists by digest, each once. The caller numbers the revision, names its kind (trunk or candidate) and what it was read +// from. +func CatalogRevisionOf(rev meshcatalog.CatalogRevision, entries map[string]meshcatalog.CatalogEntry) (meshcatalog.CatalogRevision, map[string][]byte, error) { + rev.V, rev.Entries = CatalogEntryLayout, make([]meshcatalog.EntryRef, 0, len(entries)) + objects := map[string][]byte{} + for _, module := range slices.Sorted(maps.Keys(entries)) { + digest, data, err := EncodeEntry(entries[module]) + if err != nil { + return meshcatalog.CatalogRevision{}, nil, err + } + objects[digest] = data + rev.Entries = append(rev.Entries, meshcatalog.EntryRef{Module: module, Digest: digest}) + } + return rev, objects, nil +} diff --git a/modules/mesh-delivery-indexer/internal/index/toolchains_test.go b/modules/mesh-delivery-indexer/internal/index/toolchains_test.go index 17667eef..804983ee 100644 --- a/modules/mesh-delivery-indexer/internal/index/toolchains_test.go +++ b/modules/mesh-delivery-indexer/internal/index/toolchains_test.go @@ -285,6 +285,32 @@ func TestBasesAreComputedFirstAndNoFingerprintTakesTheBuilder(t *testing.T) { // if g := graphOf(t, ix, "other"); g.FP != "" || !strings.Contains(g.None, "ts-base") || !reflect.DeepEqual(g.Bases, []string{"ts-base"}) { t.Errorf("graph.other = %+v, want its base and its reason", g) } + // Milestone 47/G W4.1, W4.2 (design 47, Two graphs): the catalog graph entry of a module is its graph node with where the + // trunk holds it; a revision lists one entry per module by digest, an unchanged entry has the same digest in the next + // revision, and a changed one only its own. + entries := map[string]meshcatalog.CatalogEntry{} + for name := range mods.vals { + m, _ := moduleOf(t, mods, name) + entries[name] = CatalogEntryOf(name, m, graphOf(t, ix, name)) + } + if e := entries["avahi"]; e.Fp != want["avahi"] || e.Commit == "" || e.Manifest == "" || !reflect.DeepEqual(e.Bases, []string{"mesh-tools"}) || + e.Toolchains["go"] != (meshcatalog.BuildEdge{Module: "mesh-build-go", Version: "go1.27.1", Recipe: "1"}) { + t.Errorf("the catalog entry of avahi = %+v", e) + } + rev, objects, err := CatalogRevisionOf(meshcatalog.CatalogRevision{Revision: 1, Kind: "trunk", Source: 7}, entries) + if err != nil || len(rev.Entries) != len(mods.vals) || len(objects) != len(mods.vals) || rev.V != CatalogEntryLayout { + t.Fatalf("revision %+v with %d objects (%v), want one entry per module of %d", rev, len(objects), err, len(mods.vals)) + } + moved := maps.Clone(entries) + tools := moved["mesh-tools"] + tools.Fp = "src2:" + strings.Repeat("b", 64) + moved["mesh-tools"] = tools + next, _, _ := CatalogRevisionOf(meshcatalog.CatalogRevision{Revision: 2, Kind: "trunk", Source: 8}, moved) + for i, ref := range next.Entries { + if same := ref.Digest == rev.Entries[i].Digest; same == (ref.Module == "mesh-tools") { + t.Errorf("entry %s: digest %s against %s before; only mesh-tools' moves", ref.Module, ref.Digest, rev.Entries[i].Digest) + } + } graph := ix.Graph.(*memStore) written := graph.writes if err := ix.Recompute(context.Background()); err != nil || graph.writes != written {