diff --git a/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/main.go b/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/main.go index 258dc66..d795395 100644 --- a/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/main.go +++ b/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/main.go @@ -124,6 +124,24 @@ func Tools(r view.Reader, u view.UsageReader) []stdio.Tool { key, _ := args["key"].(string) return view.ControlRecord(r.ConfigCandidatesState, delivery.IndexerConfigCandidatesState, key) }}, + {Name: view.PackagesReadTool, + Description: "The mesh packages as the indexer publishes them (novox/hq ADR 0593 §2): per mesh Go module (its module " + + "path, such as git.novox.be/novox/mesh-sdk/go), the trunk merge that published it (the version a build pins), the " + + "digest of its go.mod and go.sum, and the content digest of each package in it. A module's fingerprint takes the " + + "digests of the packages its last build imported, so this says why a package merge moved a module or not. Give " + + "module for one. Read-only.", + Input: map[string]any{"type": "object", "properties": map[string]any{ + "module": map[string]any{"type": "string", "description": "one mesh Go module's path"}}}, + Run: func(args map[string]any) (any, error) { + key := "" + if module, _ := args["module"].(string); strings.TrimSpace(module) != "" { + var err error + if key, err = delivery.PackagesKey(strings.TrimSpace(module)); err != nil { + return nil, err + } + } + return view.ControlRecord(r.PackagesState, delivery.PackagesState, key) + }}, {Name: view.CandidatesReadTool, Description: "Every candidate the indexer holds, by group, each as stored under candidates.; with module, only those with a " + "pull request touching that module. Keys that cannot be read are listed as unreadable. Read-only.", @@ -288,6 +306,7 @@ func main() { PresenceState: read(stdio.State(view.StatusPresenceState)), StuckState: read(stdio.State(view.StatusStuckState)), PredefinedState: read(stdio.State(delivery.IndexerModule + "." + delivery.IndexerPredefinedState)), ConfigCandidatesState: read(stdio.State(delivery.IndexerModule + "." + delivery.IndexerConfigCandidatesState)), + PackagesState: read(stdio.State(delivery.IndexerModule + "." + delivery.PackagesState)), BuildLog: stdio.Log(delivery.BuildLogRead).Since} u := view.UsageReader{Haves: r.HavesState, Resolved: read(stdio.State("mesh-config.resolved")), Tries: read(stdio.State("mesh-delivery-checker.tries")), Steps: map[string]view.StateRead{}, Pauses: map[string]view.StateRead{}, Backlog: backlogOf(socket())} diff --git a/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/main_test.go b/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/main_test.go index bca6fc8..43364b3 100644 --- a/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/main_test.go +++ b/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/main_test.go @@ -548,3 +548,23 @@ func TestAssignerDifferencesAndNodeHavesToolsAnswerAsStored(t *testing.T) { t.Errorf("the values were read one at a time (at most %d in flight)", slow.maxFlight.Load()) } } + +// ADR 0593 §2 (D2): indexer_packages answers the indexer's published packages, one Go module by its path or all. +func TestPackagesToolAnswersThePublishedModules(t *testing.T) { + key, err := delivery.PackagesKey("git.novox.be/novox/mesh-sdk/go") + if err != nil { + t.Fatal(err) + } + const value = `{"v":1,"module":"git.novox.be/novox/mesh-sdk/go","commit":"c1"}` + r := view.Reader{PackagesState: fake{key: value}.reader().State} + out, err := run(t, r, view.PackagesReadTool, map[string]any{"module": "git.novox.be/novox/mesh-sdk/go"}) + if err != nil || string(out.(json.RawMessage)) != value { + t.Fatalf("one module: %v %v", out, err) + } + if all, err := run(t, r, view.PackagesReadTool, nil); err != nil || all.(map[string]any)["holds"] != 1 { + t.Errorf("every module: %v %v", all, err) + } + if _, err := run(t, r, view.PackagesReadTool, map[string]any{"module": "github.com/x/y"}); err == nil { + t.Error("a path that is no mesh Go module's was read") + } +} diff --git a/modules/mesh-delivery-view/internal/view/read.go b/modules/mesh-delivery-view/internal/view/read.go index fe28ea0..d9af163 100644 --- a/modules/mesh-delivery-view/internal/view/read.go +++ b/modules/mesh-delivery-view/internal/view/read.go @@ -53,6 +53,8 @@ type Reader struct { ConditionsState, AttemptsState, PresenceState, StuckState StateRead // PredefinedState and ConfigCandidatesState are the indexer's control records (ADR 0565 §2, §15); unset reads none. PredefinedState, ConfigCandidatesState StateRead + // PackagesState is the indexer's published packages (delivery.PackagesBucket, ADR 0593 §2); unset reads none. + PackagesState StateRead // BuildLog reads the builder's log from a time (stdio.Log(delivery.BuildLogRead).Since); unset reads none. BuildLog LogSince } @@ -64,6 +66,8 @@ type LogSince func(at time.Time, keys string, limit int) (*stdio.LogPage, error) const ( PredefinedReadTool = "indexer_predefined" ConfigCandidatesReadTool = "indexer_config_candidates" + // PackagesReadTool reads the indexer's published packages (ADR 0593 §2): what a fingerprint takes of a mesh package. + PackagesReadTool = "indexer_packages" ) // ControlRecord is every key of one of the indexer's control records as stored, or one key, with how many it holds. diff --git a/modules/mesh-delivery-view/manifest_test.go b/modules/mesh-delivery-view/manifest_test.go index 72c912c..eb5d1e2 100644 --- a/modules/mesh-delivery-view/manifest_test.go +++ b/modules/mesh-delivery-view/manifest_test.go @@ -87,7 +87,7 @@ func TestManifestIsTakenAndReadsOnly(t *testing.T) { // 1134/AC7 if m.Module != "mesh-delivery-view" { t.Errorf("module = %q", m.Module) } - want := []string{"mesh-build-go.pauses", "mesh-build-go.steps", "mesh-build-image.pauses", "mesh-build-image.steps", "mesh-build-node.pauses", "mesh-build-node.steps", "mesh-conditions.conditions", "mesh-config.resolved", "mesh-delivery-assigner.asks", "mesh-delivery-assigner.differences", "mesh-delivery-assigner.placement", "mesh-delivery-assigner.wants", "mesh-delivery-builder.build-log", "mesh-delivery-builder.builds", "mesh-delivery-checker.tries", "mesh-delivery-gate.asks", "mesh-delivery-gate.source", "mesh-delivery-indexer.candidates", "mesh-delivery-indexer.config-candidates", "mesh-delivery-indexer.heard", "mesh-delivery-indexer.modules", "mesh-delivery-indexer.predefined", "mesh-delivery-indexer.taken", "mesh-delivery-teller.answers", "mesh-delivery-watchman.presence", "mesh-delivery-watchman.stuck", "mesh-healers.attempts", "mesh-vault.signers", "node-engine.have"} // view_status reads the keeper's conditions and the healers' attempts (1952/V1); build_usage reads the build modules' steps and pauses, mesh-config's capacities and the checker's tries; 1168/AC1, 1241/AC4, 1345/AC3, 1297/B8, B9 + want := []string{"mesh-build-go.pauses", "mesh-build-go.steps", "mesh-build-image.pauses", "mesh-build-image.steps", "mesh-build-node.pauses", "mesh-build-node.steps", "mesh-conditions.conditions", "mesh-config.resolved", "mesh-delivery-assigner.asks", "mesh-delivery-assigner.differences", "mesh-delivery-assigner.placement", "mesh-delivery-assigner.wants", "mesh-delivery-builder.build-log", "mesh-delivery-builder.builds", "mesh-delivery-checker.tries", "mesh-delivery-gate.asks", "mesh-delivery-gate.source", "mesh-delivery-indexer.candidates", "mesh-delivery-indexer.config-candidates", "mesh-delivery-indexer.heard", "mesh-delivery-indexer.modules", "mesh-delivery-indexer.packages", "mesh-delivery-indexer.predefined", "mesh-delivery-indexer.taken", "mesh-delivery-teller.answers", "mesh-delivery-watchman.presence", "mesh-delivery-watchman.stuck", "mesh-healers.attempts", "mesh-vault.signers", "node-engine.have"} // view_status reads the keeper's conditions and the healers' attempts (1952/V1); build_usage reads the build modules' steps and pauses, mesh-config's capacities and the checker's tries; 1168/AC1, 1241/AC4, 1345/AC3, 1297/B8, B9 got := slices.Clone(m.Reads) slices.Sort(got) if !reflect.DeepEqual(got, want) { @@ -119,7 +119,7 @@ func TestManifestIsTakenAndReadsOnly(t *testing.T) { // 1134/AC7 } // The two read tools of the indexer's modules (issue 1562), carried by a bundle the runtime launches; the indexer // itself serves no tool (ADR 0437 §4). Carrying them gives the tool runner read access to every bucket the view reads. - if want := []string{"view_status", "indexer_module", "indexer_modules", "indexer_candidate", "indexer_candidates", "indexer_predefined", "indexer_config_candidates", "builder_build", "builder_builds", "builder_history", "assigner_wants", "assigner_want", "assigner_differences", "node_haves", "node_trusts", "build_usage", "view_reach", "indexer_module_history", "indexer_heard"}; !reflect.DeepEqual(m.Tools, want) { + if want := []string{"view_status", "indexer_module", "indexer_modules", "indexer_candidate", "indexer_candidates", "indexer_predefined", "indexer_config_candidates", "indexer_packages", "builder_build", "builder_builds", "builder_history", "assigner_wants", "assigner_want", "assigner_differences", "node_haves", "node_trusts", "build_usage", "view_reach", "indexer_module_history", "indexer_heard"}; !reflect.DeepEqual(m.Tools, want) { t.Errorf("tools = %v, want %v", m.Tools, want) } for _, a := range m.Build.Artifacts { diff --git a/modules/mesh-delivery-view/module.json b/modules/mesh-delivery-view/module.json index 6f16694..8ba2032 100644 --- a/modules/mesh-delivery-view/module.json +++ b/modules/mesh-delivery-view/module.json @@ -11,6 +11,7 @@ "indexer_candidates", "indexer_predefined", "indexer_config_candidates", + "indexer_packages", "builder_build", "builder_builds", "builder_history", @@ -55,6 +56,7 @@ "mesh-delivery-indexer.config-candidates", "mesh-delivery-indexer.heard", "mesh-delivery-indexer.modules", + "mesh-delivery-indexer.packages", "mesh-delivery-indexer.predefined", "mesh-delivery-indexer.taken", "mesh-delivery-teller.answers",