Hand-acts recorder is mesh-sdk's views.HandActs; the conditions client writes a part's findings as views.FindingWriter (ADR 0634 §4, ADR 0635 §4; issue 1966)

This commit is contained in:
2026-11-05 14:16:53 +01:00
parent 97b7da6128
commit af804f5abf
6 changed files with 115 additions and 0 deletions
@@ -11,6 +11,7 @@ import (
"time"
seat "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions"
"git.novox.be/novox/mesh-sdk/go/views"
)
// The two severities, how soon the operator is needed.
@@ -170,3 +171,44 @@ func SilencedAt(c seat.ConditionsByKeyValue, now time.Time) bool {
func Escalated(c seat.ConditionsByKeyValue) bool {
return c.Resolver == ResolverOperator && len(c.Tried) > 0
}
// FindingVersion is the layout version of a finding's value.
const FindingVersion = 1
// FindingState is a part's own state `findings`, where the condition keeper reads what a part saw: the runtime's kept
// state (mesh-sdk's KeptState) satisfies it.
type FindingState interface {
Put(key string, value any) (uint64, error)
Delete(key string) error
}
// FindingWriter writes and clears a part's findings of a module in its own state `findings`, in this seat's layout; it
// is mesh-sdk's views.FindingWriter. A finding is a warning about the module its ID names, and is checked
// (CheckFinding) before it is written.
type FindingWriter struct{ State FindingState }
var _ views.FindingWriter = FindingWriter{}
// FindingFor is the value and key of the seat's finding for f.
func FindingFor(f views.Finding) seat.FindingsByKeyValue {
at := f.At.UTC().Format(time.RFC3339Nano)
return seat.FindingsByKeyValue{V: FindingVersion, Key: Key(ScopeModule, f.ID, f.Kind), Kind: f.Kind,
Subject: seat.Subject{Scope: ScopeModule, ID: f.ID}, Severity: Warning, Summary: f.Summary, Headline: f.Headline,
Explanation: f.Explanation, Evidence: []seat.Evidence{{At: at, Said: f.Summary}}, Source: f.Observer + "." + f.Kind,
Observer: f.Observer, At: at}
}
// Put writes the finding.
func (w FindingWriter) Put(f views.Finding) error {
v := FindingFor(f)
if err := CheckFinding(v); err != nil {
return err
}
_, err := w.State.Put(v.Key, v)
return err
}
// Delete clears the finding.
func (w FindingWriter) Delete(f views.Finding) error {
return w.State.Delete(Key(ScopeModule, f.ID, f.Kind))
}
@@ -6,6 +6,7 @@ import (
"time"
seat "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions"
"git.novox.be/novox/mesh-sdk/go/views"
)
// ADR 0564 clause 3: the key rule, the checks of a finding, the order and the silence a person reads conditions by.
@@ -60,3 +61,41 @@ func TestAFindingsKeyAndTheOrderOfConditions(t *testing.T) {
t.Errorf("the seat's addresses: %+v", a)
}
}
type fakeFindings struct {
put map[string]any
deleted []string
}
func (f *fakeFindings) Put(key string, value any) (uint64, error) {
if f.put == nil {
f.put = map[string]any{}
}
f.put[key] = value
return 1, nil
}
func (f *fakeFindings) Delete(key string) error { f.deleted = append(f.deleted, key); return nil }
func TestTheFindingWriterWritesAndClearsAModulesFindingInTheSeatsLayout(t *testing.T) {
state := &fakeFindings{}
var w views.FindingWriter = FindingWriter{State: state}
f := views.Finding{Observer: "mesh-config", Kind: "unanswered", ID: "mesh-config.mesh-vault.secrets", Summary: "no first view",
Headline: "waits", Explanation: "look", At: time.Date(2026, 11, 5, 12, 0, 0, 0, time.UTC)}
if err := w.Put(f); err != nil {
t.Fatal(err)
}
key := "module.mesh-config.mesh-vault.secrets.unanswered"
v, ok := state.put[key].(seat.FindingsByKeyValue)
if !ok || v.Key != key || v.Severity != Warning || v.Source != "mesh-config.unanswered" || v.Subject.Scope != ScopeModule || len(v.Evidence) != 1 {
t.Fatalf("written %#v", state.put)
}
if err := CheckFinding(v); err != nil {
t.Error(err)
}
if err := w.Delete(f); err != nil || len(state.deleted) != 1 || state.deleted[0] != key {
t.Errorf("deleted %v: %v", state.deleted, err)
}
if err := w.Put(views.Finding{Observer: "x", Kind: "k", ID: "i"}); err == nil {
t.Error("a finding that says nothing was written")
}
}
@@ -3,3 +3,5 @@ module git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshcondit
go 1.26.0
require git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions v0.0.0-20261105000000-000000000000
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174118-f94abb95aaef
@@ -10,6 +10,7 @@ import (
"strings"
seat "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshhandacts"
"git.novox.be/novox/mesh-sdk/go/views"
)
// The verbs an act is recorded under in the log, and the causes and kinds that are no repair.
@@ -71,6 +72,15 @@ func (r Recorder) Record(q Request) (Taken, error) {
return r.record(seat.VerbHandAct, body)
}
// RecordHandAct records a part's act before it does it, as mesh-sdk's views.HandActs asks: the recorder is the
// interface's implementation, and an act the seat does not take is the error that says nothing was recorded.
func (r Recorder) RecordHandAct(a views.HandAct) error {
_, err := r.Record(Request{What: a.What, Why: a.Why, Cause: a.Cause, Verb: a.Verb})
return err
}
var _ views.HandActs = Recorder{}
// RecordWarrant records what the asking module did on the operator's warrant for its ask.
func (r Recorder) RecordWarrant(asker, ask string) (Taken, error) {
return r.record(seat.VerbWarranted, map[string]any{"asker": asker, "ask": ask})
@@ -3,11 +3,13 @@ package meshhandactsclient
import (
"encoding/json"
"errors"
"fmt"
"reflect"
"strings"
"testing"
seat "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshhandacts"
"git.novox.be/novox/mesh-sdk/go/views"
)
// ADR 0565 clauses 10 and 11 (issue 1953 L5-S2, S4, L5b): an act reads the controller's entries unchanged, and a
@@ -71,3 +73,21 @@ func TestAnActIsRecordedInTheLogThatAnswers(t *testing.T) {
t.Error("an act with no why was recorded")
}
}
func TestTheRecorderIsMeshSDKsHandActRecorder(t *testing.T) {
var asked []string
r := Recorder{Ask: func(key string, body any) (json.RawMessage, error) {
asked = append(asked, key+" "+fmt.Sprint(body))
return nil, nil
}}
var acts views.HandActs = r
if err := acts.RecordHandAct(views.HandAct{What: "asked a view again", Why: "it is stale", Cause: "stale"}); err != nil {
t.Fatal(err)
}
if len(asked) != 1 || !strings.HasPrefix(asked[0], "seat:mesh-hand-acts.hand-act ") || !strings.Contains(asked[0], "asked a view again") {
t.Errorf("asked %v", asked)
}
if err := acts.RecordHandAct(views.HandAct{What: "x"}); err == nil {
t.Error("an act with no why was taken")
}
}
@@ -3,3 +3,5 @@ module git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandac
go 1.26.0
require git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshhandacts v0.0.0-20261105000000-000000000000
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174118-f94abb95aaef