view_tiers names each wait by its edge, a verb, a reader capability or bus state, and every verb edge left out (ADR 0614 §2-§5; issue 774)

The words of a want held on a verb or a reader capability are written and read in one place, the module graph's
package: the assigner writes them, the view names the edge from them. A bus-state wait is named bus state, and each
module's verb edges the graph leaves out on a cycle are listed beside its waits.

Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
This commit is contained in:
2026-11-03 23:26:25 +01:00
parent 0279c5c288
commit 702b19fdb1
8 changed files with 131 additions and 30 deletions
@@ -1,30 +1,24 @@
package assign
import (
"fmt"
"slices"
"git.novox.be/novox/mesh-delivery/sdk/graph"
)
// verbWaiting says why the module's want on node waits for a verb it invokes (novox/hq ADR 0614 §2): the first seat verb or
// module tool of Invokes (the module graph's verb edges, those it leaves out on a cycle not among them, §3) that no
// running healthy build serves. Nil Served (nothing read of what runs) waits for none, and a verb a running build serves
// already adds no wait. It holds under the provisions' rule (RuleProvisions): no reader knows a rule number of its own
// yet, so the words name the edge.
// yet, so the words name the edge (graph.VerbWaitWords, which the delivery view reads).
func (m Module) verbWaiting(node string) (string, bool) {
if m.Served == nil {
return "", false
}
for _, v := range slices.Sorted(slices.Values(m.Invokes)) {
if !m.Served(v) {
return VerbWaitWords(v, m.Name, node), true
return graph.VerbWaitWords(v, m.Name, node), true
}
}
return "", false
}
// VerbWaitWords are the words of a want held for a verb it invokes (ADR 0614 §2): they open with "the verb", so a reader
// of the want's words (the delivery view) names the edge.
func VerbWaitWords(verb, module, node string) string {
return fmt.Sprintf("the verb %s %s invokes is served by no running build: its want on %s waits until a build serving it runs healthy (ADR 0614 §2)",
verb, module, node)
}
@@ -11,6 +11,8 @@ import (
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/provisions"
"git.novox.be/novox/mesh-delivery/sdk/graph"
)
// Module is what the six rules read of one module (ADR 0429 §6, ADR 0528 clause 3): its newest fingerprint F at the
@@ -175,8 +177,7 @@ func (m Module) readerLacking(node string, n Nodes) (string, bool) {
if len(missing) == 0 {
return "", false
}
return fmt.Sprintf("the node-engine of %s does not read %s, which the manifest of %s at %s needs: the want moves once that engine states it",
node, strings.Join(missing, ", "), m.Name, short(m.FP)), true
return graph.ReaderWaitWords(node, missing, m.Name, short(m.FP)), true // the words the delivery view names the edge by
}
// provisionWaiting says why the module's want on node waits for a provision (ADR 0570 §4): a provision it requires that has
@@ -9,6 +9,8 @@ import (
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/provisions"
"git.novox.be/novox/mesh-delivery/sdk/graph"
)
func fpOf(c byte) string { return delivery.SourcePrefix2 + strings.Repeat(string(c), 64) }
@@ -169,7 +171,7 @@ func TestEachRuleHoldsTheWantBack(t *testing.T) {
}
continue
}
if out.Wants["g14"].FP == fNew || out.Rules["g14"].Rule != delivery.RuleProvisions || out.Rules["g14"].Words != VerbWaitWords(c.holds, "mail", "g14") {
if out.Wants["g14"].FP == fNew || out.Rules["g14"].Rule != delivery.RuleProvisions || out.Rules["g14"].Words != graph.VerbWaitWords(c.holds, "mail", "g14") {
t.Errorf("%s: want %+v, held by %+v", c.name, out.Wants["g14"], out.Rules["g14"])
}
}
@@ -48,9 +48,11 @@ func Tools(r view.Reader, u view.UsageReader) []stdio.Tool {
}},
{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 " +
"(each whose fingerprint last changed at it), ordered by the module graph (a module after the bases, the providers " +
"of the provisions and bus state it awaits and the modules serving the verbs it invokes, among them; novox/hq ADR 0614), " +
"and for each module what it waits on now and why, named by its edge: its build key, on each node that wants it its want " +
"(the assigner's rule and words when held back: a verb no running build serves, a reader capability the node-engine " +
"does not state), its have, each provision and its bus state; and the verb edges the graph leaves out on a cycle. 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{
@@ -26,6 +26,9 @@ func encoded(t *testing.T, v []byte, err error) string {
return string(v)
}
// short is a fingerprint as the assigner's words shorten it.
func short(fp string) string { return fp[:len("src2:")+12] }
// 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.
@@ -104,6 +107,31 @@ func tiersToolSaysEachModulesWait(t *testing.T) {
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)
}
// ADR 0614 §2-§5: a want held on a verb or a reader capability is named by its edge, a wait for bus state as bus state,
// and each verb edge the graph leaves out on a cycle is named.
r.GraphState = fake{
"db": node(graph.Node{FP: fp("a"), Repository: "novox/mesh-catalog", Provides: []string{"database"}}),
"web": node(graph.Node{FP: fp("b"), Repository: "novox/mesh-catalog", Requires: []string{"database", graph.BusState}, Awaits: []string{"database", graph.BusState},
Invokes: []string{"seat:x.v", "db.loop"}, LeftOut: []string{"db.loop"}}),
"lib": node(graph.Node{FP: fp("c"), Repository: "novox/mesh-catalog"}),
}.reader().State
for words, edge := range map[string]string{
graph.VerbWaitWords("seat:x.v", "web", "ace"): "verb seat:x.v",
graph.ReaderWaitWords("ace", []string{"tool-runner"}, "web", short(fp("b"))): "reader capability tool-runner",
"the provision ace.web.database of web on ace is asked, not provided": "want",
} {
r.WantsState = fake{key(delivery.WantKey("ace", "web")): want(delivery.Want{Self: true, FP: fp("f"), Newest: fp("b"), Rule: delivery.RuleProvisions, Words: words})}.reader().State
out, err := run(t, r, view.TiersTool, map[string]any{"revision": float64(9)})
got, _ := out.(view.Tiers)
if err != nil || len(got.Tiers) != 2 {
t.Fatalf("view_tiers 9 = %+v, %v", out, err)
}
web := got.Tiers[1][0]
if len(web.Waits) != 3 || web.Waits[0].Edge != edge || web.Waits[1].Edge != "bus state" || !strings.Contains(web.Waits[1].Words, "no key of bus-state") ||
!reflect.DeepEqual(web.LeftOut, []string{"db.loop"}) {
t.Errorf("web held by %q = %+v, want its want named %s, a bus state wait and db.loop left out", words, web, edge)
}
}
// 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 ||
@@ -16,9 +16,11 @@ import (
)
// 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
// modules the revision moves, ordered by the module graph's ordering edges among them (a base, a provision or bus state
// awaited from another of them, a verb another of them serves whose edge the graph keeps, ADR 0614 §1), and for each module
// what it waits on now: its build key, each node's want (named by its edge: a verb, a reader capability) and have, and
// each provision it awaits on each node, with the rule and the words of the part that holds it, and the verb edges the
// graph leaves out. 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).
@@ -43,18 +45,22 @@ type Tiers struct {
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.
// TierModule is one module of a tier: its fingerprint, the modules of the same revision it waits for (After), each
// wait of its own now (no wait is a module that reached every node present), and the verbs it invokes whose edge the
// module graph leaves out on a cycle, so nothing waits on them (LeftOut, ADR 0614 §3).
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"`
Module string `json:"module"`
FP string `json:"fp,omitempty"`
None string `json:"none,omitempty"`
After []string `json:"after,omitempty"`
Waits []TierWait `json:"waits,omitempty"`
LeftOut []string `json:"left-out,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.
// TierWait is one thing a module waits on: the edge (build, have, provision <name>, bus state, or for a want held back
// want, verb <verb> or reader capability <capabilities> as the assigner's words name it, ADR 0614 §2, §4, §5), 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"`
@@ -169,7 +175,7 @@ func (r Reader) Tiers(revision uint64) (Tiers, error) {
out.Tiers = append(out.Tiers, nil)
}
m := moved[n]
tm := TierModule{Module: n, FP: m.FP2, None: m.None2, After: after[n]}
tm := TierModule{Module: n, FP: m.FP2, None: m.None2, After: after[n], LeftOut: gr[n].LeftOut}
tm.Waits = r.waitsOf(n, m, gr[n], wants, provided, perr, away)
out.Tiers[t] = append(out.Tiers[t], tm)
}
@@ -234,7 +240,11 @@ func (r Reader) waitsOf(module string, m delivery.Module, g graph.Node, wants, p
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))})
edge := graph.EdgeOfWords(w.Words)
if edge == "" {
edge = "want"
}
out = append(out, TierWait{Edge: edge, 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)
}
@@ -313,6 +323,9 @@ func (r Reader) haveWait(node, module, fp string) (TierWait, bool) {
// 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 a == graph.BusState {
edge = "bus state" // a bus-state entry of the manifest (ADR 0614 §5)
}
if perr != nil {
return []TierWait{{Edge: edge, Node: node, Words: "the provisioner's keys cannot be read: " + perr.Error()}}
}