mesh-delivery-sdk: a part's findings state is written through the seat mesh-conditions' client (findings.Writer over its bucket), and the family-not-heard finding is encoded by it (ADR 0564 §2, ADR 0635 §4; issue 1966)

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
This commit is contained in:
2026-11-05 14:42:38 +01:00
parent da2f8cfc10
commit 145cc70ceb
4 changed files with 127 additions and 8 deletions
+54
View File
@@ -0,0 +1,54 @@
// Package findings is a part's own bucket `findings` as the state the seat mesh-conditions' client writes into
// (meshconditionsclient.FindingState), so a part gives mesh-sdk's views the FindingWriter of the seat's layout without
// each writing the same adapter over its bucket (novox/hq ADR 0564 §2, ADR 0635 §4).
package findings
import (
"context"
"encoding/json"
"errors"
"github.com/nats-io/nats.go/jetstream"
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient"
"git.novox.be/novox/mesh-sdk/go/views"
)
// ErrUnbound says the bucket is not on the bus yet, so the finding is not written (the part says it in its log).
var ErrUnbound = errors.New("the findings state is not bound yet")
// State is the bucket the part's state `findings` is, given when it is bound (process.AwaitedBucket.KV): nil is not bound yet.
type State struct {
Ctx context.Context
KV func() jetstream.KeyValue
}
var _ meshconditionsclient.FindingState = State{}
// Put writes one value under its key as JSON.
func (s State) Put(key string, value any) (uint64, error) {
kv := s.KV()
if kv == nil {
return 0, ErrUnbound
}
data, err := json.Marshal(value)
if err != nil {
return 0, err
}
return kv.Put(s.Ctx, key, data)
}
// Delete clears one key.
func (s State) Delete(key string) error {
kv := s.KV()
if kv == nil {
return ErrUnbound
}
return kv.Delete(s.Ctx, key)
}
// Writer is the part's views.FindingWriter over its bucket; the function is what views.ProcessReader.Findings and the
// emitter's Findings take.
func Writer(ctx context.Context, kv func() jetstream.KeyValue) views.FindingWriter {
return meshconditionsclient.FindingWriter{State: State{Ctx: ctx, KV: kv}}
}
+59
View File
@@ -0,0 +1,59 @@
package findings
import (
"context"
"encoding/json"
"errors"
"testing"
"time"
"github.com/nats-io/nats.go/jetstream"
"git.novox.be/novox/mesh-sdk/go/views"
)
// fakeKV is the two calls a findings state makes of its bucket.
type fakeKV struct {
jetstream.KeyValue
held map[string][]byte
}
func (f *fakeKV) Put(_ context.Context, key string, value []byte) (uint64, error) {
f.held[key] = value
return uint64(len(f.held)), nil
}
func (f *fakeKV) Delete(_ context.Context, key string, _ ...jetstream.KVDeleteOpt) error {
delete(f.held, key)
return nil
}
// A part's finding is written in the seat mesh-conditions' layout under the key the condition keeper reads, cleared by the
// same finding, and refused while the bucket is not bound (the part says that in its log).
func TestAFindingIsWrittenInTheSeatsLayoutAndClearedAndRefusedUnbound(t *testing.T) {
kv := &fakeKV{held: map[string][]byte{}}
var bound jetstream.KeyValue
w := Writer(context.Background(), func() jetstream.KeyValue { return bound })
f := views.Finding{Observer: "mesh-delivery-teller", Kind: views.UnansweredKind, ID: "mesh-delivery-teller.asks", Summary: "s", Headline: "h",
Explanation: "e", At: time.Date(2026, 11, 5, 15, 0, 0, 0, time.UTC)}
if err := w.Put(f); !errors.Is(err, ErrUnbound) {
t.Fatalf("an unbound bucket took the finding: %v", err)
}
bound = kv
if err := w.Put(f); err != nil {
t.Fatal(err)
}
var got struct{ Key, Kind, Observer, Source string }
for key, value := range kv.held {
if err := json.Unmarshal(value, &got); err != nil || got.Key != key || got.Kind != views.UnansweredKind ||
got.Observer != f.Observer || got.Source != f.Observer+"."+f.Kind {
t.Fatalf("%s holds %s (%v)", key, value, err)
}
}
if len(kv.held) != 1 {
t.Fatalf("the bucket holds %d findings, want 1", len(kv.held))
}
if err := w.Delete(f); err != nil || len(kv.held) != 0 {
t.Fatalf("the finding is not cleared: %v, %d left", err, len(kv.held))
}
}
+2
View File
@@ -4,6 +4,8 @@ go 1.26.0
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f
require git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient v0.0.0-00010101000000-000000000000
require git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639 // indirect
require (
+12 -8
View File
@@ -7,11 +7,13 @@
package heard
import (
"encoding/json"
"fmt"
"strings"
"time"
"git.novox.be/novox/mesh-sdk/go/health"
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient"
"git.novox.be/novox/mesh-sdk/go/views"
)
// Kind is the finding's kind.
@@ -23,7 +25,7 @@ func ID(observer, family string) string { return observer + "." + family }
// Key is the finding's key in the observer's findings state.
func Key(observer, family string) string {
return health.FindingKey(health.ScopeModule, ID(observer, family), Kind)
return meshconditionsclient.Key(meshconditionsclient.ScopeModule, ID(observer, family), Kind)
}
// Words are the finding's words: its summary, headline and explanation. consequence says what the observer misses while
@@ -41,14 +43,16 @@ func Words(observer, family, consequence string, why error) (summary, headline,
return summary, headline, explanation
}
// Finding is observer's finding that it does not hear family, as health.Finding encodes it.
// Finding is observer's finding that it does not hear family, in the layout of the seat mesh-conditions' state findings.
func Finding(observer, family, consequence string, why error, at time.Time) (string, []byte, error) {
key := Key(observer, family)
summary, headline, explanation := Words(observer, family, consequence, why)
value, err := health.EncodeFinding(health.Finding{Key: key, Kind: Kind, Subject: health.Subject{Scope: health.ScopeModule, ID: ID(observer, family)},
Severity: health.Warning, Summary: summary, Headline: headline, Explanation: explanation,
Evidence: []health.Evidence{{At: at.UTC(), Said: summary}}, Source: observer + "." + Kind, Observer: observer, At: at.UTC()})
return key, value, err
v := meshconditionsclient.FindingFor(views.Finding{Observer: observer, Kind: Kind, ID: ID(observer, family), Summary: summary,
Headline: headline, Explanation: explanation, At: at})
if err := meshconditionsclient.CheckFinding(v); err != nil {
return v.Key, nil, err
}
value, err := json.Marshal(v)
return v.Key, value, err
}
// Writer writes and deletes the finding in the observer's findings state. Put or Delete nil writes nothing, said in the