Merge branch 'feat/47-G-fix11-b5i' into feat/47-G-join11

This commit is contained in:
2026-11-12 10:37:09 +01:00
6 changed files with 603 additions and 3 deletions
@@ -676,7 +676,7 @@ type ActingRule struct {
// which entries it acts on: every, or a language
On string `json:"on,omitempty"`
Seat string `json:"seat"`
// take-in, validate, check, build, compose, grant or place
// take-in, validate, check, build, compose, grant, place or pipeline
Step string `json:"step"`
}
@@ -1850,7 +1850,7 @@
},
"step": {
"type": "string",
"description": "take-in, validate, check, build, compose, grant or place"
"description": "take-in, validate, check, build, compose, grant, place or pipeline"
},
"on": {
"type": "string",
@@ -187,7 +187,7 @@ export interface ActingRule {
/** which entries it acts on: every, or a language */
"on"?: string;
"seat": string;
/** take-in, validate, check, build, compose, grant or place */
/** take-in, validate, check, build, compose, grant, place or pipeline */
"step": string;
}
@@ -0,0 +1,373 @@
package index
import (
"cmp"
"context"
"encoding/json"
"maps"
"slices"
"strings"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/seatdef"
)
// 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 is the acting rules' (§711, below). 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. The edges of kinds 1 to 4 and 6 are
// each entry's own; those of kind 5 (§711) need the others' edges of kinds 1 to 4, so they come after.
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
}
built := pathsTo(out)
for module, e := range out {
if acting := actingEdges(e, out, built); len(acting) > 0 {
e.Edges = slices.Clone(e.Edges)
for _, x := range acting {
if !slices.Contains(e.Edges, x) {
e.Edges = append(e.Edges, x)
}
}
sortEdges(e.Edges)
out[module] = e
}
}
return out
}
// kind5Steps are the steps of an acting rule that give an edge of kind 5 (§711): a grant or a credential, a composition, a
// placement and a verdict, each made for the dependent's new version. `build` gives kind 3, `take-in` and `validate` none.
var kind5Steps = []string{seatdef.StepGrant, seatdef.StepCompose, seatdef.StepPlace, seatdef.StepCheck, seatdef.StepPipeline}
// WithActs gives each entry its `acts` (§711): for each seat its manifest claims, each acting rule the seat's definition in
// defs carries, as one item {seat, step, on, bound} sorted by seat then step; an entry claiming no seat with a rule has none.
// The entries given are not changed.
func WithActs(entries map[string]meshcatalog.CatalogEntry, defs map[string]seatdef.Definition) map[string]meshcatalog.CatalogEntry {
out := make(map[string]meshcatalog.CatalogEntry, len(entries))
for module, e := range entries {
var acts []meshcatalog.ActingRule
for _, c := range manifestOf(e).Claims {
for _, r := range defs[c.Name].Acts {
acts = append(acts, meshcatalog.ActingRule{Seat: c.Name, Step: r.Step, On: r.On, Bound: r.Bound})
}
}
slices.SortStableFunc(acts, func(x, y meshcatalog.ActingRule) int {
return cmp.Or(strings.Compare(x.Seat, y.Seat), strings.Compare(x.Step, y.Step))
})
e.Acts = slices.Compact(acts)
out[module] = e
}
return out
}
// pathsTo says, for each module, the modules it is built from or needs: those with a path of edges of kinds 1 to 4 to it
// (a package, a fetched artifact, a build module or base, a provider), read from the entries' edges.
func pathsTo(entries map[string]meshcatalog.CatalogEntry) map[string]map[string]bool {
direct := map[string][]string{} // module -> the modules with an edge of kinds 1 to 4 into it
for module, e := range entries {
for _, x := range e.Edges {
if x.Kind >= 1 && x.Kind <= 4 {
direct[module] = append(direct[module], strings.SplitN(x.From, ":", 2)[0])
}
}
}
out := map[string]map[string]bool{}
for module := range entries {
seen := map[string]bool{}
stack := []string{module}
for len(stack) > 0 {
m := stack[len(stack)-1]
stack = stack[:len(stack)-1]
for _, from := range direct[m] {
if !seen[from] {
seen[from] = true
stack = append(stack, from)
}
}
}
out[module] = seen
}
return out
}
// actingEdges are the edges of kind 5 into entry a (§711): from the delivered artifact of each other entry B whose acting rule
// in a step of kind5Steps matches a, unless a has no artifact or a has a path of edges
// of kinds 1 to 4 to B (built[b][a]): a is then what B's new version is built from or needs, which B's running version acts on (§270).
func actingEdges(a meshcatalog.CatalogEntry, all map[string]meshcatalog.CatalogEntry, built map[string]map[string]bool) []meshcatalog.Edge {
to := primaryOf(a)
if to == "" {
return nil
}
var out []meshcatalog.Edge
for _, m := range slices.Sorted(maps.Keys(all)) {
b := all[m]
from := primaryOf(b)
if m == a.Module || from == "" || built[m][a.Module] {
continue
}
for _, r := range b.Acts {
if slices.Contains(kind5Steps, r.Step) && ruleActsOn(r.On, a) {
out = append(out, meshcatalog.Edge{Kind: 5, From: from, To: to, Word: "acts", Why: r.Seat + " " + r.Step})
}
}
}
return out
}
// ruleActsOn says whether an acting rule's `on` matches entry a: every (or none), a language a has an artifact of, or
// kind:image with an artifact of kind image (§324).
func ruleActsOn(on string, a meshcatalog.CatalogEntry) bool {
switch on {
case "", seatdef.OnEvery:
return true
case seatdef.OnKindImage:
return slices.ContainsFunc(a.Artifacts, func(x meshcatalog.Artifact) bool { return x.Kind == "image" })
}
return slices.ContainsFunc(a.Artifacts, func(x meshcatalog.Artifact) bool { return seatdef.ActingRule{On: on}.ActsOn(x.Language) })
}
func sortEdges(out []meshcatalog.Edge) {
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))
})
}
// 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).
// A verb whose answer's fields it relies on (the long form of `invokes`, §558) names them in the edge's `why`.
fields := manifestOf(a).InvokeFields
for _, verb := range a.Invokes {
if slices.Contains(a.LeftOut, verb) {
continue
}
why := verb
if named := fields[verb]; len(named) > 0 {
why += " fields " + strings.Join(named, ", ")
}
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: why})
}
}
}
// Kind 6 again: a package it builds against whose artifact takes `calls` (the capability words its code calls of the tool
// runner) orders it after the artifact that `states` each word, so no manifest names the tool runner's module (§605, §545).
for _, name := range slices.Sorted(maps.Keys(a.Packages)) {
for _, m := range modules {
for _, art := range all[m].Artifacts {
if art.Package == nil || art.Package.Name != name {
continue
}
for _, word := range wordsOf(all[m], art.Ref, "calls") {
for _, other := range modules {
for _, stating := range all[other].Artifacts {
if other != a.Module && slices.Contains(wordsOf(all[other], stating.Ref, "states"), word) {
add(meshcatalog.Edge{Kind: 6, From: stating.Ref, To: delivers, Word: word, Why: "calls of " + name})
}
}
}
}
}
}
}
sortEdges(out)
return out
}
// manifestOf is the manifest the entry carries whole, read by the manifest reader; none where it cannot be read.
func manifestOf(e meshcatalog.CatalogEntry) catalogue.Manifest {
raw, err := json.Marshal(e.Manifest)
if err != nil {
return catalogue.Manifest{}
}
m, err := catalogue.Decode(raw)
if err != nil {
return catalogue.Manifest{}
}
return m
}
// wordsOf are the words an artifact's entry in the entry's manifest lists under key (`calls` or `states`).
func wordsOf(e meshcatalog.CatalogEntry, ref meshcatalog.ArtifactRef, key string) []string {
build, _ := e.Manifest["build"].(map[string]any)
arts, _ := build["artifacts"].([]any)
var out []string
for _, x := range arts {
art, _ := x.(map[string]any)
if name, _ := art["name"].(string); ArtifactRefOf(e.Module, name) != ref {
continue
}
list, _ := art[key].([]any)
for _, w := range list {
if word, _ := w.(string); word != "" {
out = append(out, word)
}
}
}
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
}
// withActs gives each entry its `acts` from the trunk definitions of the seats the indexer keeps (§711; the state the verb
// seat-definitions answers from); where it keeps none, the entries are left as they are.
func (ix *Indexer) withActs(ctx context.Context, entries map[string]meshcatalog.CatalogEntry) map[string]meshcatalog.CatalogEntry {
if ix.SeatDefinitions == nil {
return entries
}
keys, err := ix.SeatDefinitions.Keys(ctx)
if err != nil {
ix.logf("catalog graph: the acting rules are not read: the %s state cannot be listed: %v", SeatDefinitionsState, err)
return entries
}
defs := map[string]seatdef.Definition{}
for _, key := range keys {
seat, ok := strings.CutSuffix(key, "."+SourceTrunk)
if !ok || strings.HasPrefix(key, outOfStepPrefix) {
continue
}
value, _, found, err := ix.SeatDefinitions.Get(ctx, key)
if err != nil || !found {
continue
}
var e SeatDefinitionEntry
if json.Unmarshal(value, &e) != nil || len(e.Definition) == 0 {
continue
}
var d seatdef.Definition // kept after the format's reader parsed it, so read as it was kept
if json.Unmarshal(e.Definition, &d) == nil {
defs[seat] = d
}
}
return WithActs(entries, defs)
}
@@ -0,0 +1,226 @@
package index
import (
"context"
"slices"
"testing"
"time"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-sdk/go/seatdef"
)
// 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)
}
}
}
// Design 47 G §558, A-208: a verb invoked in the long form {verb, fields} gives its kind-6 edge the fields it relies on,
// read by the second reader release; the short form names none.
func TestAKindSixEdgeNamesTheFieldsALongFormInvokeRelies(t *testing.T) {
entries := map[string]meshcatalog.CatalogEntry{
"holder": {Module: "holder", Artifacts: []meshcatalog.Artifact{{Ref: "holder:p"}}, Serves: []string{"seat:s.list", "seat:s.get"}},
"user": {Module: "user", Artifacts: []meshcatalog.Artifact{{Ref: "user:p"}}, Invokes: []string{"seat:s.get", "seat:s.list"},
Manifest: map[string]any{"module": "user", "version": "1.0", "invokes": []any{"seat:s.get", map[string]any{"verb": "seat:s.list", "fields": []any{"binding", "node"}}}}},
}
got := WithEdges(entries)["user"].Edges
if len(got) != 2 || got[0].Why != "seat:s.get" || got[1].Why != "seat:s.list fields binding, node" || got[1].Word != "seat:s.list" {
t.Errorf("edges %+v, want the short verb as it is and the long form with its fields", got)
}
}
// Design 47 G §605, §545, A-208: a module built against a package whose artifact takes `calls` is ordered after the artifact
// that `states` each word, by an edge of kind 6 naming the word; a module built against no such package has none.
func TestAModuleBuiltOnAPackageThatCallsTheToolRunnerIsOrderedAfterTheArtifactThatStatesTheWords(t *testing.T) {
artifactsOf := func(module string, art map[string]any) map[string]any {
return map[string]any{"module": module, "build": map[string]any{"artifacts": []any{art}}}
}
entries := map[string]meshcatalog.CatalogEntry{
"sdk": {Module: "sdk", Artifacts: []meshcatalog.Artifact{{Ref: "sdk:go", Kind: "package", Language: "go", Package: &meshcatalog.ArtifactPackage{Ecosystem: EcosystemGo, Name: "git.example/sdk/go"}}},
Manifest: artifactsOf("sdk", map[string]any{"name": "go", "calls": []any{"put-sessions", "finding-calls"}})},
"runner": {Module: "runner", Artifacts: []meshcatalog.Artifact{{Ref: "runner:runtime"}},
Manifest: artifactsOf("runner", map[string]any{"name": "runtime", "states": []any{"put-sessions"}})},
"app": {Module: "app", Artifacts: []meshcatalog.Artifact{{Ref: "app:program", Language: "go"}}, Packages: map[string]string{"git.example/sdk/go": "1.0.4"}},
"other": {Module: "other", Artifacts: []meshcatalog.Artifact{{Ref: "other:program", Language: "go"}}},
}
got := WithEdges(entries)
var six []meshcatalog.Edge
for _, e := range got["app"].Edges {
if e.Kind == 6 {
six = append(six, e)
}
}
want := []meshcatalog.Edge{{Kind: 6, From: "runner:runtime", To: "app:program", Word: "put-sessions", Why: "calls of git.example/sdk/go"}}
if !slices.Equal(six, want) {
t.Errorf("app's edges of kind 6 are %+v, want %+v", six, want)
}
if len(got["other"].Edges) != 0 {
t.Errorf("other builds on no such package, its edges %+v", got["other"].Edges)
}
}
func actingEntries() map[string]meshcatalog.CatalogEntry {
rule := func(seat, step, on string) meshcatalog.ActingRule {
return meshcatalog.ActingRule{Seat: seat, Step: step, On: on}
}
return map[string]meshcatalog.CatalogEntry{
"grants": {Module: "grants", Artifacts: []meshcatalog.Artifact{{Ref: "grants:program", Language: "go"}},
Packages: map[string]string{"git.example/lib/go": "1.0.0"}, Toolchains: map[string]meshcatalog.BuildEdge{"go": {Module: "build-go"}},
Acts: []meshcatalog.ActingRule{rule("mesh-grants", "grant", "every"), rule("mesh-grants", "take-in", "every"), rule("mesh-grants", "validate", "")}},
"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"}}, Acts: []meshcatalog.ActingRule{rule("node-build-go", "build", "go")}},
"composer": {Module: "composer", Artifacts: []meshcatalog.Artifact{{Ref: "composer:program", Language: "go"}}, Acts: []meshcatalog.ActingRule{rule("mesh-compose", "compose", "kind:image")}},
"db": {Module: "db", Artifacts: []meshcatalog.Artifact{{Ref: "db:image", Kind: "image"}}},
"web": {Module: "web", Artifacts: []meshcatalog.Artifact{{Ref: "web:program", Language: "typescript"}}},
"bare": {Module: "bare"},
}
}
// Design 47 G §711: an acting rule of a step that acts on a delivered version gives kind 5 from the acting entry's delivered
// artifact to each other entry the rule's `on` matches, with the word `acts` and the why `<seat> <step>`; none to the entry
// itself, to one it is built from (a package, a build module), to one with no artifact; take-in, validate and build give none.
func TestAnActingRuleGivesKindFiveFromTheActingEntryToTheEntriesItActsOn(t *testing.T) {
got := WithEdges(actingEntries())
five := func(m string) []meshcatalog.Edge {
var out []meshcatalog.Edge
for _, e := range got[m].Edges {
if e.Kind == 5 {
out = append(out, e)
}
}
return out
}
grant := func(to string) meshcatalog.Edge {
return meshcatalog.Edge{Kind: 5, From: "grants:program", To: to, Word: "acts", Why: "mesh-grants grant"}
}
for module, want := range map[string][]meshcatalog.Edge{
"web": {grant("web:program")},
"composer": {grant("composer:program")},
"db": {{Kind: 5, From: "composer:program", To: "db:image", Word: "acts", Why: "mesh-compose compose"}, grant("db:image")},
"build-go": nil, // grants is built with it
"lib": nil, // and from it
"grants": nil, // never to itself
"bare": nil, // no artifact
} {
if g := five(module); !slices.Equal(g, want) {
t.Errorf("kind 5 into %s: got %+v, want %+v", module, g, want)
}
}
for _, e := range got["web"].Edges {
if e.Stale || e.Declared {
t.Errorf("an edge of kind 5 only orders: %+v", e)
}
}
}
// Design 47 G §711: `acts` is filled from the definitions of each seat the manifest claims, as {seat, step, on, bound},
// sorted by seat then step; an entry claiming no seat with a rule has none.
func TestAnEntryCarriesTheActingRulesOfTheSeatsItClaims(t *testing.T) {
claims := func(seats ...string) map[string]any {
var list []any
for _, s := range seats {
list = append(list, map[string]any{"name": s})
}
return map[string]any{"module": "m", "version": "1.0", "claims": list}
}
defs := map[string]seatdef.Definition{
"mesh-grants": {Acts: []seatdef.ActingRule{{Step: "grant", On: "every", Bound: "5m"}, {Step: "build", On: "go"}}},
"a-seat": {Acts: []seatdef.ActingRule{{Step: "place"}}},
"unclaimed": {Acts: []seatdef.ActingRule{{Step: "compose"}}},
"plain": {},
}
got := WithActs(map[string]meshcatalog.CatalogEntry{
"m": {Module: "m", Manifest: claims("mesh-grants", "a-seat", "plain")},
"other": {Module: "other", Manifest: claims("plain")},
}, defs)
want := []meshcatalog.ActingRule{{Seat: "a-seat", Step: "place"}, {Seat: "mesh-grants", Step: "build", On: "go"}, {Seat: "mesh-grants", Step: "grant", On: "every", Bound: "5m"}}
if !slices.Equal(got["m"].Acts, want) {
t.Errorf("acts %+v, want %+v", got["m"].Acts, want)
}
if got["other"].Acts != nil {
t.Errorf("a module claiming no seat with a rule has acts %+v", got["other"].Acts)
}
}
// Design 47 G §711: the indexer reads the rules from the seat definitions it keeps when it keeps a revision, so the kept entry
// carries its acts and the entries it acts on carry the edge.
func TestAKeptRevisionHoldsTheActsAndTheKindFiveEdges(t *testing.T) {
ctx := context.Background()
defs := newMem()
_ = defs.Create(ctx, "mesh-grants.trunk", []byte(`{"seat":"mesh-grants","source":"trunk","digest":"d","definition":{"seat":"mesh-grants","acts":[{"step":"grant","on":"every"}]}}`))
ix := &Indexer{Revisions: newMemObjects(1 << 20), SeatDefinitions: defs, 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{
"grants": {Module: "grants", Manifest: map[string]any{"module": "grants", "version": "1.0", "claims": []any{map[string]any{"name": "mesh-grants"}}}, Artifacts: []meshcatalog.Artifact{{Ref: "grants:p"}}},
"app": {Module: "app", Manifest: map[string]any{}, Artifacts: []meshcatalog.Artifact{{Ref: "app:p"}}},
})
if err != nil {
t.Fatal(err)
}
_, entries, err := ReadRevision(ctx, ix.Revisions, "trunk.1")
if err != nil {
t.Fatal(err)
}
for _, e := range entries {
switch e.Module {
case "grants":
if len(e.Acts) != 1 || e.Acts[0].Seat != "mesh-grants" || e.Acts[0].Step != "grant" {
t.Errorf("grants holds acts %+v", e.Acts)
}
case "app":
if len(e.Edges) != 1 || e.Edges[0].Kind != 5 || e.Edges[0].From != "grants:p" || e.Edges[0].Why != "mesh-grants grant" {
t.Errorf("app holds edges %+v, want one of kind 5 from grants: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(ix.withActs(ctx, entries)) // the acting rules and 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 {