From 05b038580527fa4a35c9ac8ce66af3c106e4a53e Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 12 Nov 2026 05:38:16 +0100 Subject: [PATCH] WM8: watchman_views answers the condition keeper's view-waits by part, node and view MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Builds: design 47 G §83, §501, §609 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h --- .../cmd/mesh-delivery-watchman-tools/main.go | 67 ++++++++++++++++++- .../mesh-delivery-watchman-tools/main_test.go | 37 +++++++++- .../mesh-delivery-watchman/manifest_test.go | 4 +- modules/mesh-delivery-watchman/module.json | 6 +- 4 files changed, 106 insertions(+), 8 deletions(-) diff --git a/modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman-tools/main.go b/modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman-tools/main.go index fa5a0672..7befa24c 100644 --- a/modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman-tools/main.go +++ b/modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman-tools/main.go @@ -15,6 +15,7 @@ import ( "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient" "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshwatchdogs" stdio "git.novox.be/novox/mesh-sdk/go" + "git.novox.be/novox/mesh-sdk/go/keys" "git.novox.be/novox/mesh-sdk/go/views" "mesh-delivery-watchman/internal/socket" @@ -36,13 +37,17 @@ const ( // modules stand, and asking one again by hand (novox/hq ADR 0615, DLV-M5). ToolFirstViews = "watchman_first_views" ToolFirstViewAgain = "watchman_first_view_again" + // ToolViews answers which part waits for which view, since when (design 47 G §83, §501, §609). + ToolViews = "watchman_views" + // conditionsVerb is the seat mesh-conditions' verb that lists what is wrong now, which the tool reads (manifest invokes). + conditionsVerb = "seat:mesh-conditions.conditions" ) // Verbs are the seat's verbs, in the manifest's order. var Verbs = []string{VerbSignals, VerbStuck, VerbPresence, VerbDeadLetters, VerbResync} // ModuleTools are the module's own tools, in the manifest's order. -var ModuleTools = []string{ToolFindings, ToolFirstViews, ToolFirstViewAgain} +var ModuleTools = []string{ToolFindings, ToolFirstViews, ToolFirstViewAgain, ToolViews} // Reads names, per tool, the states it reads whole; Acts, per tool, the program's act it starts. The clause-12 test holds // that every state the manifest keeps is read by one, and every act the program takes by hand is started by one. @@ -136,7 +141,7 @@ func presenceOf(st state, machine string) (any, error) { } // Tools are the seat's verbs and the module's tools, over the states states gives and the program asked through ask. -func Tools(states func(string) state, ask func(socket.Ask) (json.RawMessage, error)) []stdio.Tool { +func Tools(states func(string) state, ask func(socket.Ask) (json.RawMessage, error), conditions func(args map[string]any) (json.RawMessage, error)) []stdio.Tool { read := func(name string) func(map[string]any) (any, error) { return func(args map[string]any) (any, error) { return readAll(states(name), name, str(args, "key"), str(args, "prefix")) @@ -200,12 +205,68 @@ func Tools(states func(string) state, ask func(socket.Ask) (json.RawMessage, err Description: "The watchman's findings (novox/hq ADR 0564 §2): each thing it observes wrong now, .., with its " + "severity, subject, evidence and the row that observed it; the condition keeper raises and clears the conditions from " + "them. Give key for one, prefix to narrow. Read-only."}, + {Name: ToolViews, Run: func(map[string]any) (any, error) { return viewsWaiting(conditions) }, + Description: "Which part waits for which view, on which machine and since when (design 47 G §83): every open condition of the " + + "kind view-waits the condition keeper holds, which each part writes when it has waited for a view past its bound. A view " + + "a part holds is not listed. Ask the part's _first_view_again to ask a view again. Read-only."}, }, views.StatusTools(watch.Observer, ToolFirstViews, ToolFirstViewAgain, stdio.State(views.StatusState), meshhandactsclient.Recorder{Ask: stdio.Ask}, time.Now)...) } +// waitingView is one part waiting for one view, as watchman_views answers it. +type waitingView struct { + Module string `json:"module"` + Node string `json:"node,omitempty"` + View string `json:"view"` + Since string `json:"since"` + Summary string `json:"summary,omitempty"` + Key string `json:"key"` +} + +// viewsWaiting answers the keeper's open conditions of the kind view-waits as the parts and views they name (the keeper's +// condition of a finding the part wrote: its subject the part's module on its node, its key .. with the id the +// view, escaped as a key's name is). +func viewsWaiting(conditions func(args map[string]any) (json.RawMessage, error)) (any, error) { + raw, err := conditions(map[string]any{"scope": "module"}) + if err != nil { + return nil, fmt.Errorf("the condition keeper's open conditions cannot be read: %w", err) + } + var ans struct { + Conditions []struct { + Key string `json:"key"` + Kind string `json:"kind"` + Summary string `json:"summary"` + Raised string `json:"raised"` + Subject struct { + ID string `json:"id"` + Machine string `json:"machine"` + } `json:"subject"` + } `json:"conditions"` + } + if err := json.Unmarshal(raw, &ans); err != nil { + return nil, fmt.Errorf("the keeper's answer is not its conditions: %w", err) + } + out := []waitingView{} + for _, c := range ans.Conditions { + if c.Kind != views.ViewWaitsKind { + continue + } + view := strings.TrimSuffix(strings.TrimPrefix(c.Key, views.FindingScope+"."), "."+views.ViewWaitsKind) + if u, err := keys.UnescapeName(view); err == nil { + view = u + } + out = append(out, waitingView{Module: c.Subject.ID, Node: c.Subject.Machine, View: view, Since: c.Raised, Summary: c.Summary, Key: c.Key}) + } + sort.Slice(out, func(i, j int) bool { return out[i].Since < out[j].Since }) + return map[string]any{"waiting": out, "count": len(out)}, nil +} + func main() { path := socket.PathOf(os.Getenv) - tools := Tools(func(name string) state { return stdio.State(name) }, func(a socket.Ask) (json.RawMessage, error) { return socket.On(path, a) }) + tools := Tools(func(name string) state { return stdio.State(name) }, func(a socket.Ask) (json.RawMessage, error) { return socket.On(path, a) }, + func(args map[string]any) (json.RawMessage, error) { + r, err := stdio.Ask(conditionsVerb, args) + return json.RawMessage(r), err + }) if err := stdio.Serve("", tools); err != nil { fmt.Fprintln(os.Stderr, "mesh-delivery-watchman-tools:", err) os.Exit(1) diff --git a/modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman-tools/main_test.go b/modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman-tools/main_test.go index d6100784..3a1e37d9 100644 --- a/modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman-tools/main_test.go +++ b/modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman-tools/main_test.go @@ -62,7 +62,7 @@ func TestEveryStateAndActHasATool(t *testing.T) { return nil, errors.New("refused") } return json.RawMessage(`{}`), nil - }) + }, func(map[string]any) (json.RawMessage, error) { return json.RawMessage(`{"conditions":[]}`), nil }) served := map[string]stdio.Tool{} for _, tool := range tools { served[tool.Name] = tool @@ -171,3 +171,38 @@ func TestEveryStateAndActHasATool(t *testing.T) { } }) } + +// Design 47 G §83, §501, §609 (WM8): watchman_views answers the keeper's view-waits conditions as the parts and views they name, +// oldest first, and lists no other condition. +func TestWatchmanViewsAnswersTheKeepersViewWaits(t *testing.T) { + var got map[string]any + tools := Tools(func(string) state { return fakeState{} }, nil, func(args map[string]any) (json.RawMessage, error) { + got = args + return json.RawMessage(`{"conditions":[ + {"key":"module.mesh-catalog.main.view-waits","kind":"view-waits","raised":"2026-11-12T10:05:00Z","summary":"b waits","subject":{"scope":"module","id":"mesh-delivery-builder","machine":"ace"}}, + {"key":"module.x.unit-failed","kind":"first-view-unanswered","raised":"2026-11-12T10:00:00Z","subject":{"scope":"module","id":"x"}}, + {"key":"module.git.main.view-waits","kind":"view-waits","raised":"2026-11-12T10:01:00Z","summary":"i waits","subject":{"scope":"module","id":"mesh-delivery-indexer","machine":"novox"}}]}`), nil + }) + var tool stdio.Tool + for _, x := range tools { + if x.Name == ToolViews { + tool = x + } + } + if tool.Run == nil { + t.Fatalf("%s is not served", ToolViews) + } + out, err := tool.Run(map[string]any{}) + if err != nil { + t.Fatal(err) + } + raw, _ := json.Marshal(out) + want := `{"count":2,"waiting":[{"module":"mesh-delivery-indexer","node":"novox","view":"git.main","since":"2026-11-12T10:01:00Z","summary":"i waits","key":"module.git.main.view-waits"},` + + `{"module":"mesh-delivery-builder","node":"ace","view":"mesh-catalog.main","since":"2026-11-12T10:05:00Z","summary":"b waits","key":"module.mesh-catalog.main.view-waits"}]}` + if string(raw) != want { + t.Errorf("answered %s\nwant %s", raw, want) + } + if got["scope"] != "module" { + t.Errorf("asked the keeper %v", got) + } +} diff --git a/modules/mesh-delivery-watchman/manifest_test.go b/modules/mesh-delivery-watchman/manifest_test.go index ce1c6227..8fb2ae9a 100644 --- a/modules/mesh-delivery-watchman/manifest_test.go +++ b/modules/mesh-delivery-watchman/manifest_test.go @@ -126,7 +126,7 @@ func TestManifestStateAndReads(t *testing.T) { // 1135/AC6 } // The join: its first view asked again by hand is a hand act, recorded through the seat (views.StatusTools). The waits the // indexer holds at the watchman's start are asked of its tool (ADR 0633 §5), a verb edge as the view's. - if want := []string{"mesh-delivery-assigner.assigner_read_wants", "mesh-delivery-indexer.indexer_pending", "mesh-delivery-indexer.indexer_read_modules", "seat:mesh-hand-acts.hand-act"}; !reflect.DeepEqual(m.Invokes, want) { + if want := []string{"mesh-delivery-assigner.assigner_read_wants", "mesh-delivery-indexer.indexer_pending", "mesh-delivery-indexer.indexer_read_modules", "seat:mesh-hand-acts.hand-act", "seat:mesh-conditions.conditions"}; !reflect.DeepEqual(m.Invokes, want) { t.Errorf("invokes = %v, want %v", m.Invokes, want) } // DLV-M5 its first views, 1585/T20 the indexer's take-in events, and DLV-W1 (ADR 0595 §4): the accepted verdicts and the bus module's created and refused, durably. @@ -137,7 +137,7 @@ func TestManifestStateAndReads(t *testing.T) { // 1135/AC6 if !reflect.DeepEqual(m.Claims, []claim{{Name: "mesh-watchdogs", Scope: "mesh", Serves: []string{"signals", "stuck", "presence", "dead-letters", "resync"}}}) { t.Errorf("claims = %+v", m.Claims) } - if !reflect.DeepEqual(m.Tools, []string{"watchman_findings", "watchman_first_views", "watchman_first_view_again"}) { + if !reflect.DeepEqual(m.Tools, []string{"watchman_findings", "watchman_first_views", "watchman_first_view_again", "watchman_views"}) { t.Errorf("tools = %v", m.Tools) } for _, k := range []string{"emits", "context", "items", "provides"} { diff --git a/modules/mesh-delivery-watchman/module.json b/modules/mesh-delivery-watchman/module.json index a636cee8..52e7103b 100644 --- a/modules/mesh-delivery-watchman/module.json +++ b/modules/mesh-delivery-watchman/module.json @@ -25,7 +25,8 @@ "mesh-delivery-assigner.assigner_read_wants", "mesh-delivery-indexer.indexer_pending", "mesh-delivery-indexer.indexer_read_modules", - "seat:mesh-hand-acts.hand-act" + "seat:mesh-hand-acts.hand-act", + "seat:mesh-conditions.conditions" ], "consumes": [ "mesh-delivery-assigner.views.wants", @@ -73,7 +74,8 @@ "tools": [ "watchman_findings", "watchman_first_views", - "watchman_first_view_again" + "watchman_first_view_again", + "watchman_views" ], "state": [ {