From 4c112de4d5dc009eff5dd424bedc95c3f1d4ce48 Mon Sep 17 00:00:00 2001 From: jochen Date: Fri, 13 Nov 2026 17:28:04 +0100 Subject: [PATCH 1/5] A-355: the watchman requires and replaces the build seats it imports cmd/mesh-delivery-watchman imports the seats nodebuildimage and nodebuildnode, but its go.mod named only nodebuildgo, so the watchman did not build from main. Every G part builds from main before any is placed. Builds: design 47 G A-355 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h --- modules/mesh-delivery-watchman/go.mod | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/modules/mesh-delivery-watchman/go.mod b/modules/mesh-delivery-watchman/go.mod index 5da8d8f4..b8d2e875 100644 --- a/modules/mesh-delivery-watchman/go.mod +++ b/modules/mesh-delivery-watchman/go.mod @@ -58,3 +58,12 @@ require ( require git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/nodebuildgo v0.0.0-00010101000000-000000000000 replace git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/nodebuildgo => ../mesh-delivery-contracts/go/seats/nodebuildgo + +require ( + git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/nodebuildimage v0.0.0-00010101000000-000000000000 + git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/nodebuildnode v0.0.0-00010101000000-000000000000 +) + +replace git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/nodebuildimage => ../mesh-delivery-contracts/go/seats/nodebuildimage + +replace git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/nodebuildnode => ../mesh-delivery-contracts/go/seats/nodebuildnode From a015b5ed245a376ec549064d8813203f490584f4 Mon Sep 17 00:00:00 2001 From: jochen Date: Fri, 13 Nov 2026 17:30:39 +0100 Subject: [PATCH 2/5] A-354: the indexer serves the first view modules again, for the grants part The grants part reads mesh-delivery-indexer.modules as one of its five first views; G's indexer stopped serving it (deb88b17), so the read is a failed gap and grants issues no new right once G's indexer runs. The tool comes back in its form before G (the first-view tool over the state modules, its callers list), answered from the newest trunk revision, until grants reads the catalog half. Builds: design 47 G A-354 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h --- .../internal/index/toolchains_test.go | 11 +++++++++++ .../internal/index/views.go | 8 +++++--- .../mesh-delivery-indexer/manifest_test.go | 19 +++++++++---------- modules/mesh-delivery-indexer/module.json | 2 ++ 4 files changed, 27 insertions(+), 13 deletions(-) diff --git a/modules/mesh-delivery-indexer/internal/index/toolchains_test.go b/modules/mesh-delivery-indexer/internal/index/toolchains_test.go index 0d54e4d1..67fca1d2 100644 --- a/modules/mesh-delivery-indexer/internal/index/toolchains_test.go +++ b/modules/mesh-delivery-indexer/internal/index/toolchains_test.go @@ -594,6 +594,17 @@ func TestBasesAreComputedFirstAndNoFingerprintTakesTheBuilder(t *testing.T) { // if keys, err := trunkStates.State("modules").Keys(); err != nil || len(keys) != len(builderModules(t)) { t.Errorf("the bundle's modules = %v (%v), want every module of revision 2", keys, err) } + // Design 47 G A-354: the first view modules answers the grants part every module of the newest trunk revision in the + // form it reads (meshcatalogclient's value, naming the manifest), at that revision. + if !slices.Contains(FirstViewTools, ViewedTool{Tool: "modules", State: ModulesState, Words: "every module the indexer holds, its fingerprint, inputs and manifest digest."}) { + t.Errorf("the first-view tools %+v serve no modules", FirstViewTools) + } + if a, err := views.FirstView(testSelf, ModulesState, trunkStates.State(ModulesState), trunkStates.State(views.MarksState), "", time.Now()); err != nil || + a.Revision != 2 || len(a.Values) != len(builderModules(t)) { + t.Errorf("the first view modules answered %d values at %d (%v), want every module at revision 2", len(a.Values), a.Revision, err) + } else if m, err := meshcatalogclient.DecodeModule(a.Values[viewKey]); err != nil || m.Manifest == "" { + t.Errorf("the first view's avahi = %s (%v), want the module's value naming its manifest", a.Values[viewKey], err) + } if e, ok, err := trunkStates.State(views.MarksState).Get("graph"); err != nil || !ok || !strings.Contains(string(e.Value), `"revision":2`) { t.Errorf("the bundle's mark of graph = %+v %v (%v), want revision 2", e, ok, err) } diff --git a/modules/mesh-delivery-indexer/internal/index/views.go b/modules/mesh-delivery-indexer/internal/index/views.go index afe6ed78..93837615 100644 --- a/modules/mesh-delivery-indexer/internal/index/views.go +++ b/modules/mesh-delivery-indexer/internal/index/views.go @@ -5,11 +5,13 @@ import ( ) // FirstViewTools are the indexer's first-view tools (ADR 0621 §1, ADR 0622 §1, ADR 0631 §1): the tool, the state it answers, and -// the words of what that state holds. Each answers only its callers list (CallersEnv), which the manifest writes. The fourth -// first view, of which modules exist, is the seat mesh-catalog's verb revision (ADR 0663 §1, RevisionTool), granted by -// declaration, not by a list. +// the words of what that state holds. Each answers only its callers list (CallersEnv), which the manifest writes. Which +// modules exist is the seat mesh-catalog's verb revision (ADR 0663 §1, RevisionTool), granted by declaration, not by a +// list; the first view modules stays beside it, in its form before milestone G and answered from the newest trunk +// revision (TrunkStates), until the grants part reads the catalog half (design 47 G A-354). var FirstViewTools = []ViewedTool{ {Tool: "manifests", State: "manifests", Words: "every manifest the indexer keeps, by its digest."}, + {Tool: "modules", State: ModulesState, Words: "every module the indexer holds, its fingerprint, inputs and manifest digest."}, {Tool: "verdicts", State: VerdictsState, Words: "the judge's verdict on each manifest: accepted, waiting or refused."}, {Tool: "graph", State: graph.State, Words: "the module graph: each module's build, provision and verb edges, whose verb edges a want waits on."}, } diff --git a/modules/mesh-delivery-indexer/manifest_test.go b/modules/mesh-delivery-indexer/manifest_test.go index 7530e03a..f29ca43e 100644 --- a/modules/mesh-delivery-indexer/manifest_test.go +++ b/modules/mesh-delivery-indexer/manifest_test.go @@ -187,8 +187,8 @@ func TestManifestDeclarations(t *testing.T) { // 1238/AC1, 1444 (reads the build // 1945: indexer_take_in, served by its tools bundle through the takeins state; the process serves none. The same bundle // reads the verdicts state (indexer_verdicts) and asks the indexer to judge again through the takeins state // (indexer_judge; issue 774, clause 3). 1966/AC11: indexer_old_sections reads the count of the old sections (state oldsections). - // ADR 0621 §1, ADR 0622 §1, ADR 0631 §1, ADR 0663 §1: the first-view tools manifests, verdicts and graph (modules is the seat - // verb revision's, served by the claim), and design 25 § 12.2's read + // ADR 0621 §1, ADR 0622 §1, ADR 0631 §1, ADR 0663 §1: the first-view tools manifests, modules, verdicts and graph (modules + // kept for the grants part until it reads the catalog half, design 47 G A-354), and design 25 § 12.2's read // tools, all of the tools bundle, which runs in a runtime of its own on the indexer's account (runs-as). want := []string{"indexer_take_in", "indexer_old_sections", "indexer_verdicts", "indexer_judge"} for _, v := range append(append([]index.ViewedTool{}, index.FirstViewTools...), index.ReadTools...) { @@ -206,7 +206,7 @@ func TestManifestDeclarations(t *testing.T) { // 1238/AC1, 1444 (reads the build index.EntryIndexTool, index.NamedDropTool, // design 47 G §675 index.ProposalsTool, index.ProposalDropTool, index.SeedAgainTool, index.InputsTool, index.PinsTool, index.UnpinTool, index.ConsumerTool, index.RunsTool, index.RunDropTool, // design 47 G §667, §8, §579, §64 (A-194) index.ValidateAgainTool) // design 47 G §713 - if !reflect.DeepEqual(m.Tools, want) || len(want) != 50 || slices.Contains(m.Tools, "modules") { + if !reflect.DeepEqual(m.Tools, want) || len(want) != 51 || !slices.Contains(m.Tools, "modules") { t.Errorf("tools = %v, want %v", m.Tools, want) } // Design 47 G §2, §9: every version of every registration is kept with no age limit, 64 MiB, on the old streams section. @@ -219,8 +219,8 @@ func TestManifestDeclarations(t *testing.T) { // 1238/AC1, 1444 (reads the build if !regexp.MustCompile(`^[0-9]+\.[0-9]+$`).MatchString(m.Version) { t.Errorf("version = %q, want MAJOR.MINOR (design 47 G §368)", m.Version) } - // Design 47 G §196, §185 (test 61): no state modules, no tool or view of it, and no mesh-wide hold with its states, tools - // and the engines' reads it weighed. + // Design 47 G §196, §185 (test 61): no state modules, no read tool or view of it, and no mesh-wide hold with its states, + // tools and the engines' reads it weighed; its first view stays for the grants part (A-354, above). for _, s := range m.State { if slices.Contains([]string{"modules", "holds", "engine-reads", "hold-bounds"}, s.Name) { t.Errorf("the indexer declares the state %s, which design 47 G retires", s.Name) @@ -258,9 +258,7 @@ func TestManifestDeclarations(t *testing.T) { // 1238/AC1, 1444 (reads the build t.Fatalf("%s: %v", p, err) } for _, inv := range invoked { - // The grants part (W8) and the assigner read modules until their own subtasks move them to revision; the join removes this line. - if (p == "../mesh-grants/module.json" || p == "../mesh-delivery-assigner/module.json") && inv == "mesh-delivery-indexer.modules" || - inv == "mesh-delivery-indexer.indexer_read_modules" { // design 47 G §196: the readers' own subtasks move them to the catalog half + if inv == "mesh-delivery-indexer.indexer_read_modules" { // design 47 G §196: the readers' own subtasks move them to the catalog half continue } if tool, ok := strings.CutPrefix(inv, "mesh-delivery-indexer."); ok && !slices.Contains(m.Tools, tool) { @@ -281,14 +279,15 @@ func TestManifestDeclarations(t *testing.T) { // 1238/AC1, 1444 (reads the build // ADR 0621 §2, ADR 0622 §1, ADR 0631 §1: each first-view tool answers exactly its readers; the indexer is on none, and // graph answers the assigner alone. // ADR 0663 §2: revision has no callers list; it answers the parts whose entry declares it. - callers := map[string]string{"manifests": "mesh-grants,node-tools,mesh-delivery-assigner", "verdicts": "mesh-grants", + // Design 47 G A-354: modules answers its readers as before milestone G. + callers := map[string]string{"manifests": "mesh-grants,node-tools,mesh-delivery-assigner", "modules": "mesh-grants,node-tools,mesh-delivery-assigner", "verdicts": "mesh-grants", "graph": "mesh-delivery-assigner"} for _, a := range m.Build.Artifacts { if a.Name != "tools" { continue } if len(a.Env) != len(callers) { - t.Errorf("tools env = %v, want only the three callers lists", a.Env) + t.Errorf("tools env = %v, want only the four callers lists", a.Env) } for tool, list := range callers { if got := a.Env[views.CallersEnv(tool)]; got != list { diff --git a/modules/mesh-delivery-indexer/module.json b/modules/mesh-delivery-indexer/module.json index f6dedfc9..9f9b320c 100644 --- a/modules/mesh-delivery-indexer/module.json +++ b/modules/mesh-delivery-indexer/module.json @@ -32,6 +32,7 @@ "indexer_verdicts", "indexer_judge", "manifests", + "modules", "verdicts", "graph", "indexer_read_manifests", @@ -244,6 +245,7 @@ ], "env": { "MESH_VIEW_CALLERS_MANIFESTS": "mesh-grants,node-tools,mesh-delivery-assigner", + "MESH_VIEW_CALLERS_MODULES": "mesh-grants,node-tools,mesh-delivery-assigner", "MESH_VIEW_CALLERS_VERDICTS": "mesh-grants", "MESH_VIEW_CALLERS_GRAPH": "mesh-delivery-assigner" } From 359c37d17f570b81c11278ca0581be43e0a499f5 Mon Sep 17 00:00:00 2001 From: jochen Date: Fri, 13 Nov 2026 17:31:54 +0100 Subject: [PATCH 3/5] A-357: a package's seat-verb call word orders its users after the verb's server MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A package's `calls` names the seat verbs its code calls on a module's behalf in the invokes form; the indexer derives an edge of kind 6 from such a word against the verb's server, as from `invokes`, so every module built against the SDK is delivered after the running versions that serve its start. The path for stated capability words stays. Builds: design 47 G A-357, §47 (kind 6) Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h --- .../internal/index/catalog_edges.go | 9 +++++++++ .../internal/index/catalog_edges_test.go | 13 ++++++++++--- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/modules/mesh-delivery-indexer/internal/index/catalog_edges.go b/modules/mesh-delivery-indexer/internal/index/catalog_edges.go index 12b65693..0f6aaad5 100644 --- a/modules/mesh-delivery-indexer/internal/index/catalog_edges.go +++ b/modules/mesh-delivery-indexer/internal/index/catalog_edges.go @@ -265,6 +265,8 @@ func edgesOf(a meshcatalog.CatalogEntry, all map[string]meshcatalog.CatalogEntry } // 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). + // A call word in the invokes form, `seat:.` (a seat verb the package's code calls on the module's behalf), + // orders it after the verb's server, exactly as the same word in its `invokes` would (design 47 G A-357). for _, name := range slices.Sorted(maps.Keys(a.Packages)) { for _, m := range modules { for _, art := range all[m].Artifacts { @@ -272,6 +274,13 @@ func edgesOf(a meshcatalog.CatalogEntry, all map[string]meshcatalog.CatalogEntry continue } for _, word := range wordsOf(all[m], art.Ref, "calls") { + if _, _, isSeat := catalogue.SeatInvoke(word); isSeat && !slices.Contains(a.LeftOut, word) { + for _, server := range serving.Servers(word) { + if server != a.Module { + add(meshcatalog.Edge{Kind: 6, From: primaryOf(all[server]), To: delivers, Word: word, Why: "calls of " + name}) + } + } + } for _, other := range modules { for _, stating := range all[other].Artifacts { if other != a.Module && slices.Contains(wordsOf(all[other], stating.Ref, "states"), word) { 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 109ffcf2..cf10ec52 100644 --- a/modules/mesh-delivery-indexer/internal/index/catalog_edges_test.go +++ b/modules/mesh-delivery-indexer/internal/index/catalog_edges_test.go @@ -86,14 +86,17 @@ func TestAKindSixEdgeNamesTheFieldsALongFormInvokeRelies(t *testing.T) { } // 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. +// that `states` each word, by an edge of kind 6 naming the word; a module built against no such package has none. A-357: a +// call word in the invokes form orders it after the verb's server, as from `invokes`, and the server after none of its own. 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"}})}, + Manifest: artifactsOf("sdk", map[string]any{"name": "go", "calls": []any{"put-sessions", "finding-calls", "seat:mesh-catalog.registrations"}})}, + "catalog": {Module: "catalog", Artifacts: []meshcatalog.Artifact{{Ref: "catalog:program", Language: "go"}}, Serves: []string{"seat:mesh-catalog.registrations"}, + Packages: map[string]string{"git.example/sdk/go": "1.0.4"}}, "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"}}, @@ -106,10 +109,14 @@ func TestAModuleBuiltOnAPackageThatCallsTheToolRunnerIsOrderedAfterTheArtifactTh 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"}} + want := []meshcatalog.Edge{{Kind: 6, From: "catalog:program", To: "app:program", Word: "seat:mesh-catalog.registrations", Why: "calls of git.example/sdk/go"}, + {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 slices.ContainsFunc(got["catalog"].Edges, func(e meshcatalog.Edge) bool { return e.From == "catalog:program" }) { + t.Errorf("the verb's server is ordered after itself: %+v", got["catalog"].Edges) + } if len(got["other"].Edges) != 0 { t.Errorf("other builds on no such package, its edges %+v", got["other"].Edges) } From ec58535ca02ddd7aab00b8bf18168e81d83a0ba7 Mon Sep 17 00:00:00 2001 From: jochen Date: Fri, 13 Nov 2026 17:33:57 +0100 Subject: [PATCH 4/5] A-360: the indexer says each new trunk revision's change of modules on views.modules The grants part follows the first view modules on the indexer's views family after its first ask. G's indexer keeps no state modules, so nothing said its changes and a newly merged module reached grants only when it asked again. A new trunk revision that changes the modules is now said as the mark of modules at that revision's number (the number the first view answers every value at), after which a reader short of it asks the first view once; views.modules is among the indexer's emits. Builds: design 47 G A-360, A-354 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h --- .../cmd/mesh-delivery-indexer/main.go | 3 +- .../mesh-delivery-indexer/modulesview_test.go | 56 +++++++++++++++++++ .../cmd/mesh-delivery-indexer/objects.go | 30 ++++++++++ .../internal/index/views.go | 21 +++++++ .../mesh-delivery-indexer/manifest_test.go | 9 +-- modules/mesh-delivery-indexer/module.json | 1 + 6 files changed, 115 insertions(+), 5 deletions(-) create mode 100644 modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/modulesview_test.go diff --git a/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/main.go b/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/main.go index 148243ce..f1aa923e 100644 --- a/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/main.go +++ b/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/main.go @@ -466,7 +466,8 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro SeatDefinitions: awaitedStore{b: seatDefs}, ReadFile: readFileVia(calls.Call), SaySeatDefinition: saySeats, Revisions: revisions, EntryIndex: awaitedStore{b: process.AwaitBucket(ctx, js, conn, index.Bucket(cred.Module, index.EntryIndexState), "no entry is found by its fingerprint", logger.Printf)}, // Message a kept call: a read, called again through a transport fault (ADR 0668 §1) // Each catalog revision may bring another holder of a build seat: the build modules are found again (buildmodules.go). - SayCatalogRevision: afterRevision(refreshingBuildModules(sayCatalogRevision(conn, cred), &buildFollow), &fillFollow), + // A new trunk revision that changes the modules is said on the views family of modules too (design 47 G A-360). + SayCatalogRevision: afterRevision(refreshingBuildModules(sayingModulesView(sayCatalogRevision(conn, cred), cred, views.JetStreamPublish(js)), &buildFollow), &fillFollow), SayFill: sayFill(conn, cred), // The catalog's released versions (design 47 G §349): the state versions, bound when it appears, and the event version-released. Versions: awaitedStore{b: process.AwaitBucket(ctx, js, conn, index.Bucket(cred.Module, index.VersionsState), "no released version is kept, so no package-published makes a version", logger.Printf)}, diff --git a/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/modulesview_test.go b/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/modulesview_test.go new file mode 100644 index 00000000..897504e3 --- /dev/null +++ b/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/modulesview_test.go @@ -0,0 +1,56 @@ +package main + +import ( + "context" + "errors" + "strings" + "testing" + "time" + + "github.com/nats-io/nats.go" + + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshcatalog" + "git.novox.be/novox/mesh-sdk/go/keys" + "git.novox.be/novox/mesh-sdk/go/views" + + "mesh-delivery-indexer/internal/index" +) + +// Design 47 G A-360 (completing A-354): a new trunk revision that changes the modules says one views event of modules, +// the mark at the revision's number, on the indexer's own event subject; a revision of another watched branch, one that +// changes nothing, and a publish that fails say so. +func TestANewTrunkRevisionChangingTheModulesSaysOneViewsEventOfModules(t *testing.T) { + at := time.Date(2026, 11, 13, 18, 0, 0, 0, time.UTC) + var sent []*nats.Msg + var ids []string + var fail error + said := 0 + say := sayingModulesView(func(context.Context, meshcatalog.CatalogRevisionEvent) error { said++; return nil }, testAccount, + func(_ context.Context, m *nats.Msg, id string) error { + sent, ids = append(sent, m), append(ids, id) + return fail + }) + trunk := keys.EscapeName(index.TrunkBranch) + for _, e := range []meshcatalog.CatalogRevisionEvent{ + {Revision: 7, Kind: "trunk", Branch: trunk, At: &at, Changed: []string{"avahi", "nats"}}, + {Revision: 8, Kind: "trunk", Branch: keys.EscapeName("feat/x"), At: &at, Changed: []string{"avahi"}}, + {Revision: 9, Kind: "candidate", Branch: trunk, At: &at, Changed: []string{"avahi"}}, + {Revision: 10, Kind: "trunk", Branch: trunk, At: &at}, + } { + if err := say(context.Background(), e); err != nil { + t.Fatalf("revision %d: %v", e.Revision, err) + } + } + if said != 4 || len(sent) != 1 || sent[0].Subject != testAccount.EventSubject(views.EventName("modules")) { + t.Fatalf("catalog-revision said %d times, views events %d (%v), want 4 and one on views.modules", said, len(sent), sent) + } + ev, err := views.DecodeEvent(sent[0].Data) + if err != nil || !ev.Mark || ev.Owner != testIndexer || ev.State != "modules" || ev.Revision != 7 || !ev.At.Equal(at) || ids[0] != ev.ID() { + t.Errorf("the views event of modules is %+v (%v), id %q, want the mark of modules at revision 7 under its own id", ev, err, ids[0]) + } + fail = errors.New("the bus refuses") + if err := say(context.Background(), meshcatalog.CatalogRevisionEvent{Revision: 11, Kind: "trunk", Branch: trunk, At: &at, Changed: []string{"nats"}}); err == nil || + !strings.Contains(err.Error(), "views event of modules at revision 11") { + t.Errorf("a views event not said answered %v, want it said", err) + } +} diff --git a/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/objects.go b/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/objects.go index 345d7996..459c2584 100644 --- a/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/objects.go +++ b/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/objects.go @@ -16,6 +16,7 @@ import ( "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/process" + "git.novox.be/novox/mesh-sdk/go/views" "mesh-delivery-indexer/internal/index" ) @@ -145,6 +146,35 @@ func sayCatalogRevision(conn *nats.Conn, cred process.Account) func(context.Cont } } +// sayingModulesView is say, and then, for a new trunk revision that changes the modules, the indexer's views event of +// modules (index.ModulesViewsEvent; design 47 G A-360) on its own event subject under the event's duplicate id, so the +// readers of the first view modules hear a newly merged module without asking again. One not said is returned with +// say's error, which the take-in logs. +func sayingModulesView(say func(context.Context, meshcatalog.CatalogRevisionEvent) error, cred process.Account, + publish func(context.Context, *nats.Msg, string) error) func(context.Context, meshcatalog.CatalogRevisionEvent) error { + return func(ctx context.Context, e meshcatalog.CatalogRevisionEvent) error { + err := say(ctx, e) + ev, ok := index.ModulesViewsEvent(cred.Module, e) + if !ok { + return err + } + data, encErr := ev.Encode() + if encErr == nil { + msg := nats.NewMsg(cred.EventSubject(views.EventName(index.ModulesState))) + msg.Header.Set("x-event-id", ev.ID()) + msg.Header.Set("x-source", cred.Module) + msg.Header.Set("x-time", time.Now().UTC().Format(time.RFC3339Nano)) + msg.Header.Set("content-type", "application/json") + msg.Data = data + encErr = publish(ctx, msg, ev.ID()) + } + if encErr != nil { + return errors.Join(err, fmt.Errorf("the views event of modules at revision %d is not said, its readers hold the revision before until the next: %w", e.Revision, encErr)) + } + return err + } +} + // sayFill publishes the seat mesh-catalog's event filling, full or room of the catalog graph revisions under the duplicate // id given (milestone 47/G W4.6), so one said twice is heard once. func sayFill(conn *nats.Conn, cred process.Account) func(context.Context, string, string, meshcatalog.Fill) error { diff --git a/modules/mesh-delivery-indexer/internal/index/views.go b/modules/mesh-delivery-indexer/internal/index/views.go index 93837615..16327b47 100644 --- a/modules/mesh-delivery-indexer/internal/index/views.go +++ b/modules/mesh-delivery-indexer/internal/index/views.go @@ -1,7 +1,12 @@ package index import ( + "time" + + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshcatalog" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-sdk/graph" + "git.novox.be/novox/mesh-sdk/go/keys" + "git.novox.be/novox/mesh-sdk/go/views" ) // FirstViewTools are the indexer's first-view tools (ADR 0621 §1, ADR 0622 §1, ADR 0631 §1): the tool, the state it answers, and @@ -41,3 +46,19 @@ var ViewedStates = map[string]bool{ ManifestsState: true, VerdictsState: true, PredefinedState: true, ConfigCandidatesState: true, CandidatesState: true, PackagesState: true, HeardState: true, TakenState: true, FindingsState: true, SeatDefinitionsState: true, UnjudgedState: true, } + +// ModulesViewsEvent is what the indexer says on its views family of modules when a new revision of the trunk branch +// changes the modules (design 47 G A-360, completing A-354): the first view modules answers every value at the trunk +// revision's number, so one revision's changes are said as one event, the mark at that number, after which a reader +// whose picture is short of it asks the first view once and holds the new revision whole. A revision of another watched +// branch, or one that changes nothing, says nothing (false). +func ModulesViewsEvent(owner string, e meshcatalog.CatalogRevisionEvent) (views.Event, bool) { + if e.Kind != "trunk" || e.Branch != keys.EscapeName(TrunkBranch) || e.Revision <= 0 || len(e.Changed) == 0 { + return views.Event{}, false + } + var at time.Time + if e.At != nil { + at = e.At.UTC() + } + return views.Event{V: views.Version, Owner: owner, State: ModulesState, Revision: uint64(e.Revision), At: at, Mark: true}, true +} diff --git a/modules/mesh-delivery-indexer/manifest_test.go b/modules/mesh-delivery-indexer/manifest_test.go index f29ca43e..fa439b68 100644 --- a/modules/mesh-delivery-indexer/manifest_test.go +++ b/modules/mesh-delivery-indexer/manifest_test.go @@ -220,7 +220,7 @@ func TestManifestDeclarations(t *testing.T) { // 1238/AC1, 1444 (reads the build t.Errorf("version = %q, want MAJOR.MINOR (design 47 G §368)", m.Version) } // Design 47 G §196, §185 (test 61): no state modules, no read tool or view of it, and no mesh-wide hold with its states, - // tools and the engines' reads it weighed; its first view stays for the grants part (A-354, above). + // tools and the engines' reads it weighed; its first view and its views family stay for the grants part (A-354, A-360). for _, s := range m.State { if slices.Contains([]string{"modules", "holds", "engine-reads", "hold-bounds"}, s.Name) { t.Errorf("the indexer declares the state %s, which design 47 G retires", s.Name) @@ -231,8 +231,8 @@ func TestManifestDeclarations(t *testing.T) { // 1238/AC1, 1444 (reads the build t.Errorf("the indexer serves %s, which design 47 G retires", retired) } } - if slices.Contains(m.Emits, "views.modules") || slices.Contains(m.Consumes, "node-engine.manifest-reads-changed") { - t.Errorf("the indexer says views.modules or hears manifest-reads-changed: emits %v, consumes %v", m.Emits, m.Consumes) + if slices.Contains(m.Consumes, "node-engine.manifest-reads-changed") { + t.Errorf("the indexer hears manifest-reads-changed: consumes %v", m.Consumes) } // Every read tool of the indexer a reader of this repository invokes is one it serves (the checker's seventh first // view, indexer_read_seat_definitions, was invoked and not served). @@ -309,7 +309,8 @@ func TestManifestDeclarations(t *testing.T) { // 1238/AC1, 1444 (reads the build // registrations' history (design 47 G §2, asserted above). // ADR 0636 §4: seat-definition-changed and -removed are the seat mesh-catalog's events, said on the seat's subject and kept // on the shared events stream, and not among its own emits. - for _, state := range []string{"candidates", "config-candidates", "findings", "graph", "heard", "manifests", "packages", "predefined", "seat-definitions", "taken", "verdicts"} { + // Design 47 G A-360: modules too, at each new trunk revision that changes it. + for _, state := range []string{"candidates", "config-candidates", "findings", "graph", "heard", "manifests", "modules", "packages", "predefined", "seat-definitions", "taken", "verdicts"} { if !slices.Contains(m.Emits, views.EventPrefix+state) { t.Errorf("emits = %v: want %s%s", m.Emits, views.EventPrefix, state) } diff --git a/modules/mesh-delivery-indexer/module.json b/modules/mesh-delivery-indexer/module.json index 9f9b320c..c9102662 100644 --- a/modules/mesh-delivery-indexer/module.json +++ b/modules/mesh-delivery-indexer/module.json @@ -151,6 +151,7 @@ "views.graph", "views.heard", "views.manifests", + "views.modules", "views.packages", "views.predefined", "views.seat-definitions", From 536c5732238e6f471b4ba379f003375de2e97e8e Mon Sep 17 00:00:00 2001 From: jochen Date: Fri, 13 Nov 2026 17:45:43 +0100 Subject: [PATCH 5/5] 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}