view_tiers: a source revision's tiers worked out from the module graph at each call, and per module what it waits on now, its build, each node's want and have and each provision it awaits, the nodes offline named away (ADR 0603 §6)

This commit is contained in:
2026-11-03 16:45:06 +01:00
parent 027c445bae
commit 4f6e1021c6
7 changed files with 527 additions and 4 deletions
@@ -13,6 +13,7 @@ import (
"mesh-delivery-view/internal/view"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/provisions"
stdio "git.novox.be/novox/mesh-sdk/go"
)
@@ -43,6 +44,16 @@ func Tools(r view.Reader, u view.UsageReader) []stdio.Tool {
module, _ := args["module"].(string)
return r.Reach(repository, module)
}},
{Name: view.TiersTool,
Description: "A source revision's tiers, worked out now and never stored (novox/hq ADR 0603 §6): the modules the revision moves " +
"(each whose fingerprint last changed at it), ordered by the module graph (a module after the bases and the providers " +
"of the provisions it awaits among them), and for each module what it waits on now and why: its build key, on each node " +
"that wants it its want (the assigner's rule and words when held back), its have, and each provision it awaits. Nodes " +
"the watchman says are offline are named away and left out. At most 500 modules, said when cut short; said when the " +
"module graph cannot be read. Give revision for an older one, none for the gate's newest. Read-only.",
Input: map[string]any{"type": "object", "properties": map[string]any{
"revision": map[string]any{"type": "integer", "description": "the source revision; none for the gate's newest"}}},
Run: func(args map[string]any) (any, error) { return r.TiersOf(args) }},
{Name: view.ModuleHistoryTool,
Description: "The history of one module's value in the indexer's modules state (modules.<module>), with times: each value it " +
"still keeps (up to 20), newest first, with its revision, when it was written, put or del, the repository and commit " +
@@ -307,7 +318,9 @@ func main() {
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}
GraphState: read(stdio.State(delivery.GraphRead)), SourceState: read(stdio.State(delivery.GateModule + ".source")),
ProvisionsState: read(stdio.State(provisions.ProvisionsRead)),
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())}
for _, lang := range delivery.AskLanguages {
@@ -55,6 +55,7 @@ const (
// 1562/AC1: indexer_module answers the key's value as stored: FP, FP2, None2, Inputs2, Path and Toolchains are all there.
func TestModuleToolAnswersTheStoredValue(t *testing.T) {
t.Run("indexer_packages answers the published modules", packagesToolAnswersThePublishedModules)
t.Run("view_tiers names each module's wait", tiersToolSaysEachModulesWait)
r := fake{"a": withFP2, "b": withNone}.reader()
for key, want := range map[string]string{"a": withFP2, "b": withNone} {
out, err := run(t, r, view.ModuleReadTool, map[string]any{"module": key})
@@ -0,0 +1,120 @@
package main
import (
"encoding/json"
"errors"
"fmt"
"reflect"
"strings"
"testing"
"time"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/provisions"
"mesh-delivery-view/internal/view"
)
// encoded is v as its contract's encoder writes it, failing the test when it refuses.
func encoded(t *testing.T, v []byte, err error) string {
t.Helper()
if err != nil {
t.Fatal(err)
}
return string(v)
}
// tiersToolSaysEachModulesWait: view_tiers orders a source revision's modules by the module graph and names each one's
// wait (ADR 0603 §6): a consumer after its provider, held by the assigner's provision rule and its provision not provided;
// a module whose build waits for a package; a provider with no wait; a node offline named away; older modules left out.
func tiersToolSaysEachModulesWait(t *testing.T) {
fp := func(c string) string { return "src2:" + strings.Repeat(c, 64) }
manifest := strings.Repeat("d", 64)
mod := func(f string, src uint64) string {
b, err := delivery.EncodeModule(delivery.Module{FP2: f, Manifest: manifest, Src: src, FPSrc: src, Repository: "novox/mesh-catalog", Commit: "c"})
return encoded(t, b, err)
}
node := func(g delivery.GraphNode) string {
b, err := delivery.EncodeGraphNode(g)
return encoded(t, b, err)
}
build := func(b delivery.BuilderBuild) string {
v, err := delivery.EncodeBuilderBuild(b)
return encoded(t, v, err)
}
want := func(w delivery.Want) string {
v, err := delivery.EncodeWant(w)
return encoded(t, v, err)
}
have := func(h delivery.Have) string {
v, err := delivery.EncodeHave(h)
return encoded(t, v, err)
}
key := func(k string, err error) string {
if err != nil {
t.Fatal(err)
}
return k
}
since := time.Date(2026, 11, 3, 16, 40, 0, 0, time.UTC)
r := fake{"db": mod(fp("a"), 9), "web": mod(fp("b"), 9), "lib": mod(fp("c"), 9), "old": mod(fp("e"), 5)}.reader()
r.GraphState = fake{
"db": node(delivery.GraphNode{FP: fp("a"), Repository: "novox/mesh-catalog", Provides: []string{"database"}}),
"web": node(delivery.GraphNode{FP: fp("b"), Repository: "novox/mesh-catalog", Requires: []string{"database"}, Awaits: []string{"database"}}),
"lib": node(delivery.GraphNode{FP: fp("c"), Repository: "novox/mesh-catalog"}),
}.reader().State
r.Builds = fake{
key(delivery.BuilderBuildKey("db", fp("a"))): build(delivery.BuilderBuild{State: delivery.BuildDone, Manifest: manifest}),
key(delivery.BuilderBuildKey("web", fp("b"))): build(delivery.BuilderBuild{State: delivery.BuildDone, Manifest: manifest}),
key(delivery.BuilderBuildKey("lib", fp("c"))): build(delivery.BuilderBuild{State: delivery.BuildQueued, Waits: "@novox/x@1.2.0"}),
}.reader().State
r.WantsState = fake{
key(delivery.WantKey("ace", "db")): want(delivery.Want{FP: fp("a"), Self: true}),
key(delivery.WantKey("ace", "web")): want(delivery.Want{Self: true, FP: fp("f"), Newest: fp("b"), Rule: delivery.RuleProvisions, Words: "the provision ace.web.database of web on ace is asked, not provided"}),
key(delivery.WantKey("g14", "web")): want(delivery.Want{Self: true, FP: fp("f"), Newest: fp("b"), Rule: delivery.RuleCanary, Words: "the canary"}),
}.reader().State
r.WantsState.History = func(string) ([]view.Revision, error) { return []view.Revision{{At: since}}, nil }
r.HavesState = fake{key(delivery.HaveKey("ace", "db")): have(delivery.Have{FP: fp("a"), Phase: delivery.PhaseRunning, Health: delivery.HealthOK})}.reader().State
p, err := provisions.Encode(provisions.Provision{Provision: "database", Consumer: provisions.End{Module: "web", Node: "ace"},
Provider: provisions.End{Module: "db", Node: "ace"}, State: provisions.Asked, Why: "the build db runs does not offer it", At: since})
r.ProvisionsState = fake{key(provisions.Key("ace", "web", "database", "")): encoded(t, p, err)}.reader().State
off, err := delivery.EncodePresence(delivery.Presence{Online: false})
r.PresenceState = fake{key(delivery.PresenceNodeKey("g14")): encoded(t, off, err)}.reader().State
out, err := run(t, r, view.TiersTool, map[string]any{"revision": float64(9)})
got, _ := out.(view.Tiers)
if err != nil || got.Revision != 9 || got.Modules != 3 || len(got.Tiers) != 2 || !reflect.DeepEqual(got.Away, []string{"g14"}) || got.Graph != "" {
t.Fatalf("view_tiers 9 = %+v, %v", out, err)
}
first := fmt.Sprint(got.Tiers[0])
if len(got.Tiers[0]) != 2 || got.Tiers[0][0].Module != "db" || len(got.Tiers[0][0].Waits) != 0 || got.Tiers[0][1].Module != "lib" ||
len(got.Tiers[0][1].Waits) != 1 || !strings.Contains(got.Tiers[0][1].Waits[0].Words, "waiting for the package @novox/x@1.2.0") {
t.Errorf("the first tier = %s, want db with no wait and lib's build waiting for its package", first)
}
web := got.Tiers[1][0]
if web.Module != "web" || !reflect.DeepEqual(web.After, []string{"db"}) || len(web.Waits) != 2 ||
web.Waits[0].Edge != "want" || web.Waits[0].Node != "ace" || web.Waits[0].Rule != delivery.RuleProvisions || !web.Waits[0].Since.Equal(since) ||
web.Waits[1].Edge != "provision database" || !strings.Contains(web.Waits[1].Words, "asked: the build db runs does not offer it") {
t.Errorf("web = %+v, want after db, held on ace by the provision rule and its provision asked (g14 is away)", web)
}
// No revision named: the gate's newest, its source key's revision.
r.SourceState = view.StateRead{GetEntry: func(string) (json.RawMessage, uint64, error) { return json.RawMessage(`{}`), 9, nil }}
if out, err := run(t, r, view.TiersTool, map[string]any{}); err != nil || out.(view.Tiers).Revision != 9 || out.(view.Tiers).Modules != 3 {
t.Errorf("view_tiers of the gate's newest = %+v, %v", out, err)
}
// The graph not readable: every module in the first tier, said; past 500 modules the answer says it is cut short.
r.GraphState = view.StateRead{Keys: func() ([]string, error) { return nil, errors.New("no bucket") }, Get: r.GraphState.Get}
if out, err := run(t, r, view.TiersTool, map[string]any{"revision": float64(9)}); err != nil || len(out.(view.Tiers).Tiers) != 1 ||
!strings.Contains(out.(view.Tiers).Graph, "cannot be read") {
t.Errorf("view_tiers with no graph = %+v, %v", out, err)
}
many := fake{}
for i := 0; i < view.TiersMost+2; i++ {
many[fmt.Sprintf("m%03d", i)] = mod(fp("a"), 9)
}
r.State = many.reader().State
if out, err := run(t, r, view.TiersTool, map[string]any{"revision": "9"}); err != nil || out.(view.Tiers).Modules != view.TiersMost+2 ||
!strings.Contains(out.(view.Tiers).Cut, "the first 500") {
t.Errorf("view_tiers past its bound = %+v, %v", out.(view.Tiers).Cut, err)
}
}
@@ -55,6 +55,9 @@ type Reader struct {
PredefinedState, ConfigCandidatesState StateRead
// PackagesState is the indexer's published packages (delivery.PackagesBucket, ADR 0593 §2); unset reads none.
PackagesState StateRead
// GraphState is the indexer's module graph (delivery.GraphBucket, ADR 0603 §2), SourceState the gate's source and
// ProvisionsState the provisioner's keys: what view_tiers reads (tiers.go). Unset reads none, said in the answer.
GraphState, SourceState, ProvisionsState StateRead
// BuildLog reads the builder's log from a time (stdio.Log(delivery.BuildLogRead).Since); unset reads none.
BuildLog LogSince
}
@@ -0,0 +1,383 @@
package view
import (
"encoding/json"
"fmt"
"maps"
"slices"
"strconv"
"strings"
"time"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/provisions"
)
// The tiers of a source revision are a view, worked out at each call and never stored (novox/hq ADR 0603 §1, §6): the
// modules the revision moves, ordered by the module graph's ordering edges among them (a base, a provision awaited from
// another of them), and for each module what it waits on now: its build key, each node's want and have, and each
// provision it awaits on each node, with the rule and the words of the part that holds it. Nothing in the mesh waits on
// a tier; the tiers say where the revision stands. A node the watchman says is offline is named away and left out of
// what the revision waits on (ADR 0603 §7).
// TiersTool is the view's read tool of a source revision's tiers.
const TiersTool = "view_tiers"
// TiersMost is the most modules view_tiers answers (ADR 0603 §2); past it the answer says how many it left out.
const TiersMost = 500
// Tiers is a source revision's tiers: Revision the source revision (the gate's source key's revision), Modules how many
// modules it moves (each whose fingerprint last changed at it, delivery.Module.FPSrc), Tiers those modules in order, the
// first tier waiting on none of the others. Cut says when the answer holds fewer than Modules, Graph why the module graph
// could not be read (then every module is in the first tier), Cycle a cycle among them (its modules form the last
// tier), and Away the nodes left out.
type Tiers struct {
Revision uint64 `json:"revision"`
Modules int `json:"modules"`
Tiers [][]TierModule `json:"tiers"`
Cut string `json:"cut,omitempty"`
Graph string `json:"graph,omitempty"`
Cycle []string `json:"cycle,omitempty"`
Away []string `json:"away,omitempty"`
}
// TierModule is one module of a tier: its fingerprint, the modules of the same revision it waits for (After), and each
// wait of its own now; no wait is a module that reached every node present.
type TierModule struct {
Module string `json:"module"`
FP string `json:"fp,omitempty"`
None string `json:"none,omitempty"`
After []string `json:"after,omitempty"`
Waits []TierWait `json:"waits,omitempty"`
}
// TierWait is one thing a module waits on: the edge (build, want, have, or provision <name>), the node it waits on (none
// for its build), the assigner's rule for a want held back, the part's words and since when, where the part says it.
type TierWait struct {
Edge string `json:"edge"`
Node string `json:"node,omitempty"`
Rule int `json:"rule,omitempty"`
Words string `json:"words"`
Since time.Time `json:"since,omitzero"`
}
// Tiers works out the tiers of a source revision, the gate's newest when revision is 0.
func (r Reader) Tiers(revision uint64) (Tiers, error) {
if revision == 0 {
if r.SourceState.GetEntry == nil {
return Tiers{}, fmt.Errorf("the gate's source cannot be read here: name the source revision")
}
_, rev, err := r.SourceState.GetEntry(delivery.SourceMainKey)
if err != nil {
return Tiers{}, fmt.Errorf("the gate's source: %w", err)
}
if rev == 0 {
return Tiers{}, fmt.Errorf("the gate holds no source revision yet")
}
revision = rev
}
modules, err := values(r.State)
if err != nil {
return Tiers{}, fmt.Errorf("the indexer's modules: %w", err)
}
moved := map[string]delivery.Module{}
for key, raw := range modules {
if m, err := delivery.DecodeModule(raw); err == nil && m.FPSrc == revision {
moved[key] = m
}
}
out := Tiers{Revision: revision, Modules: len(moved)}
names := slices.Sorted(maps.Keys(moved))
if len(names) > TiersMost {
out.Cut = fmt.Sprintf("the revision moves %d modules: the first %d by name are shown, %s and the %d after it are not",
len(names), TiersMost, names[TiersMost], len(names)-TiersMost-1)
names = names[:TiersMost]
}
graph := delivery.Graph{}
if raws, err := values(r.GraphState); err != nil {
out.Graph = "the module graph cannot be read, so no module is ordered after another: " + err.Error()
} else {
for key, raw := range raws {
if n, err := delivery.DecodeGraphNode(raw); err == nil {
graph[key] = n
}
}
}
away := r.away()
out.Away = slices.Sorted(maps.Keys(away))
wants, _ := values(r.WantsState)
provided, perr := values(r.ProvisionsState)
// The ordering among the revision's modules: each one's tier is one past the highest of the modules it waits for.
in := map[string]bool{}
for _, n := range names {
in[n] = true
}
after := map[string][]string{}
for _, n := range names {
for _, b := range graph.Before(n) {
if in[b] {
after[n] = append(after[n], b)
}
}
}
sub := delivery.Graph{}
for _, n := range names {
if g, ok := graph[n]; ok {
sub[n] = g
}
}
out.Cycle = sub.Cycle()
tier := map[string]int{}
var level func(string, map[string]bool) int
level = func(n string, visiting map[string]bool) int {
if t, ok := tier[n]; ok {
return t
}
if visiting[n] {
return -1
}
visiting[n] = true
t := 0
for _, b := range after[n] {
lb := level(b, visiting)
if lb < 0 {
return -1
}
t = max(t, lb+1)
}
delete(visiting, n)
tier[n] = t
return t
}
top := 0
for _, n := range names {
if t := level(n, map[string]bool{}); t > top {
top = t
}
}
for _, n := range names {
if _, ok := tier[n]; !ok {
tier[n] = top + 1 // on a cycle: after every tier that is ordered
}
}
for _, n := range names {
t := tier[n]
for len(out.Tiers) <= t {
out.Tiers = append(out.Tiers, nil)
}
m := moved[n]
tm := TierModule{Module: n, FP: m.FP2, None: m.None2, After: after[n]}
tm.Waits = r.waitsOf(n, m, graph[n], wants, provided, perr, away)
out.Tiers[t] = append(out.Tiers[t], tm)
}
compact := out.Tiers[:0]
for _, t := range out.Tiers {
if len(t) > 0 {
compact = append(compact, t)
}
}
out.Tiers = compact
return out, nil
}
// away are the nodes the watchman's presence says are offline.
func (r Reader) away() map[string]bool {
out := map[string]bool{}
presence, err := values(r.PresenceState)
if err != nil {
return out
}
for key, raw := range presence {
node, part, _, err := delivery.ParsePresenceKey(key)
if err != nil || part != "" {
continue
}
if p, err := delivery.DecodePresence(raw); err == nil && !p.Online {
out[node] = true
}
}
return out
}
// waitsOf is what module m waits on now: its build at its fingerprint, then on each node present that wants it, its want,
// its have, and each provision its graph node awaits.
func (r Reader) waitsOf(module string, m delivery.Module, g delivery.GraphNode, wants, provided map[string]json.RawMessage,
perr error, away map[string]bool) []TierWait {
var out []TierWait
if m.FP2 == "" {
return []TierWait{{Edge: "build", Words: "it has no fingerprint, so nothing is built: " + m.None2}}
}
if w, ok := r.buildWait(module, m.FP2); ok {
out = append(out, w)
}
var nodes []string
for key := range wants {
node, mod, err := delivery.ParseWantKey(key)
if err == nil && mod == module {
nodes = append(nodes, node)
}
}
slices.Sort(nodes)
for _, node := range nodes {
if away[node] {
continue
}
w, err := delivery.DecodeWant(wants[mustWantKey(node, module)])
if err != nil || w.Removed {
continue
}
if w.FP != m.FP2 {
words := w.Words
if words == "" {
words = "the want names " + short(w.FP) + ", not " + short(m.FP2)
}
out = append(out, TierWait{Edge: "want", Node: node, Rule: w.Rule, Words: words, Since: r.since(r.WantsState, mustWantKey(node, module))})
} else if hw, ok := r.haveWait(node, module, m.FP2); ok {
out = append(out, hw)
}
for _, a := range g.Awaits {
out = append(out, provisionWaits(node, module, a, provided, perr)...)
}
}
return out
}
func mustWantKey(node, module string) string {
k, _ := delivery.WantKey(node, module)
return k
}
// buildWait is the wait of the build key <module>.<fp>, none when it is done.
func (r Reader) buildWait(module, fp string) (TierWait, bool) {
key, err := delivery.BuilderBuildKey(module, fp)
if err != nil || r.Builds.Get == nil {
return TierWait{Edge: "build", Words: "the builder's builds cannot be read here"}, true
}
raw, err := r.Builds.Get(key)
switch {
case err != nil:
return TierWait{Edge: "build", Words: "the build key " + key + " cannot be read: " + err.Error()}, true
case raw == nil:
return TierWait{Edge: "build", Words: "the builder holds no build of " + short(fp) + " yet"}, true
}
b, err := delivery.DecodeBuilderBuild(raw)
if err != nil {
return TierWait{Edge: "build", Words: "the build key " + key + " does not read: " + err.Error()}, true
}
switch b.State {
case delivery.BuildDone:
return TierWait{}, false
case delivery.BuildFailed:
return TierWait{Edge: "build", Words: "the build of " + short(fp) + " failed: " + b.Error}, true
}
words := "the build of " + short(fp) + " is " + b.State
if b.Waits != "" {
words += ", waiting for the package " + b.Waits
}
if b.Step != "" {
words += ": " + b.Step
}
return TierWait{Edge: "build", Words: words, Since: r.since(r.Builds, key)}, true
}
// haveWait is the wait of node's have of module at fp, none when it runs fp healthy.
func (r Reader) haveWait(node, module, fp string) (TierWait, bool) {
key, err := delivery.HaveKey(node, module)
if err != nil || r.HavesState.Get == nil {
return TierWait{}, false
}
raw, err := r.HavesState.Get(key)
if err != nil || raw == nil {
return TierWait{Edge: "have", Node: node, Words: node + " has not yet said what it runs of " + module}, true
}
h, err := delivery.DecodeHave(raw)
if err != nil {
return TierWait{Edge: "have", Node: node, Words: "its have does not read: " + err.Error()}, true
}
if h.FP == fp && h.Phase == delivery.PhaseRunning && h.Health == delivery.HealthOK {
return TierWait{}, false
}
words := fmt.Sprintf("%s runs %s, %s", node, short(h.FP), h.Phase)
if h.Health != "" {
words += ", health " + string(h.Health)
}
if h.Detail != "" {
words += ": " + h.Detail
}
return TierWait{Edge: "have", Node: node, Words: words, Since: r.since(r.HavesState, key)}, true
}
// provisionWaits are the waits of the provision a that module awaits on node: none once every key of it is provided.
func provisionWaits(node, module, a string, provided map[string]json.RawMessage, perr error) []TierWait {
edge := "provision " + a
if perr != nil {
return []TierWait{{Edge: edge, Node: node, Words: "the provisioner's keys cannot be read: " + perr.Error()}}
}
prefix, _ := provisions.Key(node, module, a, "")
var out []TierWait
found := false
for _, key := range slices.Sorted(maps.Keys(provided)) {
if key != prefix && !strings.HasPrefix(key, prefix+".") {
continue
}
found = true
p, err := provisions.Decode(provided[key])
if err != nil {
out = append(out, TierWait{Edge: edge, Node: node, Words: "the provision key " + key + " does not read: " + err.Error()})
continue
}
if p.State == provisions.Provided {
continue
}
words := "the provision " + key + " is " + p.State
if p.Why != "" {
words += ": " + p.Why
}
out = append(out, TierWait{Edge: edge, Node: node, Words: words, Since: p.At})
}
if !found {
out = append(out, TierWait{Edge: edge, Node: node, Words: "the provisioner holds no key of " + a + " for " + module + " on " + node + " yet"})
}
return out
}
// since is when key's value was written, from the state's history, or zero when it cannot say.
func (r Reader) since(s StateRead, key string) time.Time {
if s.History == nil {
return time.Time{}
}
revs, err := s.History(key)
if err != nil || len(revs) == 0 {
return time.Time{}
}
return revs[len(revs)-1].At
}
// tiersRevision reads the tool's revision argument: a number, as JSON gives it or as text; 0 for the gate's newest.
func tiersRevision(v any) (uint64, error) {
switch x := v.(type) {
case nil:
return 0, nil
case float64:
if x < 0 || x != float64(uint64(x)) {
return 0, fmt.Errorf("the revision %v is no source revision", x)
}
return uint64(x), nil
case string:
if strings.TrimSpace(x) == "" {
return 0, nil
}
return strconv.ParseUint(strings.TrimSpace(x), 10, 64)
}
return 0, fmt.Errorf("the revision %v is no source revision", v)
}
// TiersOf is view_tiers' answer for its arguments.
func (r Reader) TiersOf(args map[string]any) (any, error) {
revision, err := tiersRevision(args["revision"])
if err != nil {
return nil, err
}
return r.Tiers(revision)
}
+2 -2
View File
@@ -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.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
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.graph", "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-provisioner.provisions", "mesh-vault.signers", "node-engine.have"} // view_tiers reads the module graph and the provisioner's keys (ADR 0603 §6); 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", "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) {
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", "view_tiers"}; !reflect.DeepEqual(m.Tools, want) {
t.Errorf("tools = %v, want %v", m.Tools, want)
}
for _, a := range m.Build.Artifacts {
+4 -1
View File
@@ -23,7 +23,8 @@
"build_usage",
"view_reach",
"indexer_module_history",
"indexer_heard"
"indexer_heard",
"view_tiers"
],
"emits": [
"delivered",
@@ -54,6 +55,7 @@
"mesh-delivery-gate.source",
"mesh-delivery-indexer.candidates",
"mesh-delivery-indexer.config-candidates",
"mesh-delivery-indexer.graph",
"mesh-delivery-indexer.heard",
"mesh-delivery-indexer.modules",
"mesh-delivery-indexer.packages",
@@ -62,6 +64,7 @@
"mesh-delivery-teller.answers",
"mesh-conditions.conditions",
"mesh-healers.attempts",
"mesh-provisioner.provisions",
"mesh-delivery-watchman.presence",
"mesh-delivery-watchman.stuck",
"mesh-vault.signers",