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
This commit is contained in:
2026-11-11 11:44:56 +01:00
parent bcd0888928
commit ab87109ec5
2 changed files with 91 additions and 0 deletions
@@ -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.<revision>, candidate.<group>.<heads>, entry.<digest>).
// 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
}
@@ -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 {