From 536c5732238e6f471b4ba379f003375de2e97e8e Mon Sep 17 00:00:00 2001 From: jochen Date: Fri, 13 Nov 2026 17:45:43 +0100 Subject: [PATCH] A-361: a package's seat call words obey the cycle rule MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The graph left out only invoked verbs on a cycle, so a package's seat call word's kind-6 edge stayed on a cycle (the gate and the indexer, the gate and mesh-hand-acts at every SDK release). The module graph's LeaveOut now runs over the entries with each package's seat call words counted among the invokes of every module built against it, and what it leaves out joins each entry's LeftOut. Builds: design 47 G A-361, A-357; ADR 0614 §3 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h --- .../internal/index/catalog_edges.go | 57 +++++++++++++++++++ .../internal/index/catalog_edges_test.go | 41 +++++++++++++ 2 files changed, 98 insertions(+) diff --git a/modules/mesh-delivery-indexer/internal/index/catalog_edges.go b/modules/mesh-delivery-indexer/internal/index/catalog_edges.go index 0f6aaad5..a26a47c4 100644 --- a/modules/mesh-delivery-indexer/internal/index/catalog_edges.go +++ b/modules/mesh-delivery-indexer/internal/index/catalog_edges.go @@ -21,6 +21,7 @@ import ( // 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 { + entries = withCallsLeftOut(entries) out := make(map[string]meshcatalog.CatalogEntry, len(entries)) for module, e := range entries { e.Edges = edgesOf(e, entries) @@ -42,6 +43,62 @@ func WithEdges(entries map[string]meshcatalog.CatalogEntry) map[string]meshcatal return out } +// withCallsLeftOut gives each entry the seat call words left out on a cycle (design 47 G A-361, A-357; ADR 0614 §3): the +// module graph's LeaveOut is run over the entries with a package's seat call words (seatCallsOf) counted among the invokes of +// every module built against it, and what it leaves out joins each entry's LeftOut, so a call word's edge on a cycle is +// left out like an invoked verb's, and so is an invoked verb's edge that a call word's edge closes a cycle with. The entries +// given are not changed; with no seat call word anywhere they are returned as they are. +func withCallsLeftOut(entries map[string]meshcatalog.CatalogEntry) map[string]meshcatalog.CatalogEntry { + g, any := graph.Graph{}, false + for module, e := range entries { + calls := seatCallsOf(e, entries) + any = any || len(calls) > 0 + invokes := slices.Clone(e.Invokes) + for _, c := range calls { + if !slices.Contains(invokes, c) { + invokes = append(invokes, c) + } + } + g[module] = graph.Node{Bases: e.Bases, Awaits: e.Awaits, Provides: e.Provides, Invokes: invokes, Serves: e.Serves} + } + if !any { + return entries + } + g.LeaveOut() + out := make(map[string]meshcatalog.CatalogEntry, len(entries)) + for module, e := range entries { + for _, v := range g[module].LeftOut { + if !slices.Contains(e.LeftOut, v) { + e.LeftOut = append(slices.Clone(e.LeftOut), v) + } + } + out[module] = e + } + return out +} + +// seatCallsOf are the call words in the invokes form, `seat:.`, of the package artifacts entry a builds +// against (their `calls`), sorted, each once. +func seatCallsOf(a meshcatalog.CatalogEntry, all map[string]meshcatalog.CatalogEntry) []string { + var out []string + for _, name := range slices.Sorted(maps.Keys(a.Packages)) { + for _, m := range slices.Sorted(maps.Keys(all)) { + for _, art := range all[m].Artifacts { + if art.Package == nil || art.Package.Name != name { + continue + } + for _, word := range wordsOf(all[m], art.Ref, "calls") { + if _, _, isSeat := catalogue.SeatInvoke(word); isSeat && !slices.Contains(out, word) { + out = append(out, word) + } + } + } + } + } + slices.Sort(out) + 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} diff --git a/modules/mesh-delivery-indexer/internal/index/catalog_edges_test.go b/modules/mesh-delivery-indexer/internal/index/catalog_edges_test.go index cf10ec52..07f9d977 100644 --- a/modules/mesh-delivery-indexer/internal/index/catalog_edges_test.go +++ b/modules/mesh-delivery-indexer/internal/index/catalog_edges_test.go @@ -122,6 +122,47 @@ func TestAModuleBuiltOnAPackageThatCallsTheToolRunnerIsOrderedAfterTheArtifactTh } } +// Design 47 G A-361: a package's seat call words count among the invokes of every module built against it when the graph +// leaves out the edges on a cycle, so a call word's edge on a cycle is left out like an invoked verb's (the gate and the +// indexer: the gate invokes the indexer's verb, the indexer's SDK calls the gate's), and an edge off the cycle stays. +func TestASeatCallWordsEdgeOnACycleIsLeftOutLikeAnInvokedVerbs(t *testing.T) { + sdk := map[string]any{"module": "sdk", "build": map[string]any{"artifacts": []any{map[string]any{"name": "go", + "calls": []any{"seat:mesh-delivery.source", "seat:mesh-catalog.registrations"}}}}} + pkgs := map[string]string{"git.example/sdk/go": "1.0.4"} + 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: sdk}, + "gate": {Module: "gate", Artifacts: []meshcatalog.Artifact{{Ref: "gate:program", Language: "go"}}, Serves: []string{"seat:mesh-delivery.source"}, + Invokes: []string{"seat:mesh-catalog.registrations"}}, + "indexer": {Module: "indexer", Artifacts: []meshcatalog.Artifact{{Ref: "indexer:program", Language: "go"}}, Serves: []string{"seat:mesh-catalog.registrations"}, + Packages: pkgs}, + "app": {Module: "app", Artifacts: []meshcatalog.Artifact{{Ref: "app:program", Language: "go"}}, Packages: pkgs}, + } + got := WithEdges(entries) + for _, m := range []string{"gate", "indexer"} { + for _, e := range got[m].Edges { + if e.Kind == 6 { + t.Errorf("%s keeps the edge %+v, which lies on the cycle gate-indexer", m, e) + } + } + } + if !slices.Contains(got["indexer"].LeftOut, "seat:mesh-delivery.source") || !slices.Contains(got["gate"].LeftOut, "seat:mesh-catalog.registrations") { + t.Errorf("left out: indexer %v, gate %v, want each one's word on the cycle", got["indexer"].LeftOut, got["gate"].LeftOut) + } + var six []meshcatalog.ArtifactRef + for _, e := range got["app"].Edges { + if e.Kind == 6 { + six = append(six, e.From) + } + } + if !slices.Equal(six, []meshcatalog.ArtifactRef{"gate:program", "indexer:program"}) || len(got["app"].LeftOut) != 0 { + t.Errorf("app's edges of kind 6 come from %v, left out %v, want both servers and none left out", six, got["app"].LeftOut) + } + if len(entries["gate"].LeftOut) != 0 { + t.Error("the entries given were changed") + } +} + func actingEntries() map[string]meshcatalog.CatalogEntry { rule := func(seat, step, on string) meshcatalog.ActingRule { return meshcatalog.ActingRule{Seat: seat, Step: step, On: on}