COND10 (stated kinds, both subject forms): the keeper clears only conditions of the kinds a statement covers

A findings-stated carrying kinds sets the keeper's view of that observer, node and set of kinds, and clears only the
conditions of those kinds it does not name; no kinds covers every kind. The finding events are taken from either form of the
part's finding stream, the observer and node read from the subject.

Builds: design 47 G §580, §529 (A-30), §383, §384

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
2026-11-11 22:50:17 +01:00
parent b15c511528
commit 72ccb75c1b
3 changed files with 146 additions and 15 deletions
@@ -347,6 +347,7 @@ type keptView struct {
Pending map[string]time.Time `json:"pending,omitempty"`
Asks map[string]time.Time `json:"asks,omitempty"`
Findings []seat.Finding `json:"findings,omitempty"`
Sets map[string]time.Time `json:"sets,omitempty"`
}
// saveView keeps what the keeper heard of the observer on the node, by compare-and-set; a source is kept once it is heard.
@@ -360,7 +361,7 @@ func (k *Keeper) saveView(observer, node string) {
}
v.kept = true
value := keptView{Observer: observer, Node: node, At: v.At, Pending: maps.Clone(v.Pending), Asks: maps.Clone(v.Asks),
Findings: slices.Clone(v.Findings)}
Findings: slices.Clone(v.Findings), Sets: maps.Clone(v.Sets)}
k.mu.Unlock()
key := viewKey(observer, node)
for range ConditionWrites {
@@ -403,6 +404,9 @@ func (k *Keeper) RestoreViews() {
v.At, v.Findings, v.kept = kv.At, kv.Findings, true
maps.Copy(v.Pending, kv.Pending)
maps.Copy(v.Asks, kv.Asks)
if len(kv.Sets) > 0 {
v.Sets = maps.Clone(kv.Sets)
}
k.mu.Unlock()
}
}
@@ -35,13 +35,35 @@ func emitterOf(key, event string) (string, bool) {
return m, ok && m != "" && !strings.Contains(m, ".")
}
// subjectOf reads the observer, the event and the node out of an event's key in either form of a part's finding stream
// (design 47 G §529): the old form `mesh.stream.<module>.event.<event>.<node>`, which the runtime delivers as
// `<module>.<event>.<node>` (or as `<module>.<event>` with the node in the envelope), and the template form
// `mesh.bus.<module>.finding-events.<event>.<node>`. The node of the key is the subject's alone; the envelope's is the
// fallback of a key without one.
func subjectOf(key, envelopeNode string) (module, event, node string, ok bool) {
parts := strings.Split(key, ".")
switch {
case len(parts) == 6 && parts[0] == "mesh" && parts[1] == "bus" && parts[3] == FindingEventsTemplate:
return parts[2], parts[4], parts[5], true
case len(parts) == 3:
return parts[0], parts[1], parts[2], true
case len(parts) == 2:
return parts[0], parts[1], envelopeNode, true
}
return "", "", "", false
}
// FindingEvent is one `finding-written` or `finding-cleared` heard from an observer on a node.
func (k *Keeper) FindingEvent(e stdio.Envelope) error {
if observer, ok := emitterOf(e.Key, EventFindingWritten); ok {
return k.FindingWritten(observer, e.Node, e.Body)
}
if observer, ok := emitterOf(e.Key, EventFindingCleared); ok {
return k.FindingCleared(observer, e.Node, e.Body)
if module, event, node, ok := subjectOf(e.Key, e.Node); ok {
switch event {
case EventFindingWritten:
return k.FindingWritten(module, node, e.Body)
case EventFindingCleared:
return k.FindingCleared(module, node, e.Body)
case EventFindingsStated:
return k.FindingsStated(module, node, e.Body)
}
}
if _, ok := emitterOf(e.Key, watchdogs.EventMachineAway); ok && strings.HasPrefix(e.Key, watchdogs.Seat+".") {
return k.MachinePresence(e.Body, true)
@@ -55,9 +77,6 @@ func (k *Keeper) FindingEvent(e stdio.Envelope) error {
if e.Key == broker.Seat+"."+broker.EventCreated {
return k.BrokerCreated(e.Body)
}
if observer, ok := emitterOf(e.Key, EventFindingsStated); ok {
return k.FindingsStated(observer, e.Node, e.Body)
}
return nil
}
@@ -244,6 +263,9 @@ type sourceView struct {
At time.Time
Pending map[string]time.Time
Findings []seat.Finding
// Sets are the last findings-stated's at of each set of kinds a statement covers (design 47 G §580), keyed by the kinds
// sorted and joined by a comma; the empty key is a statement with no kinds, which covers every kind.
Sets map[string]time.Time
// Asks are the asks it has not answered, with the end of their bound (asks.go).
Asks map[string]time.Time
// kept says the source is in the state `findings-views` (catalog.go).
@@ -313,13 +335,29 @@ func (k *Keeper) FindingsStated(observer, node string, body []byte) error {
k.mu.Lock()
k.init()
v := k.view(observer, node)
if !v.At.IsZero() && !s.At.After(v.At) {
last := v.Sets[setOf(s.Kinds)]
if len(s.Kinds) == 0 && last.IsZero() {
last = v.At
}
if !last.IsZero() && !s.At.After(last) {
k.mu.Unlock()
k.Logf("%s's findings-stated on %s at %s is not newer than the one at %s: left out", observer, node, s.At, v.At)
k.Logf("%s's findings-stated on %s at %s is not newer than the one at %s: left out", observer, node, s.At, last)
return nil
}
v.At = s.At
v.Findings = s.Findings
covered := func(kind string) bool { return len(s.Kinds) == 0 || slices.Contains(s.Kinds, kind) }
v.At = later(v.At, s.At)
if v.Sets == nil {
v.Sets = map[string]time.Time{}
}
v.Sets[setOf(s.Kinds)] = s.At
// Only the kinds the statement covers are replaced: another bundle's findings and pending keys stand (§580).
kept := v.Findings[:0:0]
for _, f := range v.Findings {
if !covered(f.Kind) {
kept = append(kept, f)
}
}
v.Findings = append(kept, s.Findings...)
answered := v.Asks
v.Asks = map[string]time.Time{}
named := map[string]bool{}
@@ -328,6 +366,11 @@ func (k *Keeper) FindingsStated(observer, node string, body []byte) error {
}
oldPending := v.Pending
v.Pending = map[string]time.Time{}
for key, until := range oldPending {
if !covered(keyKind(key)) {
v.Pending[key] = until
}
}
type pend struct {
key string
deadline time.Time
@@ -343,8 +386,8 @@ func (k *Keeper) FindingsStated(observer, node string, body []byte) error {
pending = append(pending, pend{p.Key, s.At.Add(bound)})
}
var drop []string
for key := range k.findings[observer] {
if !named[key] && (once || k.keyNode[observer][key] == node) {
for key, f := range k.findings[observer] {
if !named[key] && covered(f.Kind) && (once || k.keyNode[observer][key] == node) {
drop = append(drop, key)
}
}
@@ -402,6 +445,28 @@ func (k *Keeper) FindingsStated(observer, node string, body []byte) error {
return nil
}
// setOf is the key of a set of kinds: the kinds sorted and joined by a comma; none is the empty key, every kind.
func setOf(kinds []string) string {
sorted := slices.Clone(kinds)
sort.Strings(sorted)
return strings.Join(sorted, ",")
}
// keyKind is the last token of a finding's key, its kind (or its ref, §14 of ADR 0564).
func keyKind(key string) string {
if i := strings.LastIndex(key, "."); i >= 0 {
return key[i+1:]
}
return key
}
func later(a, b time.Time) time.Time {
if b.After(a) {
return b
}
return a
}
// PendingPassed is a pending key's bound passing: unless a finding or a later set named it since, its condition is cleared.
func (k *Keeper) PendingPassed(bound string) error {
rest := strings.TrimPrefix(bound, pendingPrefix)
@@ -4,6 +4,8 @@ import (
"testing"
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
cc "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient"
seat "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions"
)
@@ -79,3 +81,63 @@ func TestTheKeeperHandsAFailedHeadToTheOperatorAfterTwoHours(t *testing.T) {
t.Fatalf("a restarted keeper, the time past, hands it over from `since`: %+v", c)
}
}
// Design 47 G §580, §529: a statement carrying `kinds` clears only the conditions of those kinds the statement does not name,
// so two bundles of one observer on one node keep each other's conditions open; a statement with no kinds covers every
// kind; and the finding events are taken from either form of the part's finding stream.
func TestAStatementCoversOnlyItsKindsAndEventsComeInEitherForm(t *testing.T) {
r := newRig()
t0 := r.now
mk := func(kind string) seat.Finding {
f := eventFinding(watchman, t0)
f.Kind, f.ID, f.Subject.ID = kind, "g14", "g14"
return f
}
stated := func(at time.Time, kinds []string, fs ...seat.Finding) {
t.Helper()
if err := r.k.FindingsStated(watchman, "ace", mustJSON(t, seat.FindingsStated{At: at, Kinds: kinds, Findings: fs})); err != nil {
t.Fatal(err)
}
}
keyOf := func(kind string) string { return cc.Key(cc.ScopeMachine, "g14", kind) }
// Both bundles state their kinds; each raises its own.
stated(t0, []string{"alpha"}, mk("alpha"))
stated(t0.Add(time.Second), []string{"beta"}, mk("beta"))
for _, kind := range []string{"alpha", "beta"} {
if _, ok := r.open(keyOf(kind)); !ok {
t.Fatalf("%s is raised", kind)
}
}
// Alpha's bundle states nothing now: only alpha's condition is cleared, beta's stays open.
stated(t0.Add(2*time.Second), []string{"alpha"})
if _, ok := r.open(keyOf("alpha")); ok {
t.Error("alpha's condition is cleared by alpha's own statement")
}
if _, ok := r.open(keyOf("beta")); !ok {
t.Error("beta's condition stays open: another bundle's statement does not clear it")
}
// A statement with no kinds covers every kind.
stated(t0.Add(3*time.Second), nil)
if _, ok := r.open(keyOf("beta")); ok {
t.Error("a statement with no kinds clears every kind it does not name")
}
// Either form of the stream's subject reaches the same handler, the node taken from the subject.
body := mustJSON(t, eventFinding(watchman, t0.Add(time.Minute)))
for _, key := range []string{watchman + ".finding-written.ace", "mesh.bus." + watchman + ".finding-events.finding-written.ace"} {
r.k.Settle(keyOf("silent"))
if err := r.k.FindingEvent(stdioEnvelope(key, "", body)); err != nil {
t.Fatal(err)
}
if _, ok := r.open(keyOf("silent")); !ok {
t.Errorf("%s raised nothing", key)
}
if err := r.k.FindingEvent(stdioEnvelope(watchman+".finding-cleared.ace", "", mustJSON(t, seat.FindingCleared{At: t0.Add(2 * time.Minute),
ID: "g14", Kind: "silent", Scope: cc.ScopeMachine, Why: "back"}))); err != nil {
t.Fatal(err)
}
}
}
func stdioEnvelope(key, node string, body []byte) stdio.Envelope {
return stdio.Envelope{Key: key, Node: node, Body: body}
}