diff --git a/modules/mesh-delivery-assigner/manifest_test.go b/modules/mesh-delivery-assigner/manifest_test.go index c1ceeaea..856f996a 100644 --- a/modules/mesh-delivery-assigner/manifest_test.go +++ b/modules/mesh-delivery-assigner/manifest_test.go @@ -126,7 +126,8 @@ func TestManifest(t *testing.T) { if !slices.Equal(m.Consumes, []string{"mesh-catalog.seat-definition-changed", "mesh-catalog.seat-definition-removed", "mesh-delivery-indexer.views.graph", "mesh-delivery-indexer.views.manifests", "mesh-delivery-indexer.views.modules", buckets.TellerModule + "." + tellerevents.AskEnded, "node-engine.manifest-reads-changed"}) { t.Errorf("consumes = %v, want the indexer's views of graph, manifests and modules, the teller's ask-ended and the engines' reads changed", m.Consumes) } - if want := []string{"mesh-delivery-indexer.graph", "mesh-delivery-indexer.manifests", "mesh-delivery-indexer.modules", + if want := []string{"mesh-delivery-indexer.graph", "mesh-delivery-indexer.manifests", + "seat:mesh-catalog.revision", // ADR 0663 §2: which modules exist "seat:mesh-catalog.seat-definitions", // ADR 0636 §4: the seat list "seat:mesh-hand-acts.hand-act", "seat:mesh-hand-acts.hand-acts", "seat:mesh-hand-acts.warranted", // ADR 0565 §9, §11: the seat alone "seat:node-backup.backed-up", "seat:node-backup.now", diff --git a/modules/mesh-delivery-assigner/module.json b/modules/mesh-delivery-assigner/module.json index e594b25c..729c8655 100644 --- a/modules/mesh-delivery-assigner/module.json +++ b/modules/mesh-delivery-assigner/module.json @@ -50,7 +50,7 @@ "invokes": [ "mesh-delivery-indexer.graph", "mesh-delivery-indexer.manifests", - "mesh-delivery-indexer.modules", + "seat:mesh-catalog.revision", "seat:mesh-catalog.seat-definitions", "seat:mesh-hand-acts.hand-act", "seat:mesh-hand-acts.hand-acts", diff --git a/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer-tools/main.go b/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer-tools/main.go index 2889ddc7..7b2ba739 100644 --- a/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer-tools/main.go +++ b/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer-tools/main.go @@ -283,8 +283,12 @@ func main() { acts := meshhandactsclient.Recorder{Ask: stdio.Ask} tools := Tools(asks, time.Now, stdio.State(index.VerdictsState), heldCounts(stdio.State(index.OldSectionsState)), acts) // The modules and the module graph are the newest catalog graph revision's (milestone 47/G): revision.go. - trunk := index.TrunkStates{Objects: bundleObjects{busstate.ObjectsOf(index.CatalogRevisionsObjects)}, Under: keptSource} + revisions := bundleObjects{busstate.ObjectsOf(index.CatalogRevisionsObjects)} + trunk := index.TrunkStates{Objects: revisions, Under: keptSource} tools = append(tools, catalogTools(trunk.State)...) + // The seat mesh-catalog's verb revision, a first view of the catalog half answered in this runtime of its own to exactly + // the parts whose entry declares it (novox/hq ADR 0663). + tools = append(tools, index.RevisionTool(revisions, index.Module)) tools = append(tools, viewTools(os.Getenv, trunk.State, time.Now)...) tools = append(tools, index.PendingTools(keptRead{stdio.State(index.PendingState)}, keptRead{stdio.State(index.OwnBuildStateName)}, asks, time.Now)...) tools = append(tools, index.FingerprintsTools(asks, time.Now, func(what, why string) error { diff --git a/modules/mesh-delivery-indexer/internal/index/revision_verb.go b/modules/mesh-delivery-indexer/internal/index/revision_verb.go new file mode 100644 index 00000000..82ffac84 --- /dev/null +++ b/modules/mesh-delivery-indexer/internal/index/revision_verb.go @@ -0,0 +1,102 @@ +package index + +import ( + "context" + "encoding/json" + "fmt" + "slices" + "strconv" + "strings" + + "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" + stdio "git.novox.be/novox/mesh-sdk/go" + "git.novox.be/novox/mesh-sdk/go/views" +) + +// RevisionInvoke is how a part declares the seat mesh-catalog's verb revision in its manifest's invokes, the one way a +// part reads the catalog half (novox/hq ADR 0663 §2). +const RevisionInvoke = "seat:" + meshcatalog.Seat + "." + meshcatalog.VerbRevision + +// RevisionCallers are the parts the newest trunk revision's entries say declare the verb revision in their invokes, +// sorted, the holder itself never among them (ADR 0663 §2: granted to exactly the parts that declare it). +func RevisionCallers(entries []meshcatalog.CatalogEntry, holder string) stdio.Callers { + var out stdio.Callers + for _, e := range entries { + if e.Module != holder && slices.Contains(e.Invokes, RevisionInvoke) { + out = append(out, e.Module) + } + } + slices.Sort(out) + return out +} + +// Revision answers the seat mesh-catalog's verb revision from the indexer's own objects (ADR 0663 §1, §3): one catalog +// graph revision, the trunk's newest when the input names none, a kept trunk revision by its number, or a candidate's by +// its group and heads, with one page of its entries whole, by module name. The caller is the runtime's proof: it is +// answered only when the newest trunk revision's entry of its module declares the verb in invokes, and the holder is +// refused too. With no trunk revision kept yet nobody is answered and the reader waits (ErrNoRevision), never an empty +// catalog. +func Revision(ctx context.Context, o Objects, holder, caller string, in meshcatalog.RevisionInput) (meshcatalog.RevisionAnswer, error) { + var out meshcatalog.RevisionAnswer + newest, ok, err := newestTrunk(ctx, o) + if err != nil { + return out, fmt.Errorf("the catalog graph revisions cannot be read: %w", err) + } + if !ok { + return out, ErrNoRevision + } + _, trunkEntries, err := ReadRevision(ctx, o, RevisionObject(newest)) + if err != nil { + return out, err + } + if err := views.Refusal(meshcatalog.Seat+"."+meshcatalog.VerbRevision, RevisionCallers(trunkEntries, holder), caller); err != nil { + return out, fmt.Errorf("%w (novox/hq ADR 0663 §2: a part reads it by declaring %s in invokes)", err, RevisionInvoke) + } + name := RevisionObject(newest) + switch { + case in.Group != "" || in.Heads != "": + if in.Group == "" || in.Heads == "" { + return out, fmt.Errorf("a candidate's revision is named by its group and its heads, both") + } + name = RevisionObject(meshcatalog.CatalogRevision{Kind: "candidate", Group: in.Group, Heads: in.Heads}) + case in.Revision != 0 && in.Revision != newest.Revision: + name = "trunk." + strconv.FormatInt(in.Revision, 10) + } + rev, entries := newest, trunkEntries + if name != RevisionObject(newest) { + if rev, entries, err = ReadRevision(ctx, o, name); err != nil { + return out, err + } + if in.Revision != 0 && rev.Revision != in.Revision { + return out, fmt.Errorf("the candidate %s.%s keeps its newest revision only, %d, not %d", in.Group, in.Heads, rev.Revision, in.Revision) + } + } + slices.SortFunc(entries, func(a, b meshcatalog.CatalogEntry) int { return strings.Compare(a.Module, b.Module) }) + out.Revision, out.Entries = rev, []meshcatalog.CatalogEntry{} + size := 0 + for _, e := range entries { + if e.Module <= in.After { + continue + } + data, _ := json.Marshal(e) + if len(out.Entries) > 0 && size+len(data) > views.PageBytes { + out.Next = out.Entries[len(out.Entries)-1].Module + break + } + size += len(data) + out.Entries = append(out.Entries, e) + } + return out, nil +} + +// RevisionTool is the verb revision as the tool mesh-catalog.revision of the indexer's own runtime (ADR 0663 §1): the +// tool runner describes it with the seat's definition, so it carries no description or input of its own. +func RevisionTool(o Objects, holder string) stdio.Tool { + return stdio.Tool{Name: meshcatalog.Seat + "." + meshcatalog.VerbRevision, Run: func(args map[string]any) (any, error) { + var in meshcatalog.RevisionInput + if raw, err := json.Marshal(args); err == nil { + _ = json.Unmarshal(raw, &in) + } + return Revision(context.Background(), o, holder, stdio.CallerOf(args), in) + }} +} diff --git a/modules/mesh-delivery-indexer/internal/index/toolchains_test.go b/modules/mesh-delivery-indexer/internal/index/toolchains_test.go index d20d2798..5de024cf 100644 --- a/modules/mesh-delivery-indexer/internal/index/toolchains_test.go +++ b/modules/mesh-delivery-indexer/internal/index/toolchains_test.go @@ -549,6 +549,44 @@ func TestBasesAreComputedFirstAndNoFingerprintTakesTheBuilder(t *testing.T) { // if _, err := (TrunkStates{Objects: newMemObjects(0)}).State("graph").Keys(); !errors.Is(err, ErrNoRevision) { t.Errorf("the bundle's graph with no revision kept answered %v, want it to wait", err) } + // W4.4, ADR 0663: the seat mesh-catalog's verb revision answers, from the indexer's own objects, exactly the parts whose + // entry in the newest trunk revision declares it, the holder refused too; a candidate's by its group and heads; none + // kept, everyone waits. + verbObjects := newMemObjects(0) + keep := func(rev meshcatalog.CatalogRevision, declares map[string]bool) { + entries := map[string][]byte{} + for _, name := range []string{"avahi", Module, "mesh-grants", "node-tools"} { + e := meshcatalog.CatalogEntry{Module: name, Repository: catalogueRepo} + if declares[name] { + e.Invokes = []string{RevisionInvoke} + } + data, _ := json.Marshal(e) + digest := fmt.Sprintf("%s-%d-%s", rev.Kind, rev.Revision, name) + entries[digest] = data + rev.Entries = append(rev.Entries, meshcatalog.EntryRef{Module: name, Digest: digest}) + } + if err := KeepRevision(context.Background(), verbObjects, rev, entries); err != nil { + t.Fatal(err) + } + } + if _, err := Revision(context.Background(), verbObjects, Module, "n1.node-tools", meshcatalog.RevisionInput{}); !errors.Is(err, ErrNoRevision) { + t.Errorf("revision with none kept answered %v, want it to wait", err) + } + keep(meshcatalog.CatalogRevision{Revision: 3, Kind: "trunk"}, map[string]bool{"node-tools": true, Module: true}) + keep(meshcatalog.CatalogRevision{Revision: 4, Kind: "candidate", Group: "feat/x", Heads: "0123456789ab"}, map[string]bool{"node-tools": true, "mesh-grants": true}) + for caller, answered := range map[string]bool{"n1.node-tools": true, "n1.mesh-grants": false, "n1." + Module: false, "person.someone": false, "unproven": false} { + got, err := Revision(context.Background(), verbObjects, Module, caller, meshcatalog.RevisionInput{}) + if (err == nil) != answered || (answered && (got.Revision.Revision != 3 || len(got.Entries) != 4 || got.Next != "")) { + t.Errorf("revision to %s answered %+v (%v), want answered %v with trunk revision 3 whole", caller, got, err, answered) + } + } + if got, err := Revision(context.Background(), verbObjects, Module, "n1.node-tools", meshcatalog.RevisionInput{Group: "feat/x", Heads: "0123456789ab", After: "mesh-grants"}); err != nil || + got.Revision.Kind != "candidate" || len(got.Entries) != 1 || got.Entries[0].Module != "node-tools" { + t.Errorf("a candidate's revision after mesh-grants answered %+v (%v), want its one entry node-tools", got, err) + } + if _, err := Revision(context.Background(), verbObjects, Module, "n1.node-tools", meshcatalog.RevisionInput{Revision: 9}); err == nil { + t.Error("a trunk revision not kept was answered") + } // ADR 0614 §2, §3: the graph names what each module invokes and serves, and leaves out, over the whole graph, the // verb edges of two modules calling each other, from either side; a third calling one of them keeps its edge. invoking := func(name, invokes string) mod { diff --git a/modules/mesh-delivery-indexer/internal/index/views.go b/modules/mesh-delivery-indexer/internal/index/views.go index 88bebe00..a428ba9d 100644 --- a/modules/mesh-delivery-indexer/internal/index/views.go +++ b/modules/mesh-delivery-indexer/internal/index/views.go @@ -11,10 +11,11 @@ import ( const MarksBucket = Module + "_" + views.MarksState // 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 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. var FirstViewTools = []ViewedTool{ {Tool: "manifests", State: "manifests", Words: "every manifest the indexer keeps, by its digest."}, - {Tool: "modules", State: "modules", 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 25a07565..c94f7d6d 100644 --- a/modules/mesh-delivery-indexer/manifest_test.go +++ b/modules/mesh-delivery-indexer/manifest_test.go @@ -175,7 +175,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: the first-view tools manifests, modules, verdicts and graph, and design 25 § 12.2's read + // 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 // 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...) { @@ -189,12 +190,13 @@ func TestManifestDeclarations(t *testing.T) { // 1238/AC1, 1444 (reads the build index.SeatDefinitionTool, index.SeatDefinitionsAgainTool, index.OutOfStepTool, // DLV-I19 index.HoldsTool, index.EngineReadsTool, // ADR 0632 §2: the mesh-wide holds and the engines' reads they weigh index.PendingTool, index.TakeInPendingTool, index.DropPendingTool, index.TryCandidatesTool, index.DropTryCandidateTool, index.RecoverTriesTool, index.TakeInMergeTool, index.FingerprintsAgainTool) // issue 1966; 1585/T12, T13 (ADR 0633 §5) - if !reflect.DeepEqual(m.Tools, want) || len(want) != 40 { + if !reflect.DeepEqual(m.Tools, want) || len(want) != 40 || slices.Contains(m.Tools, "modules") { t.Errorf("tools = %v, want %v", m.Tools, want) } // 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). readers, _ := filepath.Glob("../*/module.json") + var declaring []string for _, p := range readers { raw, err := os.ReadFile(p) if err != nil { @@ -207,24 +209,36 @@ func TestManifestDeclarations(t *testing.T) { // 1238/AC1, 1444 (reads the build t.Fatalf("%s: %v", p, err) } for _, inv := range r.Invokes { + // The grants part reads modules until its own subtask (W8) moves it to revision; the join removes this line. + if p == "../mesh-grants/module.json" && inv == "mesh-delivery-indexer.modules" { + continue + } if tool, ok := strings.CutPrefix(inv, "mesh-delivery-indexer."); ok && !slices.Contains(m.Tools, tool) { t.Errorf("%s invokes %s, which the indexer does not serve", p, inv) } + if inv == index.RevisionInvoke { + declaring = append(declaring, filepath.Base(filepath.Dir(p))) + } } } + // ADR 0663 §2: the assigner reads which modules exist by declaring the seat's verb revision (the grants part too, from W8). + if !slices.Contains(declaring, "mesh-delivery-assigner") { + t.Errorf("the parts of this repository declaring %s are %v, want the assigner", index.RevisionInvoke, declaring) + } if m.RunsAs != m.Module || m.SecretsOwner != m.RunsAs { t.Errorf("runs-as = %q, secrets-owner %q: the tools bundle runs on the indexer's own account (ADR 0621 §1)", m.RunsAs, m.SecretsOwner) } // 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. - callers := map[string]string{"manifests": "mesh-grants,node-tools,mesh-delivery-assigner", "modules": "mesh-grants,node-tools,mesh-delivery-assigner", "verdicts": "mesh-grants", + // 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", "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 four callers lists", a.Env) + t.Errorf("tools env = %v, want only the three callers lists", a.Env) } for tool, list := range callers { if got := a.Env[views.CallersEnv(tool)]; got != list { @@ -254,10 +268,10 @@ func TestManifestDeclarations(t *testing.T) { // 1238/AC1, 1444 (reads the build } } // ADR 0604 §2, ADR 0636 §4: it holds the seat mesh-catalog, serving its four verbs and seat-definitions from its tools - // bundle (which stays without a request subject). + // bundle (which stays without a request subject); ADR 0663 §1: and revision, in the same runtime of its own. if len(m.Claims) != 1 || m.Claims[0].Name != index.CatalogSeat || m.Claims[0].Scope != catalogue.ScopeMesh || - !slices.Equal(m.Claims[0].Serves, []string{"modules", "module", "provides", "dependents", index.SeatDefinitionsVerb}) { - t.Errorf("claims = %+v, want mesh-catalog with its five verbs", m.Claims) + !slices.Equal(m.Claims[0].Serves, []string{"modules", "module", "provides", "dependents", index.SeatDefinitionsVerb, "revision"}) { + t.Errorf("claims = %+v, want mesh-catalog with its six verbs", m.Claims) } for _, e := range []string{index.SeatDefinitionChangedEvent, index.SeatDefinitionRemovedEvent} { if slices.Contains(m.Emits, e) { diff --git a/modules/mesh-delivery-indexer/module.json b/modules/mesh-delivery-indexer/module.json index ef26c524..b928b5e3 100644 --- a/modules/mesh-delivery-indexer/module.json +++ b/modules/mesh-delivery-indexer/module.json @@ -12,7 +12,8 @@ "module", "provides", "dependents", - "seat-definitions" + "seat-definitions", + "revision" ] } ], @@ -22,7 +23,6 @@ "indexer_verdicts", "indexer_judge", "manifests", - "modules", "verdicts", "graph", "indexer_read_manifests", @@ -195,7 +195,6 @@ ], "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" }