Take a missing graph state as no graph yet and a Publisher's directory as the nearest module above it

The indexer's recompute and merge failed on listing a graph bucket not yet on the bus, and a package published from inside a module's directory had no publisher, so the dependents of a build module missed what imports its packages (review findings 3 and 4 of L3).

Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
This commit is contained in:
2026-11-03 17:21:01 +01:00
parent 4a736858b6
commit 1387b10e78
4 changed files with 56 additions and 14 deletions
@@ -2,12 +2,14 @@ package index
import (
"context"
"errors"
"fmt"
"maps"
"slices"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/process"
"git.novox.be/novox/mesh-delivery/sdk/graph"
)
@@ -60,11 +62,18 @@ func graphNode(e graphEdges, providers func(string) []catalogue.Manifest) graph.
}
// writeGraph writes the graph node of every module of edges whose value changed, at most GraphMost of them, then
// deletes each graph key whose module the modules state no longer holds. Nil Graph writes nothing.
// deletes each graph key whose module the modules state no longer holds. Nil Graph writes nothing, and neither does a
// graph state that is not on the bus yet (process.ErrNotYet; main.go: until then no graph is written), said in the log;
// a listing that fails otherwise skips the deletion pass, said, and never fails the merge or the recompute that wrote.
func (ix *Indexer) writeGraph(ctx context.Context, edges map[string]graphEdges) error {
if ix.Graph == nil {
return nil
}
keys, listErr := ix.Graph.Keys(ctx)
if errors.Is(listErr, process.ErrNotYet) {
ix.logf("the module graph is not written: its state is not on the bus yet")
return nil
}
names := slices.Sorted(maps.Keys(edges))
if len(names) > GraphMost {
ix.logf("the module graph holds at most %d modules: %d are known, so %s and the %d after it are not written",
@@ -104,19 +113,21 @@ func (ix *Indexer) writeGraph(ctx context.Context, edges map[string]graphEdges)
}
}
held, err := ix.Modules.Keys(ctx)
if err != nil {
return fmt.Errorf("listing the modules for the graph: %w", err)
}
keys, err := ix.Graph.Keys(ctx)
if err != nil {
return fmt.Errorf("listing the graph: %w", err)
switch {
case listErr != nil:
ix.logf("the graph keys of modules the modules state no longer holds are not deleted: listing the graph: %v", listErr)
return nil
case err != nil:
ix.logf("the graph keys of modules the modules state no longer holds are not deleted: listing the modules: %v", err)
return nil
}
for _, key := range keys {
if slices.Contains(held, key) {
continue
}
if err := write(ctx, ix.Graph, key, func(_ []byte, found bool) ([]byte, bool, error) { return nil, found, nil }); err != nil {
return fmt.Errorf("graph.%s: %w", key, err)
ix.logf("graph.%s is not deleted: %v", key, err)
continue
}
ix.logf("graph.%s deleted: the modules state holds no module %s", key, key)
}
@@ -19,6 +19,7 @@ const (
// key under the indexer; unbound, when set, is every read's error (a state not yet on the bus).
type memStore struct {
unbound error
keysErr error // the listing's error
vals map[string][]byte
revs map[string]uint64
next uint64
@@ -36,6 +37,9 @@ func (m *memStore) Get(_ context.Context, key string) ([]byte, uint64, bool, err
return v, m.revs[key], ok, nil
}
func (m *memStore) Keys(context.Context) ([]string, error) {
if m.keysErr != nil {
return nil, m.keysErr
}
var keys []string
for k := range m.vals {
keys = append(keys, k)
+17 -4
View File
@@ -230,8 +230,9 @@ func (g Graph) Requirers(provision string) []string {
return out
}
// Publisher is the module of the graph whose repository and directory publish the mesh package pkg (PackageRepository),
// or "" when the graph holds none.
// Publisher is the module of the graph whose repository and directory publish the mesh package pkg (PackageRepository):
// the module whose directory is the package's directory or the nearest one above it (the longest match, a package
// published from inside a module's directory belonging to that module), or "" when the graph holds none.
func (g Graph) Publisher(pkg string) string {
repository, dir, ok := delivery.PackageRepository(pkg)
if !ok {
@@ -240,12 +241,24 @@ func (g Graph) Publisher(pkg string) string {
if dir == "" {
dir = delivery.RootPath
}
best, bestLen := "", -1
for _, name := range slices.Sorted(maps.Keys(g)) {
if n := g[name]; n.Repository == repository && n.Path == dir {
n := g[name]
if n.Repository != repository {
continue
}
switch {
case n.Path == dir:
return name
case n.Path == delivery.RootPath:
if bestLen < 0 {
best, bestLen = name, 0
}
case strings.HasPrefix(dir, n.Path+"/") && len(n.Path) > bestLen:
best, bestLen = name, len(n.Path)
}
}
return ""
return best
}
// buildEdges are the modules n's build edges reach: its bases, the publisher of each of its packages, and the build
+16 -2
View File
@@ -32,7 +32,11 @@ func TestGraphEdges(t *testing.T) {
n.Bases, n.Packages, n.Toolchains = []string{"base"}, []string{"git.novox.be/novox/lib"}, gotool
}),
"top": node("novox/mesh-catalog", "modules/top", func(n *Node) { n.Bases = []string{"app"} }),
"db": node("novox/mesh-catalog", "modules/db", func(n *Node) { n.Provides = []string{"database"} }),
// A package published from inside a module's directory belongs to that module (ADR 0604 §1): users imports build-go's.
"users": node("novox/other", "modules/users", func(n *Node) {
n.Packages = []string{"git.novox.be/novox/mesh-catalog/modules/build-go/buildrun"}
}),
"db": node("novox/mesh-catalog", "modules/db", func(n *Node) { n.Provides = []string{"database"} }),
"web": node("novox/mesh-catalog", "modules/web", func(n *Node) {
n.Requires, n.Awaits = []string{"database", "db"}, []string{"database"}
}),
@@ -40,7 +44,7 @@ func TestGraphEdges(t *testing.T) {
for module, want := range map[string][]string{
"base": {"app", "top"},
"lib": {"app", "top"},
"build-go": {"app", "top"}, // build-go's own toolchain edge reaches itself, which is left out
"build-go": {"app", "top", "users"}, // build-go's own toolchain edge reaches itself, which is left out
"top": nil,
"db": nil, // a provision edge is no build edge
} {
@@ -63,6 +67,16 @@ func TestGraphEdges(t *testing.T) {
if got := g.Requirers("db"); !reflect.DeepEqual(got, []string{"web"}) {
t.Errorf("requirers of db = %v", got)
}
for pkg, want := range map[string]string{
"git.novox.be/novox/mesh-catalog/modules/build-go/buildrun": "build-go", // inside its directory
"git.novox.be/novox/mesh-catalog/modules/build-go": "build-go", // its directory
"git.novox.be/novox/mesh-catalog/modules/build-gone": "", // beside it, not inside
"git.novox.be/novox/lib/sub": "lib", // a root module publishes what is below it
} {
if got := g.Publisher(pkg); got != want {
t.Errorf("publisher of %s = %q, want %q", pkg, got, want)
}
}
if g.Publisher("git.novox.be/novox/lib") != "lib" || g.Publisher("git.novox.be/novox/mesh-sdk/go") != "" {
t.Error("a package's publisher is not the module at its repository and directory")
}