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:
@@ -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"} {
|
||||
|
||||
@@ -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": [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user