IX8: the indexer writes each entry's edges of kinds 1, 2, 3, 4 and 6 into the revision it keeps

An entry's edges run between artifact references, from the artifact that comes first: kind 1 from the publisher of a
package it builds against, 2 from an artifact its `needs` names, 3 from its build module and its bases, 4 from the
provider of a provision a want of it waits for, 6 from the server of a verb it invokes (none on a cycle). Kind 5 waits
on a design line (A-212).

Builds: design 47 G §47, §48, §49, §50, §121, §122, §378, §432 (A-208, A-212)

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
2026-11-12 06:02:58 +01:00
parent c2950e332f
commit ca4aa88583
3 changed files with 235 additions and 0 deletions
@@ -0,0 +1,168 @@
package index
import (
"cmp"
"maps"
"slices"
"strings"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshcatalog"
)
// The edges of a catalog graph entry (design 47 G §47 to §50, §121; A-208): the indexer writes, in each entry of a revision,
// the edges between artifact references of kinds 1, 2, 3, 4 and 6 that the entry's own words and the other entries of the same
// revision say (the builder and the assigner read them from the entry, never derive their own). Kind 5 waits on a design line (A-212). An edge runs from B, the artifact that comes first, to A, the artifact that waits for it.
// WithEdges gives each entry of a revision its edges. The entries given are not changed.
func WithEdges(entries map[string]meshcatalog.CatalogEntry) map[string]meshcatalog.CatalogEntry {
out := make(map[string]meshcatalog.CatalogEntry, len(entries))
for module, e := range entries {
e.Edges = edgesOf(e, entries)
out[module] = e
}
return out
}
// primaryOf is the artifact an entry delivers: the one its first process resource runs, else its first artifact; empty for a
// module with no artifact.
func primaryOf(e meshcatalog.CatalogEntry) meshcatalog.ArtifactRef {
if resources, ok := e.Manifest["resources"].([]any); ok {
for _, r := range resources {
res, _ := r.(map[string]any)
if res["type"] != "process" {
continue
}
if name, _ := res["artifact"].(string); name != "" && slices.ContainsFunc(e.Artifacts, func(a meshcatalog.Artifact) bool { return a.Ref == ArtifactRefOf(e.Module, name) }) {
return ArtifactRefOf(e.Module, name)
}
}
}
if len(e.Artifacts) > 0 {
return e.Artifacts[0].Ref
}
return ""
}
// ofLanguage are the artifacts of e a build of language (go or another) produces: its packages' and programs' of that language.
func ofLanguage(e meshcatalog.CatalogEntry, language string) []meshcatalog.ArtifactRef {
var out []meshcatalog.ArtifactRef
for _, a := range e.Artifacts {
if a.Language == language {
out = append(out, a.Ref)
}
}
return out
}
func edgesOf(a meshcatalog.CatalogEntry, all map[string]meshcatalog.CatalogEntry) []meshcatalog.Edge {
var out []meshcatalog.Edge
add := func(e meshcatalog.Edge) {
if e.From != "" && e.To != "" && e.From != e.To && !slices.Contains(out, e) {
out = append(out, e)
}
}
modules := slices.Sorted(maps.Keys(all))
delivers := primaryOf(a)
// Kind 1: a package its build imports, from the artifact that publishes it, to each artifact built from that ecosystem (§47).
for _, name := range slices.Sorted(maps.Keys(a.Packages)) {
for _, m := range modules {
b := all[m]
if m == a.Module {
continue
}
for _, art := range b.Artifacts {
if art.Package == nil || art.Package.Name != name {
continue
}
for _, to := range importersOf(a, art.Package.Ecosystem) {
add(meshcatalog.Edge{Kind: 1, From: art.Ref, To: to, Stale: true, Word: "packages", Why: name})
}
}
}
}
// Kind 2: an artifact its manifest's `needs` references in another module, published before the artifact that names it
// delivers (A-212, §432).
for _, n := range needsOf(a) {
if owner := all[strings.SplitN(n.ref, ":", 2)[0]]; owner.Module != "" && owner.Module != a.Module &&
slices.ContainsFunc(owner.Artifacts, func(x meshcatalog.Artifact) bool { return x.Ref == n.ref }) {
add(meshcatalog.Edge{Kind: 2, From: n.ref, To: n.by, Declared: true, Word: "needs", Why: n.ref})
}
}
// Kind 3: the build module its language names and the modules it is built on (§47, §378).
for _, language := range slices.Sorted(maps.Keys(a.Toolchains)) {
builder, ok := all[a.Toolchains[language].Module]
if !ok || builder.Module == a.Module {
continue
}
for _, to := range ofLanguage(a, language) {
add(meshcatalog.Edge{Kind: 3, From: primaryOf(builder), To: to, Stale: true, Word: "toolchains", Why: language})
}
}
for _, base := range a.Bases {
b, ok := all[base]
if !ok || b.Module == a.Module {
continue
}
for _, art := range a.Artifacts {
add(meshcatalog.Edge{Kind: 3, From: primaryOf(b), To: art.Ref, Stale: true, Word: "build.on", Why: base})
}
}
// Kind 4: a provision it requires and a provider offers (§47): the provisions a want of it waits for.
for _, provision := range a.Awaits {
for _, m := range modules {
if b := all[m]; m != a.Module && (slices.Contains(b.Provides, provision) || m == provision) {
add(meshcatalog.Edge{Kind: 4, From: primaryOf(b), To: delivers, Word: "requires", Why: provision})
}
}
}
// Kind 6: a verb or a tool it invokes and another entry serves (§47), unless its edge lies on a cycle and is left out (ADR 0614 §3).
for _, verb := range a.Invokes {
if slices.Contains(a.LeftOut, verb) {
continue
}
for _, m := range modules {
if b := all[m]; m != a.Module && slices.Contains(b.Serves, verb) {
add(meshcatalog.Edge{Kind: 6, From: primaryOf(b), To: delivers, Word: verb, Why: verb})
}
}
}
slices.SortFunc(out, func(x, y meshcatalog.Edge) int {
return cmp.Or(cmp.Compare(x.Kind, y.Kind), strings.Compare(x.From, y.From), strings.Compare(x.To, y.To), strings.Compare(x.Word, y.Word), strings.Compare(x.Why, y.Why))
})
return out
}
type need struct{ by, ref string }
// needsOf are the artifact references each artifact of the entry's manifest names in its `needs`, with the artifact that names them.
func needsOf(e meshcatalog.CatalogEntry) []need {
build, _ := e.Manifest["build"].(map[string]any)
arts, _ := build["artifacts"].([]any)
var out []need
for _, x := range arts {
art, _ := x.(map[string]any)
name, _ := art["name"].(string)
list, _ := art["needs"].([]any)
for _, r := range list {
if ref, _ := r.(string); ref != "" && name != "" {
out = append(out, need{by: ArtifactRefOf(e.Module, name), ref: ref})
}
}
}
return out
}
// importersOf are the artifacts of a built from a package of ecosystem: a Go package by the Go artifacts, an npm package by
// the others that are no image.
func importersOf(a meshcatalog.CatalogEntry, ecosystem string) []meshcatalog.ArtifactRef {
var out []meshcatalog.ArtifactRef
for _, art := range a.Artifacts {
if art.Kind == "image" {
continue
}
if (ecosystem == EcosystemGo) == (art.Language == "go") && ecosystem != EcosystemImage {
out = append(out, art.Ref)
}
}
return out
}
@@ -0,0 +1,66 @@
package index
import (
"context"
"slices"
"testing"
"time"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshcatalog"
)
// Design 47 G §47, §121, A-208, A-212: an entry's edges between artifact references are written from its own words and
// the other entries of the revision, kinds 1 (an imported package), 2 (a `needs`), 3 (build module and bases), 4 (an
// awaited provision) and 6 (an invoked verb, none that lies on a cycle), each from B to A.
func TestAnEntryCarriesItsEdgesOfKindsOneToFourAndSix(t *testing.T) {
entries := map[string]meshcatalog.CatalogEntry{
"lib": {Module: "lib", Artifacts: []meshcatalog.Artifact{{Ref: "lib:go", Language: "go", Kind: "package", Package: &meshcatalog.ArtifactPackage{Ecosystem: EcosystemGo, Name: "git.example/lib/go"}}}},
"build-go": {Module: "build-go", Artifacts: []meshcatalog.Artifact{{Ref: "build-go:program", Language: "go"}},
Provides: []string{"builder"}, Serves: []string{"seat:mesh-builds.build"}},
"db": {Module: "db", Artifacts: []meshcatalog.Artifact{{Ref: "db:image", Kind: "image"}}, Provides: []string{"sql"}},
"cfg": {Module: "cfg", Artifacts: []meshcatalog.Artifact{{Ref: "cfg:archive", Kind: "archive"}}},
"app": {Module: "app", Packages: map[string]string{"git.example/lib/go": "1.2.3"}, Bases: []string{"db"},
Toolchains: map[string]meshcatalog.BuildEdge{"go": {Module: "build-go"}},
Awaits: []string{"sql"}, Invokes: []string{"seat:mesh-builds.build", "seat:mesh-builds.cycle"}, LeftOut: []string{"seat:mesh-builds.cycle"},
Manifest: map[string]any{"build": map[string]any{"artifacts": []any{map[string]any{"name": "program", "needs": []any{"cfg:archive", "nothing:here"}}}}},
Artifacts: []meshcatalog.Artifact{{Ref: "app:program", Language: "go"}}},
}
before := entries["app"]
got := WithEdges(entries)["app"].Edges
want := []meshcatalog.Edge{
{Kind: 1, From: "lib:go", To: "app:program", Stale: true, Word: "packages", Why: "git.example/lib/go"},
{Kind: 2, From: "cfg:archive", To: "app:program", Declared: true, Word: "needs", Why: "cfg:archive"},
{Kind: 3, From: "build-go:program", To: "app:program", Stale: true, Word: "toolchains", Why: "go"},
{Kind: 3, From: "db:image", To: "app:program", Stale: true, Word: "build.on", Why: "db"},
{Kind: 4, From: "db:image", To: "app:program", Word: "requires", Why: "sql"},
{Kind: 6, From: "build-go:program", To: "app:program", Word: "seat:mesh-builds.build", Why: "seat:mesh-builds.build"},
}
if !slices.Equal(got, want) {
t.Errorf("edges\n got %+v\nwant %+v", got, want)
}
if len(before.Edges) != 0 || len(entries["app"].Edges) != 0 {
t.Errorf("the entries given were changed")
}
}
// Design 47 G A-208: a revision kept holds each entry with its edges, so the builder and the assigner read them from the entry.
func TestAKeptRevisionHoldsTheEdgesOfItsEntries(t *testing.T) {
ctx := context.Background()
ix := &Indexer{Revisions: newMemObjects(1 << 20), Now: func() time.Time { return time.Date(2026, 11, 1, 3, 0, 0, 0, time.UTC) }}
err := ix.KeepBranchEntries(ctx, "main", 1, map[string]meshcatalog.CatalogEntry{
"b": {Module: "b", Manifest: map[string]any{}, Artifacts: []meshcatalog.Artifact{{Ref: "b:p"}}, Serves: []string{"seat:s.v"}},
"a": {Module: "a", Manifest: map[string]any{}, Artifacts: []meshcatalog.Artifact{{Ref: "a:p"}}, Invokes: []string{"seat:s.v"}},
})
if err != nil {
t.Fatal(err)
}
_, entries, err := ReadRevision(ctx, ix.Revisions, "trunk.1")
if err != nil {
t.Fatal(err)
}
for _, e := range entries {
if e.Module == "a" && (len(e.Edges) != 1 || e.Edges[0].Kind != 6 || e.Edges[0].From != "b:p" || e.Edges[0].To != "a:p") {
t.Errorf("the kept entry of a holds %+v, want one edge of kind 6 from b:p", e.Edges)
}
}
}
@@ -322,6 +322,7 @@ func (ix *Indexer) keepBranchEntries(ctx context.Context, branch string, src int
ix.logf("catalog graph: no revision is kept: the newest is not read: %v", err)
return fmt.Errorf("the newest catalog graph revision: %w", err)
}
entries = WithEdges(entries) // the edges between artifact references are written here, once, for every reader (A-208)
at := ix.now().UTC()
rev, objects, err := CatalogRevisionOf(meshcatalog.CatalogRevision{Revision: newest.Revision + 1, Kind: "trunk", Branch: branch, Source: src, At: &at}, entries)
if err != nil {