COND A-22: the keeper holds findings as the contract's generic Finding and takes its own through the writer's wire form

Builds: design 47 G §434, §437, §439, A-3, A-22 (G-20)

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
This commit is contained in:
2026-11-11 21:58:08 +01:00
parent a587125047
commit 05fcebf9c5
7 changed files with 103 additions and 53 deletions
@@ -5,7 +5,6 @@ package main
// `findings-unstated`, cleared by its next `findings-stated`.
import (
"encoding/json"
"errors"
"fmt"
"sort"
@@ -144,17 +143,17 @@ func (k *Keeper) AskPassed(bound string) error {
return nil
}
now := k.Now().UTC()
f := seat.FindingsByKeyValue{V: cc.FindingVersion, Key: unstatedKey(observer, node), Kind: KindFindingsUnstated,
f := seat.Finding{Scope: cc.ScopeModule, ID: observer + "." + node, Kind: KindFindingsUnstated, Since: now,
Subject: seat.Subject{Scope: cc.ScopeModule, ID: observer + "." + node, Machine: node},
Severity: cc.Warning, Summary: fmt.Sprintf("%s on %s stated no findings within %s of the ask", observer, node, k.askedWithin()),
Headline: observer + " on " + node + " did not state its findings",
Explanation: "The keeper asked " + observer + " on " + node + " to state its findings and heard nothing; its conditions stand as they were.",
Source: "findings-asked", Observer: Module, At: now, Evidence: []seat.Evidence{{At: now, Said: "ask " + ask + " unanswered"}}}
raw, err := json.Marshal(f)
key, raw, err := storedFinding(f)
if err != nil {
return err
}
return k.Finding(Module, f.Key, raw, false)
return k.Finding(Module, key, raw, false)
}
func unstatedKey(observer, node string) string {
@@ -94,8 +94,7 @@ func (k *Keeper) FindingWritten(observer, node string, body []byte) error {
// take holds one finding an observer says on a node, and raises or changes its condition.
func (k *Keeper) take(observer, node string, f seat.Finding) error {
v := storedFinding(f)
value, err := json.Marshal(v)
key, value, err := storedFinding(f)
if err != nil {
return err
}
@@ -104,15 +103,15 @@ func (k *Keeper) take(observer, node string, f seat.Finding) error {
if k.keyNode[observer] == nil {
k.keyNode[observer] = map[string]string{}
}
k.keyNode[observer][v.Key] = node
named := k.view(observer, node).Pending[v.Key]
delete(k.view(observer, node).Pending, v.Key)
k.keyNode[observer][key] = node
named := k.view(observer, node).Pending[key]
delete(k.view(observer, node).Pending, key)
k.mu.Unlock()
if !named.IsZero() {
_ = k.Store.Delete(BoundsState, pendingKey(observer, node, v.Key))
_ = k.Store.Delete(BoundsState, pendingKey(observer, node, key))
}
k.saveView(observer, node)
return k.Finding(observer, v.Key, value, false)
return k.Finding(observer, key, value, false)
}
// FindingCleared clears the condition of the finding an observer no longer observes.
@@ -145,20 +144,26 @@ func (k *Keeper) FindingCleared(observer, node string, body []byte) error {
return k.Finding(observer, key, nil, true)
}
// storedFinding is the event's finding as the keeper holds one.
func storedFinding(f seat.Finding) seat.FindingsByKeyValue {
// storedFinding is the event's finding as the keeper holds one, with its key: the finding as the contract has it (design 47
// G §437, A-22), under the key its scope, id and ref or kind make.
func storedFinding(f seat.Finding) (string, []byte, error) {
if f.Evidence == nil {
f.Evidence = []seat.Evidence{}
}
if f.Since.IsZero() {
f.Since = f.At
}
value, err := json.Marshal(f)
return findingKey(f), value, err
}
// findingKey is the key a finding is held under: its scope, its id, and its ref where it has one, else its kind (ADR 0564).
func findingKey(f seat.Finding) string {
token := f.Ref
if token == "" {
token = f.Kind
}
evidence := f.Evidence
if evidence == nil {
evidence = []seat.Evidence{}
}
return seat.FindingsByKeyValue{V: cc.FindingVersion, Key: cc.Key(f.Scope, f.ID, token), Kind: f.Kind, Subject: f.Subject,
Severity: f.Severity, Summary: f.Summary, Headline: f.Headline, Explanation: f.Explanation, Needs: f.Needs,
Actions: f.Actions, Evidence: evidence, Source: f.Source, Resolver: f.Resolver, Observer: f.Observer, Owner: f.Owner,
Ref: f.Ref, Run: f.Run, WaitingOn: f.WaitingOn, At: f.At}
return cc.Key(f.Scope, f.ID, token)
}
// MachinePresence takes the watchman's word that a machine is away or back (design 47 G §389): an observer on a machine
@@ -319,7 +324,7 @@ func (k *Keeper) FindingsStated(observer, node string, body []byte) error {
v.Asks = map[string]time.Time{}
named := map[string]bool{}
for _, f := range s.Findings {
named[storedFinding(f).Key] = true
named[findingKey(f)] = true
}
oldPending := v.Pending
v.Pending = map[string]time.Time{}
@@ -88,7 +88,7 @@ func (k *Keeper) motion(node, module string) (motion.Motion, motion.Want) {
}
// holdFor says whether key is held now, and as what. Called with mu held.
func (k *Keeper) holdFor(key string, f *seat.FindingsByKeyValue, open bool) (held, bool) {
func (k *Keeper) holdFor(key string, f *seat.Finding, open bool) (held, bool) {
if open || f == nil {
return held{}, false
}
@@ -111,7 +111,7 @@ type Keeper struct {
AskBound time.Duration
mu sync.Mutex
findings map[string]map[string]seat.FindingsByKeyValue
findings map[string]map[string]seat.Finding
blind map[string]string
attempts map[string]meshhealers.AttemptsByAttemptValue
wants map[string]motion.Want
@@ -157,7 +157,7 @@ type Keeper struct {
func (k *Keeper) init() {
if k.findings == nil {
k.findings, k.blind, k.attempts = map[string]map[string]seat.FindingsByKeyValue{}, map[string]string{},
k.findings, k.blind, k.attempts = map[string]map[string]seat.Finding{}, map[string]string{},
map[string]meshhealers.AttemptsByAttemptValue{}
k.wants, k.haves, k.stuck, k.held, k.cleared = map[string]motion.Want{}, map[string]motion.Have{},
map[string]string{}, map[string]held{}, map[string]clearedMemo{}
@@ -180,7 +180,7 @@ func (k *Keeper) Finding(observer, key string, value []byte, deleted bool) error
k.mu.Lock()
k.init()
if k.findings[observer] == nil {
k.findings[observer] = map[string]seat.FindingsByKeyValue{}
k.findings[observer] = map[string]seat.Finding{}
}
if deleted {
delete(k.findings[observer], key)
@@ -191,9 +191,9 @@ func (k *Keeper) Finding(observer, key string, value []byte, deleted bool) error
k.Logf("%s's finding %s is not one the keeper can raise, so it is left as it stood: %v", observer, key, err)
return nil
}
if f.Key != key {
if fk := findingKey(f); fk != key {
k.mu.Unlock()
k.Logf("%s's finding under %s names the key %s, so it is left as it stood", observer, key, f.Key)
k.Logf("%s's finding under %s names the key %s, so it is left as it stood", observer, key, fk)
return nil
}
k.findings[observer][key] = f
@@ -314,7 +314,7 @@ func (k *Keeper) settleAll(keys []string) error {
// wanted is what the inputs say key should be: a finding of an observer. Nil when nothing says it; nothing is carried
// from the controller's bucket (issue 1126).
func (k *Keeper) wanted(key string) *seat.FindingsByKeyValue {
func (k *Keeper) wanted(key string) *seat.Finding {
observers := make([]string, 0, len(k.findings))
for o := range k.findings {
observers = append(observers, o)
@@ -390,7 +390,7 @@ func (k *Keeper) settleOnce(key string) error {
}
// derive is the condition key should be now, from what was open and what the inputs say.
func (k *Keeper) derive(key string, was Condition, open bool, f *seat.FindingsByKeyValue, now time.Time) Condition {
func (k *Keeper) derive(key string, was Condition, open bool, f *seat.Finding, now time.Time) Condition {
var c Condition
switch {
case open:
@@ -430,10 +430,10 @@ func (k *Keeper) derive(key string, was Condition, open bool, f *seat.FindingsBy
// fromFinding is what the condition says of its finding: its words, subject, severity and source, and its evidence
// newest first (clause 2). A healer's escalation is not undone by the finding saying a warning again.
func (k *Keeper) fromFinding(c *Condition, f seat.FindingsByKeyValue, now time.Time) {
func (k *Keeper) fromFinding(c *Condition, f seat.Finding, now time.Time) {
escalated := c.Escalated()
c.Kind, c.Subject, c.Summary, c.Source = f.Kind, f.Subject, f.Summary, f.Source
c.Headline, c.Explanation, c.Resolved, c.Needs, c.Actions = f.Headline, f.Explanation, f.Resolved, f.Needs, f.Actions
c.Headline, c.Explanation, c.Needs, c.Actions = f.Headline, f.Explanation, f.Needs, f.Actions
if !(escalated && f.Severity != cc.Urgent) {
c.Severity = f.Severity
}
@@ -1,6 +1,7 @@
package main
import (
"context"
"encoding/json"
"fmt"
nodeengine "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
@@ -17,6 +18,7 @@ import (
seat "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions"
meshhealers "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshhealers"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/views"
)
// fakeStore is the bus's states in memory, with revisions, as the runtime answers them.
@@ -162,12 +164,11 @@ func (r *rig) open(key string) (Condition, bool) {
}
// encodeFinding writes a finding as an observer does: the seat's layout, checked.
func encodeFinding(f seat.FindingsByKeyValue) ([]byte, error) {
f.V = cc.FindingVersion
func encodeFinding(f seat.Finding) ([]byte, error) {
if f.Evidence == nil {
f.Evidence = []seat.Evidence{}
}
if err := cc.CheckFinding(f); err != nil {
if err := views.CheckFinding(f); err != nil {
return nil, err
}
return json.Marshal(f)
@@ -176,14 +177,14 @@ func encodeFinding(f seat.FindingsByKeyValue) ([]byte, error) {
// finding is an observer's finding as mesh-sdk's layout writes it (encodeFinding).
func finding(t *testing.T, observer, scope, id, kind, source string, sev string, at time.Time) (string, []byte) {
t.Helper()
f := seat.FindingsByKeyValue{Key: cc.Key(scope, id, kind), Kind: kind, Subject: seat.Subject{Scope: scope, ID: id},
f := seat.Finding{Scope: scope, ID: id, Since: at, Kind: kind, Subject: seat.Subject{Scope: scope, ID: id},
Severity: sev, Summary: kind + " of " + id, Headline: kind, Explanation: "It is " + kind + ".", Source: source,
Observer: observer, At: at, Evidence: []seat.Evidence{{At: at, Said: kind}}}
raw, err := encodeFinding(f)
if err != nil {
t.Fatal(err)
}
return f.Key, raw
return cc.Key(scope, id, kind), raw
}
func mustJSON(t *testing.T, v any) []byte {
@@ -694,3 +695,28 @@ func TestTheKeeperSetsItsViewFromWhatAnObserverStates(t *testing.T) {
}
}
}
// The keeper's own finding goes in through the generic writer's wire form and is held as the contract's Finding, one that is
// not a finding the contract allows is refused, and a clear removes it (design 47 G A-22, §687).
func TestTheKeepersOwnFindingIsTheContractsFindingByItsWireForm(t *testing.T) {
r := newRig()
own := ownFindings{r.k}
wire := map[string]any{"kind": "view-unanswered", "scope": "module", "id": "mesh-conditions.assigner.wants", "severity": "warning",
"source": "views", "subject": map[string]any{"scope": "module", "id": Module}, "summary": "no first view", "at": r.now}
if err := own.WriteWire(context.Background(), wire); err != nil {
t.Fatal(err)
}
key := cc.Key("module", "mesh-conditions.assigner.wants", "view-unanswered")
if c, ok := r.open(key); !ok || c.Summary != "no first view" || c.Kind != "view-unanswered" {
t.Fatalf("the finding raised no condition: %+v %v", c, ok)
}
if err := own.WriteWire(context.Background(), map[string]any{"scope": "module", "id": "x", "kind": "a.b", "severity": "warning"}); err == nil {
t.Fatal("a kind with a dot was taken")
}
if err := own.Clear(context.Background(), "module", "mesh-conditions.assigner.wants", "view-unanswered", "no longer observed"); err != nil {
t.Fatal(err)
}
if _, ok := r.open(key); ok {
t.Fatal("the condition stands after its finding was cleared")
}
}
@@ -18,6 +18,7 @@ import (
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient"
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient"
seat "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshwantsclient"
catseat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
@@ -170,24 +171,45 @@ func NewViews(k *Keeper) *views.Reader {
return v
}
// ownFindings takes the keeper's own finding straight into its picture of findings.
// ownFindings takes the keeper's own finding straight into its picture of findings. It is the WireWriter a views reader
// writes through (design 47 G §687, A-22): the finding's wire form, read as the contract's Finding, as an observer's is.
type ownFindings struct{ k *Keeper }
func (o ownFindings) Put(f views.Finding) error {
v := meshconditionsclient.FindingFor(f)
if err := meshconditionsclient.CheckFinding(v); err != nil {
return err
}
value, err := json.Marshal(v)
func (o ownFindings) WriteWire(_ context.Context, wire map[string]any) error {
raw, err := json.Marshal(wire)
if err != nil {
return err
}
return o.k.Finding(Module, v.Key, value, false)
var f seat.Finding
if err := json.Unmarshal(raw, &f); err != nil {
return fmt.Errorf("the keeper's own finding is not readable: %w", err)
}
f.Observer = Module
if f.At.IsZero() {
f.At = o.k.Now().UTC()
}
if f.Since.IsZero() {
f.Since = f.At
}
if f.Evidence == nil {
f.Evidence = []seat.Evidence{}
}
if err := views.CheckFinding(f); err != nil {
return err
}
key, value, err := storedFinding(f)
if err != nil {
return err
}
return o.k.Finding(Module, key, value, false)
}
func (o ownFindings) Delete(f views.Finding) error {
return o.k.Finding(Module, meshconditionsclient.Key(meshconditionsclient.ScopeModule, f.ID, f.Kind), nil, true)
func (o ownFindings) Clear(_ context.Context, scope, id, kind, _ string) error {
return o.k.Finding(Module, meshconditionsclient.Key(scope, id, kind), nil, true)
}
func (ownFindings) ModuleNode() (string, string) { return Module, "" }
func main() {
k := NewKeeper()
v := NewViews(k)
@@ -13,6 +13,7 @@ import (
seat "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions"
meshhealers "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshhealers"
"git.novox.be/novox/mesh-sdk/go/views"
cc "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient"
)
@@ -213,8 +214,8 @@ func order(list []Condition) {
// decodeFinding reads an observer's finding of the seat's state `findings`, refusing one the keeper could not make a
// condition of. A finding written before `ref` was named has it as `token`.
func decodeFinding(data []byte) (seat.FindingsByKeyValue, error) {
var f seat.FindingsByKeyValue
func decodeFinding(data []byte) (seat.Finding, error) {
var f seat.Finding
if err := json.Unmarshal(data, &f); err != nil {
return f, fmt.Errorf("a finding is not readable: %w", err)
}
@@ -225,10 +226,7 @@ func decodeFinding(data []byte) (seat.FindingsByKeyValue, error) {
_ = json.Unmarshal(data, &old)
f.Ref = old.Token
}
if f.V != cc.FindingVersion {
return f, fmt.Errorf("a finding of version %d, and this reads %d", f.V, cc.FindingVersion)
}
return f, cc.CheckFinding(f)
return f, views.CheckFinding(f)
}
// triedOf is the attempt as the condition carries it in `tried`.