From ec58535ca02ddd7aab00b8bf18168e81d83a0ba7 Mon Sep 17 00:00:00 2001 From: jochen Date: Fri, 13 Nov 2026 17:33:57 +0100 Subject: [PATCH] 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",