Merge branch 'feat/47-G-fix11-b5i' into feat/47-G-join11
This commit is contained in:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user