From ab87109ec5b573fef455edd15c3a42a057df5ed1 Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 9 Nov 2026 20:17:04 +0100 Subject: [PATCH] The indexer forms a catalog graph entry per module and a revision as a list of entry digests (milestone 47/G W4.1, W4.2 form) The entry is the seat mesh-catalog's CatalogEntry filled from the module graph's node a recompute computes (build edges, provisions, verbs, the edges left out on a cycle) with where the trunk holds the module and its manifest's digest; a revision lists one entry per module by the sha256 of its bytes, so an entry two revisions share is one object (design 47, an unchanged entry is stored once). Rows in the graph test: one entry per module, and only a moved entry's digest changes. Kept as objects, numbered and said with W4.2's store and W4.3. Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ --- .../internal/index/catalog_graph.go | 65 +++++++++++++++++++ .../internal/index/toolchains_test.go | 26 ++++++++ 2 files changed, 91 insertions(+) create mode 100644 modules/mesh-delivery-indexer/internal/index/catalog_graph.go 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 {