From 702b19fdb1bd006d0b7086ea8c902012712fce63 Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 3 Nov 2026 23:25:27 +0100 Subject: [PATCH] =?UTF-8?q?view=5Ftiers=20names=20each=20wait=20by=20its?= =?UTF-8?q?=20edge,=20a=20verb,=20a=20reader=20capability=20or=20bus=20sta?= =?UTF-8?q?te,=20and=20every=20verb=20edge=20left=20out=20(ADR=200614=20?= =?UTF-8?q?=C2=A72-=C2=A75;=20issue=20774)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../internal/assign/verbs.go | 14 ++---- .../internal/assign/wants.go | 5 +- .../internal/assign/wants_test.go | 4 +- .../cmd/mesh-delivery-view-tools/main.go | 8 +-- .../tiers_rows_test.go | 28 +++++++++++ .../mesh-delivery-view/internal/view/tiers.go | 41 +++++++++------ sdk/graph/graph_test.go | 11 ++++ sdk/graph/waits.go | 50 +++++++++++++++++++ 8 files changed, 131 insertions(+), 30 deletions(-) create mode 100644 sdk/graph/waits.go diff --git a/modules/mesh-delivery-assigner/internal/assign/verbs.go b/modules/mesh-delivery-assigner/internal/assign/verbs.go index 9edf0d85..79714dcc 100644 --- a/modules/mesh-delivery-assigner/internal/assign/verbs.go +++ b/modules/mesh-delivery-assigner/internal/assign/verbs.go @@ -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) -} diff --git a/modules/mesh-delivery-assigner/internal/assign/wants.go b/modules/mesh-delivery-assigner/internal/assign/wants.go index cf53a27e..f75faf8f 100644 --- a/modules/mesh-delivery-assigner/internal/assign/wants.go +++ b/modules/mesh-delivery-assigner/internal/assign/wants.go @@ -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 diff --git a/modules/mesh-delivery-assigner/internal/assign/wants_test.go b/modules/mesh-delivery-assigner/internal/assign/wants_test.go index 278c9971..dceaec80 100644 --- a/modules/mesh-delivery-assigner/internal/assign/wants_test.go +++ b/modules/mesh-delivery-assigner/internal/assign/wants_test.go @@ -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"]) } } 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 d147a802..746dc902 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 @@ -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{ diff --git a/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/tiers_rows_test.go b/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/tiers_rows_test.go index 6236315b..ed5f0c94 100644 --- a/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/tiers_rows_test.go +++ b/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/tiers_rows_test.go @@ -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 || diff --git a/modules/mesh-delivery-view/internal/view/tiers.go b/modules/mesh-delivery-view/internal/view/tiers.go index 8055ad38..4d0a6ba4 100644 --- a/modules/mesh-delivery-view/internal/view/tiers.go +++ b/modules/mesh-delivery-view/internal/view/tiers.go @@ -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 ), 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 , bus state, or for a want held back +// want, verb or reader capability 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()}} } diff --git a/sdk/graph/graph_test.go b/sdk/graph/graph_test.go index e87a15c5..ad97e5a3 100644 --- a/sdk/graph/graph_test.go +++ b/sdk/graph/graph_test.go @@ -131,6 +131,17 @@ func TestGraphEdges(t *testing.T) { if got := OfManifests(map[string]catalogue.Manifest{"keeper": verbs["keeper"]})["keeper"]; got.Awaits != nil { t.Errorf("bus-state with no module offering it is awaited: %+v", got) } + // The words of a want held on a verb or a reader capability name their edge, and no other words do. + for words, want := range map[string]string{ + VerbWaitWords("seat:mesh-catalog.modules", "artifact-store-tools", "g14"): "verb seat:mesh-catalog.modules", + ReaderWaitWords("g14", []string{"tool-runner", "bus-state"}, "node-tools", "src2:abcdef"): "reader capability tool-runner, bus-state", + "the verb is served": "", + "the node-engine of g14 has not started": "", + } { + if got := EdgeOfWords(words); got != want { + t.Errorf("the edge of %q = %q, want %q", words, got, want) + } + } for name, c := range map[string]struct { edit func(Graph) want []string diff --git a/sdk/graph/waits.go b/sdk/graph/waits.go new file mode 100644 index 00000000..eea7dcb2 --- /dev/null +++ b/sdk/graph/waits.go @@ -0,0 +1,50 @@ +package graph + +import ( + "fmt" + "strings" +) + +// The words of a want held on an edge of ADR 0614 (novox/hq ADR 0614 §2, §4): no reader knows a rule number of its own for +// either yet, so the assigner holds them under the provisions' and the composition's rules, and these words are the one +// place they are written and read. The assigner writes them (VerbWaitWords, ReaderWaitWords); the delivery view names +// the edge a want waits on from them (EdgeOfWords). + +const ( + verbOpen = "the verb " + verbClose = " invokes is served by no running build" + readerOpen = "the node-engine of " + readerMid = " does not read " + readerTail = ", which the manifest of " +) + +// VerbWaitWords are the words of a want of module on node held until a build serving verb runs healthy (ADR 0614 §2). +func VerbWaitWords(verb, module, node string) string { + return fmt.Sprintf("%s%s %s%s: its want on %s waits until a build serving it runs healthy (ADR 0614 §2)", verbOpen, verb, + module, verbClose, node) +} + +// ReaderWaitWords are the words of a want of module at fp held on node until its node-engine states the reader +// capabilities missing (ADR 0614 §4). +func ReaderWaitWords(node string, missing []string, module, fp string) string { + return fmt.Sprintf("%s%s%s%s%s%s at %s needs: the want moves once that engine states it", readerOpen, node, readerMid, + strings.Join(missing, ", "), readerTail, module, fp) +} + +// EdgeOfWords is the edge a held want's words name: `verb ` for a verb wait, `reader capability ` for +// a reader capability's hold, or "" for words of neither. +func EdgeOfWords(words string) string { + if rest, ok := strings.CutPrefix(words, verbOpen); ok { + if verb, _, ok := strings.Cut(rest, " "); ok && strings.Contains(rest, verbClose) { + return "verb " + verb + } + } + if rest, ok := strings.CutPrefix(words, readerOpen); ok { + if _, rest, ok := strings.Cut(rest, readerMid); ok { + if missing, _, ok := strings.Cut(rest, readerTail); ok { + return "reader capability " + missing + } + } + } + return "" +}