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:
@@ -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`.
|
||||
|
||||
Reference in New Issue
Block a user