The module graph is mesh-delivery's own: a Go module sdk/ with package graph, used by path by the indexer, the view and the checker, and the seat mesh-catalog's answers built from mesh-sdk's contract types (the operator, 2026-11-03: no graph-related code in mesh-sdk, only contracts; research 081 D11, D13)

This commit is contained in:
2026-11-03 16:56:37 +01:00
parent 0ce16ad773
commit 3411b91571
17 changed files with 685 additions and 96 deletions
@@ -10,6 +10,8 @@ import (
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/composer"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/sdk/graph"
)
// The check refuses a change whose module graph has a cycle and names it (novox/hq ADR 0603 §2): modules that would each
@@ -49,15 +51,15 @@ func (st *state) refuseCycle(ctx context.Context) *delivery.Verdict {
if err != nil {
return fail(goModule, "the change's module graph cannot be worked out, so no cycle can be ruled out: "+err.Error())
}
cycle := delivery.ManifestGraph(change).Cycle()
cycle := graph.OfManifests(change).Cycle()
if cycle == nil {
st.c.note("the change's module graph has no cycle")
return nil
}
words := strings.Join(cycle, " -> ")
if before, err := read(req.Cases[1].Manifests); err == nil {
among := delivery.Graph{}
for name, n := range delivery.ManifestGraph(before) {
among := graph.Graph{}
for name, n := range graph.OfManifests(before) {
if slices.Contains(cycle, name) {
among[name] = n
}
@@ -7,6 +7,8 @@ import (
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/sdk/graph"
"mesh-delivery-indexer/internal/index"
)
@@ -35,7 +37,7 @@ func CatalogTools(c index.CatalogRead) []stdio.Tool {
}
// catalogRead reads the indexer's modules, manifests and graph states for the seat's verbs.
func catalogRead(modules, manifests, graph stdio.KeptState) index.CatalogRead {
func catalogRead(modules, manifests, nodes stdio.KeptState) index.CatalogRead {
return index.CatalogRead{
Modules: func() (map[string]delivery.Module, error) {
keys, err := modules.Keys()
@@ -72,21 +74,21 @@ func catalogRead(modules, manifests, graph stdio.KeptState) index.CatalogRead {
kept, err := delivery.DecodeManifest(e.Value)
return kept.JSON, err
},
Graph: func() (delivery.Graph, error) {
keys, err := graph.Keys()
Graph: func() (graph.Graph, error) {
keys, err := nodes.Keys()
if err != nil {
return nil, err
}
g := delivery.Graph{}
g := graph.Graph{}
for _, k := range keys {
e, err := graph.Get(k)
e, err := nodes.Get(k)
if err != nil {
return nil, fmt.Errorf("graph.%s: %w", k, err)
}
if e == nil {
continue
}
if n, err := delivery.DecodeGraphNode(e.Value); err == nil {
if n, err := graph.Decode(e.Value); err == nil {
g[k] = n
}
}
@@ -97,5 +99,5 @@ func catalogRead(modules, manifests, graph stdio.KeptState) index.CatalogRead {
// catalogTools are the seat's verbs over the indexer's own states, as main serves them once the indexer claims the seat.
func catalogTools() []stdio.Tool {
return CatalogTools(catalogRead(stdio.State("modules"), stdio.State("manifests"), stdio.State(delivery.GraphState)))
return CatalogTools(catalogRead(stdio.State("modules"), stdio.State("manifests"), stdio.State(graph.State)))
}
@@ -26,6 +26,8 @@ import (
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/sdk/graph"
"git.novox.be/novox/mesh-sdk/go/process"
"mesh-delivery-indexer/internal/index"
@@ -300,7 +302,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
// the judge refuses no use of an old section.
oldSections := process.AwaitBucket(ctx, js, conn, index.OldSectionsBucket, "the count of the old sections is not kept, so the judge refuses no use of one", logger.Printf)
// The module graph (ADR 0603 §2): a state this delivery brings, bound when it appears; until then no graph is written.
graph := process.AwaitBucket(ctx, js, conn, delivery.GraphBucket, "no module graph is written", logger.Printf)
graph := process.AwaitBucket(ctx, js, conn, graph.Bucket, "no module graph is written", logger.Printf)
ix := &index.Indexer{OldSections: awaitedStore{oldSections}, Store: kvStore{candidates}, Modules: kvStore{modules}, Manifests: kvStore{manifests}, Taken: awaitedStore{taken}, Builds: known,
Verdicts: awaitedStore{verdicts}, SayRefused: sayRefused(js, cred, logger.Printf), SayAccepted: sayAccepted(js, cred, logger.Printf),
Predefined: awaitedStore{predefined}, ConfigCandidates: awaitedStore{configCandidates}, Logf: logger.Printf,
@@ -67,7 +67,7 @@ type Indexer struct {
// Packages is the packages state (delivery.PackagesBucket, packages.go; novox/hq ADR 0593 §2); nil publishes none,
// and every fingerprint takes the version line.
Packages Store
// Graph is the module graph's state (delivery.GraphBucket, graph.go; novox/hq ADR 0603 §2); nil writes no graph.
// Graph is the module graph's state (graph.Bucket, graph.go; novox/hq ADR 0603 §2); nil writes no graph.
Graph Store
// Files asks the forge for every blob under each directory at a commit, by directory then path relative to it, each
// with its git object id (gitea.gitea_commit_trees with files): the listing a package's digest is computed from.
@@ -9,13 +9,15 @@ import (
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/sdk/graph"
)
// The seat mesh-catalog's verbs, answered from the indexer's own states (novox/hq ADR 0604 §1, §2): modules, module,
// provides and dependents read the modules state, the manifests state and the module graph, and write nothing. The
// indexer serves no request subject (ADR 0437 §4): its tools bundle serves them as <seat>.<verb>. Each answer keeps the
// field names of the old catalog module's tool it replaces (catalog_modules, catalog_module, catalog_provides,
// catalog_dependents), so a caller that moves to the seat reads the same fields.
// catalog_dependents): the answers are the seat's contract (catalogue.CatalogModules and the rest), built here.
// CatalogRead reads the states the seat's verbs answer from.
type CatalogRead struct {
@@ -24,99 +26,75 @@ type CatalogRead struct {
// Manifest is the manifest kept under a digest in the manifests state, as its JSON.
Manifest func(digest string) (string, error)
// Graph is the module graph, by module.
Graph func() (delivery.Graph, error)
Graph func() (graph.Graph, error)
}
// CatalogEntry is one module of the catalog: its repository, its directory there (delivery.RootPath for the root), the
// merge commit it was last changed at, and its fingerprint or why it has none.
type CatalogEntry struct {
Module string `json:"module"`
Repository string `json:"repository"`
Path string `json:"path"`
Commit string `json:"commit"`
FP string `json:"fp,omitempty"`
None string `json:"none,omitempty"`
}
// CatalogModule is one module whole: its entry and its manifest as the trunk holds it.
type CatalogModule struct {
CatalogEntry
Manifest json.RawMessage `json:"manifest"`
}
// CatalogEnd is a module that provides, requires or depends on something, with its commit; Scope is a provider's scope.
type CatalogEnd struct {
Module string `json:"module"`
Commit string `json:"commit"`
Scope string `json:"scope,omitempty"`
}
func entryOf(name string, m delivery.Module) CatalogEntry {
return CatalogEntry{Module: name, Repository: m.Repository, Path: m.Path, Commit: m.Commit, FP: m.FP2, None: m.None2}
func entryOf(name string, m delivery.Module) catalogue.CatalogEntry {
return catalogue.CatalogEntry{Module: name, Repository: m.Repository, Path: m.Path, Commit: m.Commit, FP: m.FP2, None: m.None2}
}
// CatalogModulesAnswer is the verb modules: every module the modules state holds, by name.
func (c CatalogRead) CatalogModulesAnswer() (map[string]any, error) {
func (c CatalogRead) CatalogModulesAnswer() (catalogue.CatalogModules, error) {
mods, err := c.Modules()
if err != nil {
return nil, fmt.Errorf("the indexer's modules cannot be read: %w", err)
return catalogue.CatalogModules{}, fmt.Errorf("the indexer's modules cannot be read: %w", err)
}
out := []CatalogEntry{}
out := []catalogue.CatalogEntry{}
for _, name := range slices.Sorted(maps.Keys(mods)) {
out = append(out, entryOf(name, mods[name]))
}
return map[string]any{"modules": out}, nil
return catalogue.CatalogModules{Modules: out}, nil
}
// CatalogModuleAnswer is the verb module: one module whole, its manifest included.
func (c CatalogRead) CatalogModuleAnswer(name string) (map[string]any, error) {
func (c CatalogRead) CatalogModuleAnswer(name string) (catalogue.CatalogOne, error) {
name = strings.TrimSpace(name)
if name == "" {
return nil, fmt.Errorf("name the module")
return catalogue.CatalogOne{}, fmt.Errorf("name the module")
}
mods, err := c.Modules()
if err != nil {
return nil, fmt.Errorf("the indexer's modules cannot be read: %w", err)
return catalogue.CatalogOne{}, fmt.Errorf("the indexer's modules cannot be read: %w", err)
}
m, ok := mods[name]
if !ok {
return nil, fmt.Errorf("the trunk holds no module %s", name)
return catalogue.CatalogOne{}, fmt.Errorf("the trunk holds no module %s", name)
}
one := CatalogModule{CatalogEntry: entryOf(name, m)}
one := catalogue.CatalogModule{CatalogEntry: entryOf(name, m)}
if m.Manifest == "" {
return nil, fmt.Errorf("the indexer keeps no manifest of %s", name)
return catalogue.CatalogOne{}, fmt.Errorf("the indexer keeps no manifest of %s", name)
}
raw, err := c.Manifest(m.Manifest)
if err != nil {
return nil, fmt.Errorf("the manifest %s of %s cannot be read: %w", m.Manifest, name, err)
return catalogue.CatalogOne{}, fmt.Errorf("the manifest %s of %s cannot be read: %w", m.Manifest, name, err)
}
one.Manifest = json.RawMessage(raw)
return map[string]any{"module": one}, nil
return catalogue.CatalogOne{Module: one}, nil
}
// CatalogProvidesAnswer is the verb provides: the modules that provide a provision, each with its scope (a module provides
// its own name, at node scope), and the modules that require it, from the module graph's provision edges.
func (c CatalogRead) CatalogProvidesAnswer(provision string) (map[string]any, error) {
func (c CatalogRead) CatalogProvidesAnswer(provision string) (catalogue.CatalogProvides, error) {
provision = strings.TrimSpace(provision)
if provision == "" {
return nil, fmt.Errorf("name the provision")
return catalogue.CatalogProvides{}, fmt.Errorf("name the provision")
}
g, mods, err := c.graphAndModules()
if err != nil {
return nil, err
return catalogue.CatalogProvides{}, err
}
provides, requires := []CatalogEnd{}, []CatalogEnd{}
provides, requires := []catalogue.CatalogEnd{}, []catalogue.CatalogEnd{}
for _, name := range g.Providers(provision) {
end := CatalogEnd{Module: name, Commit: mods[name].Commit, Scope: catalogue.ScopeNode}
end := catalogue.CatalogEnd{Module: name, Commit: mods[name].Commit, Scope: catalogue.ScopeNode}
if name != provision {
end.Scope = c.scopeOf(mods[name], provision)
}
provides = append(provides, end)
}
for _, name := range g.Requirers(provision) {
requires = append(requires, CatalogEnd{Module: name, Commit: mods[name].Commit})
requires = append(requires, catalogue.CatalogEnd{Module: name, Commit: mods[name].Commit})
}
return map[string]any{"provision": provision, "provides": provides, "requires": requires}, nil
return catalogue.CatalogProvides{Provision: provision, Provides: provides, Requires: requires}, nil
}
// scopeOf is the scope the module's manifest offers provision at, "" when its manifest cannot be read.
@@ -142,23 +120,23 @@ func (c CatalogRead) scopeOf(m delivery.Module, provision string) string {
// CatalogDependentsAnswer is the verb dependents: the modules whose build edges reach the module, directly or through
// others, from the module graph.
func (c CatalogRead) CatalogDependentsAnswer(module string) (map[string]any, error) {
func (c CatalogRead) CatalogDependentsAnswer(module string) (catalogue.CatalogDependents, error) {
module = strings.TrimSpace(module)
if module == "" {
return nil, fmt.Errorf("name the module")
return catalogue.CatalogDependents{}, fmt.Errorf("name the module")
}
g, mods, err := c.graphAndModules()
if err != nil {
return nil, err
return catalogue.CatalogDependents{}, err
}
out := []CatalogEnd{}
out := []catalogue.CatalogEnd{}
for _, name := range g.Dependents(module) {
out = append(out, CatalogEnd{Module: name, Commit: mods[name].Commit})
out = append(out, catalogue.CatalogEnd{Module: name, Commit: mods[name].Commit})
}
return map[string]any{"module": module, "dependents": out}, nil
return catalogue.CatalogDependents{Module: module, Dependents: out}, nil
}
func (c CatalogRead) graphAndModules() (delivery.Graph, map[string]delivery.Module, error) {
func (c CatalogRead) graphAndModules() (graph.Graph, map[string]delivery.Module, error) {
g, err := c.Graph()
if err != nil {
return nil, nil, fmt.Errorf("the module graph cannot be read: %w", err)
@@ -11,6 +11,8 @@ import (
"testing"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/sdk/graph"
)
// capturedKeys are the field names of the first object at path (a list's first element when the value is a list) in the
@@ -67,15 +69,15 @@ func catalogVerbsAnswerFromTheStates(t *testing.T) {
raw, _, err := ix.manifestRaw(context.Background(), digest)
return raw, err
},
Graph: func() (delivery.Graph, error) {
g := delivery.Graph{}
Graph: func() (graph.Graph, error) {
g := graph.Graph{}
for k := range ix.Graph.(*memStore).vals {
g[k] = graphOf(t, ix, k)
}
return g, nil
},
}
answer := func(v map[string]any, err error) []byte {
answer := func(v any, err error) []byte {
t.Helper()
if err != nil {
t.Fatal(err)
@@ -8,10 +8,12 @@ import (
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/sdk/graph"
)
// The module graph (novox/hq ADR 0603 §2): at every recompute the indexer writes, in its state graph
// (delivery.GraphBucket), one key per module it holds the inputs of, its build and provision edges as that recompute
// (graph.Bucket), one key per module it holds the inputs of, its build and provision edges as that recompute
// computed them, and deletes the key of a module the modules state no longer holds. A key is written only when its value
// changes, so its history of 5 is five changes of the module's edges. The delivery view, the checker and the seat
// mesh-catalog's verbs read it; nothing stores an order (ADR 0603 §1).
@@ -33,11 +35,11 @@ type graphEdges struct {
// graphNode is the graph node of e: its edges, the awaited provisions judged against providers (catalogue.Awaited, as
// the assigner judges them).
func graphNode(e graphEdges, providers func(string) []catalogue.Manifest) delivery.GraphNode {
func graphNode(e graphEdges, providers func(string) []catalogue.Manifest) graph.Node {
if e.fp == "" && e.none == "" {
e.none = "its fingerprint was not computed"
}
n := delivery.GraphEdgesOf(e.manifest, providers)
n := graph.EdgesOf(e.manifest, providers)
n.FP, n.None, n.Repository, n.Path = e.fp, e.none, e.cur.Repository, e.cur.Path
n.Packages = slices.Sorted(maps.Keys(e.pkgs))
tcs := e.tcs
@@ -50,9 +52,9 @@ func graphNode(e graphEdges, providers func(string) []catalogue.Manifest) delive
continue
}
if n.Toolchains == nil {
n.Toolchains = map[string]delivery.GraphToolchain{}
n.Toolchains = map[string]graph.Toolchain{}
}
n.Toolchains[language] = delivery.GraphToolchain{Module: builder, Version: t.Version, Recipe: t.Recipe}
n.Toolchains[language] = graph.Toolchain{Module: builder, Version: t.Version, Recipe: t.Recipe}
}
return n
}
@@ -79,7 +81,7 @@ func (ix *Indexer) writeGraph(ctx context.Context, edges map[string]graphEdges)
return out
}
for _, name := range names {
key, err := delivery.GraphKey(name)
key, err := graph.Key(name)
if err != nil {
ix.logf("graph.%s is not written: %v", name, err)
continue
@@ -87,11 +89,11 @@ func (ix *Indexer) writeGraph(ctx context.Context, edges map[string]graphEdges)
next := graphNode(edges[name], providers)
err = write(ctx, ix.Graph, key, func(value []byte, found bool) ([]byte, bool, error) {
if found {
if cur, err := delivery.DecodeGraphNode(value); err == nil && delivery.SameGraphNode(cur, next) {
if cur, err := graph.Decode(value); err == nil && graph.Same(cur, next) {
return nil, false, nil
}
}
data, err := delivery.EncodeGraphNode(next)
data, err := graph.Encode(next)
if err != nil {
return nil, false, fmt.Errorf("graph.%s: %w", key, err)
}
@@ -9,6 +9,8 @@ import (
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/sdk/graph"
"mesh-delivery-indexer/internal/fingerprint"
)
@@ -61,13 +63,13 @@ type mod struct {
// merged applies one merge of the catalogue carrying these modules (every one a Go bundle's go.sum, a TypeScript bundle's
// package-lock.json) and returns the indexer.
// graphOf is the graph node the indexer holds of module.
func graphOf(t *testing.T, ix *Indexer, module string) delivery.GraphNode {
func graphOf(t *testing.T, ix *Indexer, module string) graph.Node {
t.Helper()
v, ok := ix.Graph.(*memStore).vals[module]
if !ok {
t.Fatalf("graph.%s is not held", module)
}
g, err := delivery.DecodeGraphNode(v)
g, err := graph.Decode(v)
if err != nil {
t.Fatal(err)
}
@@ -232,7 +234,7 @@ func TestBasesAreComputedFirstAndNoFingerprintTakesTheBuilder(t *testing.T) { //
t.Errorf("other: fp2 = %q none2 = %q, want none2 naming the base ts-base", m.FP2, m.None2)
}
// L3 (ADR 0603 §2): the module graph holds each module's edges as this recompute computed them.
goTool := map[string]delivery.GraphToolchain{"go": {Module: "mesh-build-go", Version: "go1.27.1", Recipe: "1"}}
goTool := map[string]graph.Toolchain{"go": {Module: "mesh-build-go", Version: "go1.27.1", Recipe: "1"}}
if g := graphOf(t, ix, "avahi"); g.FP != want["avahi"] || !reflect.DeepEqual(g.Bases, []string{"mesh-tools"}) ||
!reflect.DeepEqual(g.Toolchains, goTool) || g.Repository != catalogueRepo || g.Path != "modules/avahi" {
t.Errorf("graph.avahi = %+v", g)
@@ -13,6 +13,8 @@ import (
"testing"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/sdk/graph"
)
// The controller's manifest reader is mesh-controller internal/catalogue (Manifest, manifest.go: Consumes :521,
@@ -91,7 +93,7 @@ func TestManifestDeclarations(t *testing.T) { // 1238/AC1, 1444 (reads the build
}
// 1126 note 76: heard keeps the last 64 FORGE messages the indexer handled, for indexer_heard.
if want := []state{{Name: "candidates", History: 10}, {Name: "heard", History: 64}, {Name: "manifests", History: 1}, {Name: "modules", History: 20}, {Name: "predefined", History: 5}, {Name: "config-candidates", History: 5}, {Name: "taken", History: 1}, {Name: "takeins", History: 1}, {Name: "verdicts", History: 5}, {Name: "oldsections", History: 1}, {Name: delivery.PackagesState, History: delivery.PackagesHistory},
{Name: delivery.GraphState, History: delivery.GraphHistory}}; !reflect.DeepEqual(m.State, want) {
{Name: graph.State, History: graph.History}}; !reflect.DeepEqual(m.State, want) {
t.Errorf("state = %+v, want %+v", m.State, want)
}
if want := []string{"mesh-build-go.toolchains", "mesh-build-node.toolchains", "mesh-delivery-assigner.placement", "mesh-delivery-builder.build-log", "mesh-delivery-checker.previews", "mesh-delivery-gate.source"}; !reflect.DeepEqual(sorted(m.Reads), want) {
@@ -13,6 +13,8 @@ import (
"mesh-delivery-view/internal/view"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/sdk/graph"
"git.novox.be/novox/mesh-sdk/go/provisions"
stdio "git.novox.be/novox/mesh-sdk/go"
@@ -318,7 +320,7 @@ func main() {
PredefinedState: read(stdio.State(delivery.IndexerModule + "." + delivery.IndexerPredefinedState)),
ConfigCandidatesState: read(stdio.State(delivery.IndexerModule + "." + delivery.IndexerConfigCandidatesState)),
PackagesState: read(stdio.State(delivery.IndexerModule + "." + delivery.PackagesState)),
GraphState: read(stdio.State(delivery.GraphRead)), SourceState: read(stdio.State(delivery.GateModule + ".source")),
GraphState: read(stdio.State(graph.Read)), SourceState: read(stdio.State(delivery.GateModule + ".source")),
ProvisionsState: read(stdio.State(provisions.ProvisionsRead)),
BuildLog: stdio.Log(delivery.BuildLogRead).Since}
u := view.UsageReader{Haves: r.HavesState, Resolved: read(stdio.State("mesh-config.resolved")), Tries: read(stdio.State("mesh-delivery-checker.tries")),
@@ -10,6 +10,8 @@ import (
"time"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/sdk/graph"
"git.novox.be/novox/mesh-sdk/go/provisions"
"mesh-delivery-view/internal/view"
@@ -34,8 +36,8 @@ func tiersToolSaysEachModulesWait(t *testing.T) {
b, err := delivery.EncodeModule(delivery.Module{FP2: f, Manifest: manifest, Src: src, FPSrc: src, Repository: "novox/mesh-catalog", Commit: "c"})
return encoded(t, b, err)
}
node := func(g delivery.GraphNode) string {
b, err := delivery.EncodeGraphNode(g)
node := func(g graph.Node) string {
b, err := graph.Encode(g)
return encoded(t, b, err)
}
build := func(b delivery.BuilderBuild) string {
@@ -59,9 +61,9 @@ func tiersToolSaysEachModulesWait(t *testing.T) {
since := time.Date(2026, 11, 3, 16, 40, 0, 0, time.UTC)
r := fake{"db": mod(fp("a"), 9), "web": mod(fp("b"), 9), "lib": mod(fp("c"), 9), "old": mod(fp("e"), 5)}.reader()
r.GraphState = fake{
"db": node(delivery.GraphNode{FP: fp("a"), Repository: "novox/mesh-catalog", Provides: []string{"database"}}),
"web": node(delivery.GraphNode{FP: fp("b"), Repository: "novox/mesh-catalog", Requires: []string{"database"}, Awaits: []string{"database"}}),
"lib": node(delivery.GraphNode{FP: fp("c"), Repository: "novox/mesh-catalog"}),
"db": node(graph.Node{FP: fp("a"), Repository: "novox/mesh-catalog", Provides: []string{"database"}}),
"web": node(graph.Node{FP: fp("b"), Repository: "novox/mesh-catalog", Requires: []string{"database"}, Awaits: []string{"database"}}),
"lib": node(graph.Node{FP: fp("c"), Repository: "novox/mesh-catalog"}),
}.reader().State
r.Builds = fake{
key(delivery.BuilderBuildKey("db", fp("a"))): build(delivery.BuilderBuild{State: delivery.BuildDone, Manifest: manifest}),
@@ -55,7 +55,7 @@ type Reader struct {
PredefinedState, ConfigCandidatesState StateRead
// PackagesState is the indexer's published packages (delivery.PackagesBucket, ADR 0593 §2); unset reads none.
PackagesState StateRead
// GraphState is the indexer's module graph (delivery.GraphBucket, ADR 0603 §2), SourceState the gate's source and
// GraphState is the indexer's module graph (graph.Bucket, ADR 0603 §2), SourceState the gate's source and
// ProvisionsState the provisioner's keys: what view_tiers reads (tiers.go). Unset reads none, said in the answer.
GraphState, SourceState, ProvisionsState StateRead
// BuildLog reads the builder's log from a time (stdio.Log(delivery.BuildLogRead).Since); unset reads none.
@@ -10,6 +10,8 @@ import (
"time"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/sdk/graph"
"git.novox.be/novox/mesh-sdk/go/provisions"
)
@@ -93,13 +95,13 @@ func (r Reader) Tiers(revision uint64) (Tiers, error) {
len(names), TiersMost, names[TiersMost], len(names)-TiersMost-1)
names = names[:TiersMost]
}
graph := delivery.Graph{}
gr := graph.Graph{}
if raws, err := values(r.GraphState); err != nil {
out.Graph = "the module graph cannot be read, so no module is ordered after another: " + err.Error()
} else {
for key, raw := range raws {
if n, err := delivery.DecodeGraphNode(raw); err == nil {
graph[key] = n
if n, err := graph.Decode(raw); err == nil {
gr[key] = n
}
}
}
@@ -115,15 +117,15 @@ func (r Reader) Tiers(revision uint64) (Tiers, error) {
}
after := map[string][]string{}
for _, n := range names {
for _, b := range graph.Before(n) {
for _, b := range gr.Before(n) {
if in[b] {
after[n] = append(after[n], b)
}
}
}
sub := delivery.Graph{}
sub := graph.Graph{}
for _, n := range names {
if g, ok := graph[n]; ok {
if g, ok := gr[n]; ok {
sub[n] = g
}
}
@@ -168,7 +170,7 @@ func (r Reader) Tiers(revision uint64) (Tiers, error) {
}
m := moved[n]
tm := TierModule{Module: n, FP: m.FP2, None: m.None2, After: after[n]}
tm.Waits = r.waitsOf(n, m, graph[n], wants, provided, perr, away)
tm.Waits = r.waitsOf(n, m, gr[n], wants, provided, perr, away)
out.Tiers[t] = append(out.Tiers[t], tm)
}
compact := out.Tiers[:0]
@@ -202,7 +204,7 @@ func (r Reader) away() map[string]bool {
// waitsOf is what module m waits on now: its build at its fingerprint, then on each node present that wants it, its want,
// its have, and each provision its graph node awaits.
func (r Reader) waitsOf(module string, m delivery.Module, g delivery.GraphNode, wants, provided map[string]json.RawMessage,
func (r Reader) waitsOf(module string, m delivery.Module, g graph.Node, wants, provided map[string]json.RawMessage,
perr error, away map[string]bool) []TierWait {
var out []TierWait
if m.FP2 == "" {
+5
View File
@@ -0,0 +1,5 @@
module git.novox.be/novox/mesh-delivery/sdk
go 1.26.0
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103155154-cf5812101b0b
+14
View File
@@ -0,0 +1,14 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103155154-cf5812101b0b h1:UPkJqQj+1y6WraOxDidkrokNjA25jxmCk+zIRkujjMc=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103155154-cf5812101b0b/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA=
github.com/nats-io/nats.go v1.54.0/go.mod h1:y+DZoD1oBOYfZTU681eTUiUjI0vbqYGixNVFHcjHJ0k=
github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg=
github.com/nats-io/nkeys v0.4.16/go.mod h1:llLgWoI0o4z/Q57q2R1kHfmocyhGV6VG/U18Glg1Afs=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
+398
View File
@@ -0,0 +1,398 @@
package graph
import (
"encoding/json"
"errors"
"fmt"
"maps"
"reflect"
"slices"
"strings"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
// Version is the version of a node's value this package writes and the only one it reads.
const Version = 1
// ErrUnknownVersion is what Decode returns for a value whose version is missing or not Version.
var ErrUnknownVersion = errors.New("graph: unknown value version")
func decode(data []byte, into *Node) error {
var probe struct {
V *int `json:"v"`
}
if err := json.Unmarshal(data, &probe); err != nil {
return fmt.Errorf("graph: value does not fit its shape: %w", err)
}
if probe.V == nil || *probe.V != Version {
return fmt.Errorf("%w: this reader reads %d", ErrUnknownVersion, Version)
}
if err := json.Unmarshal(data, into); err != nil {
return fmt.Errorf("graph: value does not fit its shape: %w", err)
}
return nil
}
// checkName refuses a module name a key cannot hold (delivery.ModulesKey's rule).
func checkName(kind, name string) error {
if _, err := delivery.ModulesKey(name); err != nil {
return fmt.Errorf("graph: the %s name %q: %w", kind, name, err)
}
return nil
}
// checkRepository refuses a repository that is not <owner>/<name>.
func checkRepository(repository string) error {
owner, name, ok := strings.Cut(repository, "/")
if !ok || owner == "" || name == "" || strings.Contains(name, "/") {
return fmt.Errorf("graph: the repository %q is not <owner>/<name>", repository)
}
return nil
}
// The module graph (novox/hq ADR 0603 §2, ADR 0604 §1): the indexer keeps, as its state `graph`, one key per module it
// holds the inputs of, written at every recompute and deleted with the module. A key holds the module's build edges
// (its bases, the mesh packages its fingerprint takes, and per language the build module, toolchain version and recipe
// version its fingerprint names, ADR 0530 §1) and its provision edges (what its manifest provides, requires, and of what
// it requires what a want of it waits for). Nothing stores an order: a reader works it out from the edges (ADR 0603
// §1, §6). The indexer writes it and works the edges out (EdgesOf), the delivery view, the checker and the indexer's
// answers of the seat mesh-catalog read it; all three are in this repository, which uses this package by path (the
// operator, 2026-11-03: no graph-related code in mesh-sdk, only contracts). It names no module.
const (
// State is the name of the indexer's state of the module graph, as its manifest declares it, and Bucket its
// bucket: one key per module (Key).
State = "graph"
Bucket = delivery.IndexerModule + "_" + State
// Read is the line a reader's manifest names in `reads` to read the graph.
Read = delivery.IndexerModule + "." + State
// History is the revisions the graph state keeps per key (ADR 0603 §2).
History = 5
)
// Key is the key, in Bucket, of a module: `<module>`, as ModulesKey.
func Key(module string) (string, error) { return delivery.ModulesKey(module) }
// ParseKey is the inverse of Key.
func ParseKey(key string) (string, error) {
module, err := Key(key)
if err != nil {
return "", fmt.Errorf("graph: %q is not a graph key: %w", key, err)
}
return module, nil
}
// Node is the value of a graph key: one module's edges as its last recompute computed them. FP is its new-form
// fingerprint then, or None why it has none (exactly one is set); Repository and Path say where it is (as Module), so a
// package edge is resolved to the module that publishes it (GraphPublisher). Bases are the modules it is built on;
// Packages the mesh packages (Go module paths) its fingerprint took (Module.Packages); Toolchains, by language, the
// build module and the toolchain it was computed with. Provides are the provisions its manifest offers (its own name is
// provided too and is not listed), Requires what it requires and does not require optionally, and Awaits those of
// Requires a want of it waits for (catalogue.Awaited, ADR 0570 §4): the provision edges that order a source revision.
// Every list is sorted and each entry once; an encode sorts them.
type Node struct {
V int `json:"v"`
FP string `json:"fp,omitempty"`
None string `json:"none,omitempty"`
Repository string `json:"repository"`
Path string `json:"path,omitempty"`
Bases []string `json:"bases,omitempty"`
Packages []string `json:"packages,omitempty"`
Toolchains map[string]Toolchain `json:"toolchains,omitempty"`
Provides []string `json:"provides,omitempty"`
Requires []string `json:"requires,omitempty"`
Awaits []string `json:"awaits,omitempty"`
}
// Toolchain is a build edge of one language: the build module that builds it (Module) and the toolchain version
// and recipe version its fingerprint names.
type Toolchain struct {
Module string `json:"module"`
Version string `json:"version"`
Recipe string `json:"recipe"`
}
func (g Node) check() error {
switch {
case (g.FP == "") == (g.None == ""):
return fmt.Errorf("graph: a graph node has either a fingerprint or a reason for none, not both or neither")
case g.FP != "" && !strings.HasPrefix(g.FP, delivery.SourcePrefix2):
return fmt.Errorf("graph: the graph node's fingerprint %q does not start with %s", g.FP, delivery.SourcePrefix2)
}
if err := checkRepository(g.Repository); err != nil {
return err
}
for _, b := range g.Bases {
if err := checkName("base", b); err != nil {
return err
}
}
for _, p := range g.Packages {
if _, _, ok := delivery.PackageRepository(p); !ok {
return fmt.Errorf("graph: the graph node's package %q is no mesh package's path", p)
}
}
for language, t := range g.Toolchains {
if err := checkName("build module", t.Module); err != nil {
return fmt.Errorf("graph: the graph node's toolchain of %q: %w", language, err)
}
if language == "" || t.Version == "" || t.Recipe == "" {
return fmt.Errorf("graph: the graph node's toolchain of %q needs its language, version and recipe version", language)
}
}
for _, list := range [][]string{g.Provides, g.Requires, g.Awaits} {
if slices.Contains(list, "") {
return fmt.Errorf("graph: a graph node names an empty provision")
}
}
for _, a := range g.Awaits {
if !slices.Contains(g.Requires, a) {
return fmt.Errorf("graph: the graph node awaits %q, which it does not require", a)
}
}
return nil
}
func sortedOnce(in []string) []string {
if len(in) == 0 {
return nil
}
out := slices.Clone(in)
slices.Sort(out)
return slices.Compact(out)
}
func (g Node) normal() Node {
g.Bases, g.Packages = sortedOnce(g.Bases), sortedOnce(g.Packages)
g.Provides, g.Requires, g.Awaits = sortedOnce(g.Provides), sortedOnce(g.Requires), sortedOnce(g.Awaits)
if len(g.Toolchains) == 0 {
g.Toolchains = nil
}
return g
}
// Encode is g as JSON at Version, its lists sorted; it refuses what Node's contract does not hold.
func Encode(g Node) ([]byte, error) {
g = g.normal()
if err := g.check(); err != nil {
return nil, err
}
g.V = Version
return json.Marshal(g)
}
// Decode reads a graph node; it refuses an unknown version (ErrUnknownVersion) and what Encode refuses.
func Decode(data []byte) (Node, error) {
var g Node
if err := decode(data, &g); err != nil {
return Node{}, err
}
if err := g.check(); err != nil {
return Node{}, err
}
return g.normal(), nil
}
// Same says whether two graph nodes are the same value once sorted, so a writer writes only when they differ.
func Same(a, b Node) bool {
a, b = a.normal(), b.normal()
a.V, b.V = 0, 0
return reflect.DeepEqual(a, b)
}
// Graph is the module graph as a reader holds it: every graph node, by module.
type Graph map[string]Node
// Providers are the modules that provide provision: each whose Provides names it, and the module of that name (a module
// provides its own name), sorted.
func (g Graph) Providers(provision string) []string {
var out []string
for name, n := range g {
if name == provision || slices.Contains(n.Provides, provision) {
out = append(out, name)
}
}
slices.Sort(out)
return out
}
// Requirers are the modules that require provision, not optionally, sorted.
func (g Graph) Requirers(provision string) []string {
var out []string
for name, n := range g {
if slices.Contains(n.Requires, provision) {
out = append(out, name)
}
}
slices.Sort(out)
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.
func (g Graph) Publisher(pkg string) string {
repository, dir, ok := delivery.PackageRepository(pkg)
if !ok {
return ""
}
if dir == "" {
dir = delivery.RootPath
}
for _, name := range slices.Sorted(maps.Keys(g)) {
if n := g[name]; n.Repository == repository && n.Path == dir {
return name
}
}
return ""
}
// buildEdges are the modules n's build edges reach: its bases, the publisher of each of its packages, and the build
// module of each language.
func (g Graph) buildEdges(n Node) []string {
out := slices.Clone(n.Bases)
for _, p := range n.Packages {
if m := g.Publisher(p); m != "" {
out = append(out, m)
}
}
for _, t := range n.Toolchains {
out = append(out, t.Module)
}
return sortedOnce(out)
}
// Dependents are the modules whose build edges reach module, directly or through other modules' (a base of a base, a
// module built by a build module that stands on module), sorted and without module itself (ADR 0604 §1): what a
// change of module builds again.
func (g Graph) Dependents(module string) []string {
reverse := map[string][]string{}
for _, name := range slices.Sorted(maps.Keys(g)) {
for _, to := range g.buildEdges(g[name]) {
reverse[to] = append(reverse[to], name)
}
}
seen := map[string]bool{module: true}
queue := []string{module}
var out []string
for len(queue) > 0 {
at := queue[0]
queue = queue[1:]
for _, from := range reverse[at] {
if !seen[from] {
seen[from] = true
out = append(out, from)
queue = append(queue, from)
}
}
}
slices.Sort(out)
return out
}
// Before are the modules name waits for, by the graph's ordering edges, sorted: its bases and, for each provision it
// awaits, every other module that provides it (ADR 0603 §1). A toolchain or package edge orders nothing (§3).
func (g Graph) Before(name string) []string {
n := g[name]
out := slices.Clone(n.Bases)
for _, a := range n.Awaits {
for _, p := range g.Providers(a) {
if p != name {
out = append(out, p)
}
}
}
return sortedOnce(out)
}
// Cycle is the first cycle of the graph's ordering edges, as the modules it passes through with the first named again
// at its end (a -> b -> a), or nil for none (ADR 0603 §2: the check refuses it and names it). The ordering edges are a
// module's bases and, for each provision it awaits, every other module that provides it: modules ordered on each other
// would each wait for the other for good. A toolchain edge is no ordering edge (ADR 0603 §3: nothing waits on a build
// module), and neither is a package edge. The modules are visited in sorted order, so the same graph names the same
// cycle.
func (g Graph) Cycle() []string {
edges := g.Before
const (
unseen = iota
onPath
done
)
state := map[string]int{}
var path []string
var visit func(string) []string
visit = func(name string) []string {
state[name] = onPath
path = append(path, name)
for _, to := range edges(name) {
if _, ours := g[to]; !ours {
continue
}
switch state[to] {
case onPath:
i := slices.Index(path, to)
return append(slices.Clone(path[i:]), to)
case unseen:
if c := visit(to); c != nil {
return c
}
}
}
path = path[:len(path)-1]
state[name] = done
return nil
}
for _, name := range slices.Sorted(maps.Keys(g)) {
if state[name] == unseen {
if c := visit(name); c != nil {
return c
}
}
}
return nil
}
// EdgesOf is the graph node of manifest's own edges: its bases (each module its build stands on), what it provides,
// what it requires and does not require optionally, and of those what a want of it waits for (catalogue.Awaited, as the
// assigner judges it), providers answering the manifests that provide a provision. The fingerprint, place, packages and
// toolchains are the indexer's to add; this is the part a reader of manifests alone (the checker) works out the same way.
func EdgesOf(manifest catalogue.Manifest, providers func(provision string) []catalogue.Manifest) Node {
var n Node
if manifest.Build != nil {
for _, on := range manifest.Build.On {
if on.Module != "" {
n.Bases = append(n.Bases, on.Module)
}
}
}
for _, o := range manifest.Provides {
n.Provides = append(n.Provides, o.Name)
}
for _, r := range manifest.Requires {
if !manifest.OptionalRequires[r] {
n.Requires = append(n.Requires, r)
}
}
n.Awaits = catalogue.Awaited(manifest, providers)
return n.normal()
}
// OfManifests is the graph of these manifests' own edges (EdgesOf), by module, each provision's providers being
// the manifests among them that offer it: what the check works out for a change to find a cycle (ADR 0603 §2).
func OfManifests(manifests map[string]catalogue.Manifest) Graph {
providers := func(provision string) []catalogue.Manifest {
var out []catalogue.Manifest
for _, name := range slices.Sorted(maps.Keys(manifests)) {
if slices.Contains(manifests[name].Offers(), provision) {
out = append(out, manifests[name])
}
}
return out
}
g := Graph{}
for name, m := range manifests {
g[name] = EdgesOf(m, providers)
}
return g
}
+174
View File
@@ -0,0 +1,174 @@
package graph
import (
"errors"
"reflect"
"slices"
"strings"
"testing"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
// TestGraphEdges: the module graph's cycle, dependents and providers, each from the edges alone (ADR 0603 §2, ADR 0604 §1).
func TestGraphEdges(t *testing.T) {
t.Run("a node's value", nodeValues)
fp := "src2:" + strings.Repeat("b", 64)
node := func(repository, path string, edit func(*Node)) Node {
n := Node{FP: fp, Repository: repository, Path: path}
if edit != nil {
edit(&n)
}
return n
}
gotool := map[string]Toolchain{"go": {Module: "build-go", Version: "go1", Recipe: "1"}}
// A base chain, a package and a build module: app stands on base, imports lib's package, is built by build-go.
g := Graph{
"build-go": node("novox/mesh-catalog", "modules/build-go", func(n *Node) { n.Toolchains = gotool }),
"lib": node("novox/lib", delivery.RootPath, nil),
"base": node("novox/mesh-catalog", "modules/base", nil),
"app": node("novox/mesh-catalog", "modules/app", func(n *Node) {
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"} }),
"web": node("novox/mesh-catalog", "modules/web", func(n *Node) {
n.Requires, n.Awaits = []string{"database", "db"}, []string{"database"}
}),
}
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
"top": nil,
"db": nil, // a provision edge is no build edge
} {
if got := g.Dependents(module); !reflect.DeepEqual(got, want) {
t.Errorf("dependents of %s = %v, want %v", module, got, want)
}
}
if got := g.Providers("database"); !reflect.DeepEqual(got, []string{"db"}) {
t.Errorf("providers of database = %v", got)
}
if got := g.Providers("db"); !reflect.DeepEqual(got, []string{"db"}) { // a module provides its own name
t.Errorf("providers of db = %v", got)
}
if got := g.Before("web"); !reflect.DeepEqual(got, []string{"db"}) { // web awaits database, which db provides
t.Errorf("web waits for %v", got)
}
if got := g.Before("top"); !reflect.DeepEqual(got, []string{"app"}) {
t.Errorf("top waits for %v", got)
}
if got := g.Requirers("db"); !reflect.DeepEqual(got, []string{"web"}) {
t.Errorf("requirers of db = %v", got)
}
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")
}
// From manifests alone (the check's graph): a requirement answered by a helper at mesh scope is awaited, an optional
// one is not, and two modules awaiting each other's provision are a cycle.
offer := func(name string) []catalogue.Offer {
return []catalogue.Offer{{Name: name, Scope: catalogue.ScopeMesh, AnsweredBy: catalogue.AnsweredByHelper}}
}
ms := map[string]catalogue.Manifest{
"db": {Module: "db", Provides: offer("database"), Requires: []string{"site", "logs"}, OptionalRequires: map[string]bool{"logs": true}},
"web": {Module: "web", Provides: offer("site"), Requires: []string{"database"}, Build: &catalogue.Build{On: []catalogue.BuildsOn{{Module: "base"}}}},
}
mg := OfManifests(ms)
if db := mg["db"]; !reflect.DeepEqual(db.Requires, []string{"site"}) || !reflect.DeepEqual(db.Awaits, []string{"site"}) ||
!reflect.DeepEqual(mg["web"].Bases, []string{"base"}) || !reflect.DeepEqual(db.Provides, []string{"database"}) {
t.Errorf("the graph of manifests = %+v", mg)
}
if got := mg.Cycle(); !reflect.DeepEqual(got, []string{"db", "web", "db"}) {
t.Errorf("the cycle of manifests = %v", got)
}
for name, c := range map[string]struct {
edit func(Graph)
want []string
}{
"none: a toolchain on itself and a requirement not awaited order nothing": {func(Graph) {}, nil},
"bases": {func(g Graph) {
b := g["base"]
b.Bases = []string{"top"}
g["base"] = b
}, []string{"app", "base", "top", "app"}},
"provisions a -> b -> a": {func(g Graph) {
d := g["db"]
d.Requires, d.Awaits = []string{"site"}, []string{"site"}
g["db"] = d
w := g["web"]
w.Provides = []string{"site"}
g["web"] = w
}, []string{"db", "web", "db"}},
"a requirement not awaited is no edge": {func(g Graph) {
d := g["db"]
d.Requires = []string{"site"}
g["db"] = d
w := g["web"]
w.Provides = []string{"site"}
g["web"] = w
}, nil},
} {
h := Graph{}
for k, v := range g {
h[k] = v
}
c.edit(h)
if got := h.Cycle(); !reflect.DeepEqual(got, c.want) {
t.Errorf("%s: cycle = %v, want %v", name, got, c.want)
}
}
}
// sampleNode is a graph node with every kind of edge, its lists unsorted.
func sampleNode() Node {
return Node{FP: "src2:" + strings.Repeat("a", 64), Repository: "novox/mesh-catalog", Path: "modules/gitea",
Bases: []string{"z-base", "a-base"}, Packages: []string{"git.novox.be/novox/mesh-sdk/go"},
Toolchains: map[string]Toolchain{"go": {Module: "build-go", Version: "go1.27.1", Recipe: "3"}},
Provides: []string{"forge"}, Requires: []string{"postgres-database", "dns"}, Awaits: []string{"postgres-database"}}
}
// nodeValues: a node round-trips with its lists sorted, an unknown version is refused, a node that does not hold is
// refused naming why, and the state is the indexer's graph (ADR 0603 §2).
func nodeValues(t *testing.T) {
if Bucket != "mesh-delivery-indexer_graph" || Read != "mesh-delivery-indexer.graph" || History != 5 || State != "graph" {
t.Error("the graph's state differs from the decided layout")
}
g := sampleNode()
data, err := Encode(g)
if err != nil || !strings.Contains(string(data), `"v":1`) {
t.Fatalf("encode: %s %v", data, err)
}
if got, err := Decode(data); err != nil || !Same(got, g) || !slices.IsSorted(got.Bases) {
t.Errorf("round trip: %+v %v", got, err)
}
if Same(g, Node{None: "x", Repository: "a/b"}) {
t.Error("different graph nodes are the same")
}
for _, in := range []string{`{"none":"x","repository":"a/b"}`, `{"v":2,"none":"x","repository":"a/b"}`} {
if _, err := Decode([]byte(in)); !errors.Is(err, ErrUnknownVersion) {
t.Errorf("decode of %s = %v, want ErrUnknownVersion", in, err)
}
}
for name, c := range map[string]struct {
change func(*Node)
word string
}{
"both": {func(g *Node) { g.None = "why" }, "both"},
"neither": {func(g *Node) { g.FP = "" }, "neither"},
"an old form": {func(g *Node) { g.FP = "src1:x" }, "src2:"},
"a dotted base": {func(g *Node) { g.Bases = []string{"a.b"} }, "a.b"},
"a foreign package": {func(g *Node) { g.Packages = []string{"example.com/x"} }, "example.com/x"},
"a toolchain of no module": {func(g *Node) { g.Toolchains = map[string]Toolchain{"go": {Version: "go1", Recipe: "1"}} }, "build module"},
"a toolchain of no recipe": {func(g *Node) { g.Toolchains = map[string]Toolchain{"go": {Module: "b", Version: "go1"}} }, "recipe"},
"awaiting what it does not require": {func(g *Node) { g.Awaits = []string{"other"} }, "other"},
"no repository": {func(g *Node) { g.Repository = "" }, "repositor"},
} {
g := sampleNode()
c.change(&g)
if _, err := Encode(g); err == nil || !strings.Contains(err.Error(), c.word) {
t.Errorf("a node with %s: %v, want it refused naming %q", name, err, c.word)
}
}
}