D1 (view): the view's tools ask the indexer, the assigner and the wants' bucket of the holders of their seats

The tools program asks indexer_read_*, assigner_read_* and indexer_pending of the module holding the seat mesh-catalog or mesh-wants when it asks, and names the wants' real bucket from that holder at each status; the assigner's module constant goes.

Builds: design 47 G §428, §717, A-210, A-300, A-318

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
2026-11-12 23:01:34 +01:00
parent 058b2458fb
commit cd9a373067
7 changed files with 83 additions and 36 deletions
@@ -398,14 +398,30 @@ func read(state stdio.KeptState) view.StateRead {
// ownersRead is the view's reading of the indexer's and the assigner's states: each through its owner's read tool, asked
// by the bundle on each call (ADR 0615 §1, design 25 § 12.2: the view keeps its tools and answers them by asking the
// owners), never their buckets.
func ownersRead(ask func(key string, body any) (json.RawMessage, error), catalog *meshcatalogclient.Branches) view.Reader {
// Each owner's read tool as the view's invokes names it.
indexer := func(tool string) view.StateRead { return view.OwnerRead(ask, view.IndexerModule+"."+tool) }
assigner := func(tool string) view.StateRead { return view.OwnerRead(ask, view.AssignerModule+"."+tool) }
func ownersRead(ask func(key string, body any) (json.RawMessage, error), holder func(seat string) (string, bool), catalog *meshcatalogclient.Branches) view.Reader {
// Each owner's read tool as the view's invokes names it, asked of the module that holds the owner's seat when it is asked
// (design 47 G A-210, §428): the indexer holds the seat mesh-catalog, the assigner the seat mesh-wants.
atHolder := func(seat string) func(key string, body any) (json.RawMessage, error) {
return func(key string, body any) (json.RawMessage, error) {
module, ok := holder(seat)
if !ok {
return nil, fmt.Errorf("the holder of %s is not found, so %s is not asked", seat, key)
}
return ask(module+"."+key, body)
}
}
indexer := func(tool string) view.StateRead { return view.OwnerRead(atHolder(view.CatalogSeat), tool) }
assigner := func(tool string) view.StateRead { return view.OwnerRead(atHolder(meshwants.Seat), tool) }
return view.Reader{State: catalogRead(catalog), CandidatesState: indexer("indexer_read_candidates"),
WantsState: assigner("assigner_read_wants"), DifferencesState: assigner("assigner_read_differences"),
// The wants' real bucket at the module the tools program reads them of (A-300), as the watchman's stuck keys name it.
WantsHeld: view.AssignerModule + "_" + meshwants.StateWants,
WantsHeld: func() string {
module, ok := holder(meshwants.Seat)
if !ok {
return ""
}
return module + "_" + meshwants.StateWants
},
HeardState: indexer("indexer_read_heard"),
PredefinedState: indexer("indexer_read_predefined"),
ConfigCandidatesState: indexer("indexer_read_config_candidates"),
@@ -449,7 +465,7 @@ func main() {
}
branches.Logf = logf
cat.Start(context.Background())
r := ownersRead(stdio.Ask, branches)
r := ownersRead(stdio.Ask, holderOf(stdio.Ask), branches)
r.Builds, r.HavesState = read(stdio.State("mesh-delivery-builder.builds")), read(stdio.State("node-engine.have"))
r.ReachState = read(stdio.State("reach"))
r.WaitsState = read(stdio.State(viewstate.WaitsState))
@@ -460,7 +476,7 @@ func main() {
r.Assignment, r.Difference = view.AssignmentOf(stdio.Ask), view.DifferenceOf(stdio.Ask)
r.ProvisionsState = read(stdio.State(view.ProvisionsRead))
r.BuildLog = stdio.Log(builds.BuildLogRead).Since
r.Pending = pendingOf
r.Pending = pendingOf(holderOf(stdio.Ask))
// The build modules are no names (design 47 G A-82): the build seats on the seat list, each held by the module the seat
// list names for it; the seat list and its two tools are wired once by mesh-sdk (§204).
// Its callers name seats, never a module, resolved from the seat-holders view the catalog half's claimant serves (A-295, A-298).
@@ -501,11 +517,18 @@ func AnswersTool(callers stdio.Callers, state func(string) views.Source, now fun
callers, state(viewstate.ViewAnswersState), state(views.MarksState), now)
}
// pendingOf asks the indexer's tool indexer_pending (declared in invokes): what it holds back and why (ADR 0633 §5).
func pendingOf() (takein.PendingAnswer, error) {
raw, err := stdio.Ask(view.IndexerModule+".indexer_pending", map[string]any{})
if err != nil {
return takein.PendingAnswer{}, err
// pendingOf asks the indexer's tool indexer_pending (declared in invokes) of the module holding the seat mesh-catalog: what it
// holds back and why (ADR 0633 §5; design 47 G A-210).
func pendingOf(holder func(seat string) (string, bool)) func() (takein.PendingAnswer, error) {
return func() (takein.PendingAnswer, error) {
module, ok := holder(view.CatalogSeat)
if !ok {
return takein.PendingAnswer{}, fmt.Errorf("the holder of %s is not found", view.CatalogSeat)
}
raw, err := stdio.Ask(module+".indexer_pending", map[string]any{})
if err != nil {
return takein.PendingAnswer{}, err
}
return takein.DecodePendingAnswer(raw)
}
return takein.DecodePendingAnswer(raw)
}
@@ -13,11 +13,27 @@ import (
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshwants"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-sdk/graph"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-view/viewstate"
"git.novox.be/novox/mesh-sdk/go/views"
)
// assignerModule is the module the tests run the assigner as, as the seat list names it; no code of the view names it.
const assignerModule = "mesh-delivery-assigner"
// holders answers the seat list as a mesh whose indexer and assigner are not the modules the view once named: the module that
// holds the seat mesh-catalog and the one that holds mesh-wants, each under a name no code of the view knows (design 47 G A-210).
func holders(seat string) (string, bool) {
switch seat {
case meshcatalog.Seat:
return "moved-" + view.IndexerModule, true
case meshwants.Seat:
return "moved-" + assignerModule, true
}
return "", false
}
// source is a fake owner state as a views.Source: what the owner's read tool answers from.
type source map[string]string
@@ -57,9 +73,9 @@ func ownersAnswerTheToolsAsBefore(t *testing.T) {
}
states := map[string]source{
view.IndexerModule + ".candidates": {"feat/x": cand, "fix": `{"v":1,"group":"fix","base":3,"heads":"h","pulls":[{"modules":["netcheck"]}]}`},
view.AssignerModule + ".wants": {"want.ace.zsh": `{"fp":"` + fp2A + `"}`, "want.novox.zsh": `{"fp":"` + fp2B + `"}`,
assignerModule + ".wants": {"want.ace.zsh": `{"fp":"` + fp2A + `"}`, "want.novox.zsh": `{"fp":"` + fp2B + `"}`,
"held." + keyB: `{"node":"ace","words":"failed"}`},
view.AssignerModule + ".differences": {"ace.zsh": `{"unexplained":true}`, "novox.zsh": `{"rule":3}`},
assignerModule + ".differences": {"ace.zsh": `{"unexplained":true}`, "novox.zsh": `{"rule":3}`},
view.IndexerModule + "." + view.IndexerPredefinedState: {view.PredefinedListKey: `{"v":1,"keys":["dir:state"]}`},
view.IndexerModule + "." + view.IndexerConfigCandidatesState: {"novox.r.7": `{"v":1,"pull":"novox/r#7"}`},
view.IndexerModule + "." + view.PackagesState: {pkgKey: `{"v":1,"module":"git.novox.be/novox/mesh-sdk/go","commit":"c1"}`},
@@ -71,13 +87,13 @@ func ownersAnswerTheToolsAsBefore(t *testing.T) {
const heard1 = `{"v":1,"subject":"s","sequence":41,"repository":"novox/mesh-catalog","commit":"9cdac09a"}`
states[view.IndexerModule+".heard"] = source{"forge": heard1}
histories := map[string]map[string][]views.Revision{
view.IndexerModule + ".heard": {"forge": {{Revision: 3, At: at, Op: "put", Value: json.RawMessage(heard1)}}},
view.AssignerModule + ".wants": {"want.ace.zsh": {{Revision: 12, At: at, Op: "put", Value: json.RawMessage(`{"fp":"` + fp2A + `"}`)}}},
view.IndexerModule + ".heard": {"forge": {{Revision: 3, At: at, Op: "put", Value: json.RawMessage(heard1)}}},
assignerModule + ".wants": {"want.ace.zsh": {{Revision: 12, At: at, Op: "put", Value: json.RawMessage(`{"fp":"` + fp2A + `"}`)}}},
}
// The direct reading, as the tools read the states before (the reads the manifest no longer has).
st := func(name string) view.StateRead { return fake(states[name]).reader().State }
direct := view.Reader{CandidatesState: st(view.IndexerModule + ".candidates"),
WantsState: st(view.AssignerModule + ".wants"), DifferencesState: st(view.AssignerModule + ".differences"),
WantsState: st(assignerModule + ".wants"), DifferencesState: st(assignerModule + ".differences"),
PredefinedState: st(view.IndexerModule + "." + view.IndexerPredefinedState),
ConfigCandidatesState: st(view.IndexerModule + "." + view.IndexerConfigCandidatesState),
PackagesState: st(view.IndexerModule + "." + view.PackagesState),
@@ -86,7 +102,7 @@ func ownersAnswerTheToolsAsBefore(t *testing.T) {
tools := map[string]string{}
for name := range states {
owner, state, _ := strings.Cut(name, ".")
short := map[string]string{view.IndexerModule: "indexer_read_", view.AssignerModule: "assigner_read_"}[owner]
short := map[string]string{view.IndexerModule: "indexer_read_", assignerModule: "assigner_read_"}[owner]
tools[owner+"."+short+strings.ReplaceAll(state, "-", "_")] = name
}
asked := map[string]int{}
@@ -95,6 +111,11 @@ func ownersAnswerTheToolsAsBefore(t *testing.T) {
asked[key]++
return a()
}
// The owners are asked at the holders the seat list names, never at the modules the view once named.
if strings.HasPrefix(key, view.IndexerModule+".") || strings.HasPrefix(key, assignerModule+".") {
return nil, fmt.Errorf("%s is asked of a module named in code, not of the seat's holder", key)
}
key = strings.TrimPrefix(key, "moved-")
name, ok := tools[key]
if !ok {
return nil, fmt.Errorf("no tool %s", key)
@@ -119,7 +140,7 @@ func ownersAnswerTheToolsAsBefore(t *testing.T) {
}
return json.Marshal(a)
}
owned := ownersRead(ask, startedBranches(t, context.Background(), ask))
owned := ownersRead(ask, holders, startedBranches(t, context.Background(), ask))
for _, row := range []struct {
tool string
args map[string]any
@@ -174,7 +195,7 @@ func ownersAnswerTheToolsAsBefore(t *testing.T) {
away := func(key string, body any) (json.RawMessage, error) { return nil, fmt.Errorf("the holder is away") }
gone, cancel := context.WithCancel(context.Background())
cancel()
none := ownersRead(away, startedBranches(t, gone, away))
none := ownersRead(away, holders, startedBranches(t, gone, away))
if out, err := run(t, none, view.ModulesReadTool, map[string]any{}); err == nil || !strings.Contains(err.Error(), "no watched branch") {
t.Errorf("indexer_modules with no view = %v, %v, want an error naming it", out, err)
}
@@ -14,8 +14,7 @@ import (
// `consumes`); a unit row checks them against it (TestTheNamesAreTheManifests). No contract names a holder (novox/hq
// ADR 0625 §4, ADR 0634 §1). The indexer's module and its reads are the catalog client's.
const (
IndexerModule = "mesh-delivery-indexer"
AssignerModule = "mesh-delivery-assigner"
IndexerModule = "mesh-delivery-indexer"
// ProvisionsRead is the seat mesh-provisioner's state provisions, as the manifest's `reads` names it.
ProvisionsRead = "mesh-provisioner.provisions"
WatchmanModule = "mesh-delivery-watchman"
@@ -51,13 +51,13 @@ type Reader struct {
State StateRead // the indexer's modules
Builds StateRead // the builder's builds
CandidatesState StateRead // the indexer's candidates
WantsState StateRead // the assigner's wants and holds
WantsHeld string // the wants' real bucket at the module WantsState reads, which the watchman's stuck keys name
DifferencesState StateRead // the assigner's differences
HavesState StateRead // the node-engine's haves
ReachState StateRead // the view's own reach (ADR 0556)
WaitsState StateRead // the view's own waits: the parts' wait-held (design 47 G §512, A-242)
CandidatesState StateRead // the indexer's candidates
WantsState StateRead // the assigner's wants and holds
WantsHeld func() string // the wants' real bucket at the module WantsState reads, read at each use, which the watchman's stuck keys name
DifferencesState StateRead // the assigner's differences
HavesState StateRead // the node-engine's haves
ReachState StateRead // the view's own reach (ADR 0556)
WaitsState StateRead // the view's own waits: the parts' wait-held (design 47 G §512, A-242)
// HeardState is the FORGE messages the indexer handled (issue 1126 note 76); unset reads none.
HeardState StateRead
// SignersState is the vault's public signing halves (ADR 0550 §2); unset reads none.
@@ -42,9 +42,9 @@ const HealsWithin = 7 * 24 * time.Hour
// StatusRead is the five buckets status reads, as a tool reads them.
type StatusRead struct {
Conditions, Attempts, Presence, Stuck, Wants, Haves StateRead
// WantsHeld is the real bucket of the wants at their holder, which the watchman's stuck keys name (design 47 G A-303);
// "" reads no want as stuck.
WantsHeld string
// WantsHeld answers the real bucket of the wants at their holder, which the watchman's stuck keys name (design 47 G A-303),
// read at each status so a moved holder is named as it moves; "" (or none) reads no want as stuck.
WantsHeld func() string
}
// Status is the view's answer to status.
@@ -157,6 +157,10 @@ func DeriveStatus(r StatusRead, now time.Time) Status {
wants, werr := values(r.Wants)
haves, herr := values(r.Haves)
held := ""
if r.WantsHeld != nil {
held = r.WantsHeld()
}
switch {
case werr != nil:
st.MovingUnread = "the assigner's wants cannot be read: " + werr.Error()
@@ -181,7 +185,7 @@ func DeriveStatus(r StatusRead, now time.Time) Status {
}
}
words, stopped := "", false
if sk, err := meshwatchdogsclient.StuckKey(r.WantsHeld, wk); err == nil && serr == nil && r.WantsHeld != "" {
if sk, err := meshwatchdogsclient.StuckKey(held, wk); err == nil && serr == nil && held != "" {
if s, err := meshwatchdogsclient.DecodeStuck(stuck[sk]); err == nil && !s.Moved {
words, stopped = s.Waited, true
}
@@ -310,7 +310,7 @@ func statusDerivesEachPartFromItsBucket(t *testing.T) {
"core.doctor.self-check": enc(mcseat.ConditionsByKeyValue{Key: "core.doctor.self-check", Severity: mcc.Urgent, Summary: "no run", Raised: t0.Add(time.Minute)}),
}),
Presence: states(map[string]string{"a": presence(true), "b": presence(false)}),
WantsHeld: held,
WantsHeld: func() string { return held },
Stuck: states(map[string]string{stuckWant: stuckOf(held, "want.b.web", "b to run web", false),
"x.moved": stuckOf("x", "moved", "y", true)}),
Wants: states(map[string]string{"want.a.web": want(fp2), "want.a.mail": want(fp1), "want.b.web": want(fp2), "want.a.db": want(fp3)}),
+1 -1
View File
@@ -258,7 +258,7 @@ func TestManifestIsTakenAndReadsOnly(t *testing.T) { // 1134/AC7
t.Errorf("the view follows %s, its manifest consumes %v", family, m.Consumes)
}
}
for _, tool := range []string{meshcatalogclient.RevisionVerb, meshcatalogclient.RegistrationsVerb, meshcatalogclient.SourceVerb, meshcatalogclient.EntryVerb, view.IndexerModule + ".indexer_read_candidates", view.IndexerModule + ".indexer_pending", view.AssignerModule + ".assigner_read_wants", view.TellerModule + ".teller_read_answers"} {
for _, tool := range []string{meshcatalogclient.RevisionVerb, meshcatalogclient.RegistrationsVerb, meshcatalogclient.SourceVerb, meshcatalogclient.EntryVerb, view.IndexerModule + ".indexer_read_candidates", view.IndexerModule + ".indexer_pending", "mesh-delivery-assigner.assigner_read_wants", view.TellerModule + ".teller_read_answers"} {
if !slices.Contains(verbs(m.Invokes), tool) {
t.Errorf("the view asks %s, its manifest invokes %v", tool, m.Invokes)
}