WM8: watchman_views answers the condition keeper's view-waits by part, node and view

Builds: design 47 G §83, §501, §609
Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
2026-11-12 05:38:23 +01:00
parent 7ba95ac07f
commit 05b0385805
4 changed files with 106 additions and 8 deletions
@@ -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, <scope>.<id>.<kind>, 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 <module>_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 <scope>.<id>.<kind> 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)
@@ -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)
}
}
@@ -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"} {
+4 -2
View File
@@ -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": [
{