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/catalog_edges.go b/modules/mesh-delivery-indexer/internal/index/catalog_edges.go index 12b65693..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} @@ -265,6 +322,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 +331,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..07f9d977 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,15 +109,60 @@ 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) } } +// 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} 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..16327b47 100644 --- a/modules/mesh-delivery-indexer/internal/index/views.go +++ b/modules/mesh-delivery-indexer/internal/index/views.go @@ -1,15 +1,22 @@ 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 -// 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."}, } @@ -39,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 7530e03a..fa439b68 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 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). @@ -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 { @@ -310,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 f6dedfc9..c9102662 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", @@ -150,6 +151,7 @@ "views.graph", "views.heard", "views.manifests", + "views.modules", "views.packages", "views.predefined", "views.seat-definitions", @@ -244,6 +246,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" } 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