G V-5, V-6: a views reader writes its findings through the part's writer; a writer owns kinds and judges an owner's form
WriterFindings replaces the reader's own OwnFindings; the writer's statement carries kinds, deletes only keys of them and keeps the reader's open findings beside observe's; owner and waiting-on of none of the three forms are refused. Builds: design 47 G §639, §580, §678 Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
This commit is contained in:
@@ -33,7 +33,8 @@ type Finding struct {
|
||||
At time.Time
|
||||
}
|
||||
|
||||
// FindingWriter writes and clears a part's findings in its own state `findings`, where the condition keeper raises and
|
||||
// FindingWriter writes and clears a part's findings in its own state `findings` (WriterFindings: through the part's mesh-sdk
|
||||
// writer, design 47 G §639), where the condition keeper raises and
|
||||
// clears conditions from them (novox/hq ADR 0564 §2): the part gives the one that encodes a Finding as that state's layout.
|
||||
type FindingWriter interface {
|
||||
Put(f Finding) error
|
||||
|
||||
+12
-18
@@ -616,28 +616,22 @@ func (r *Reader) Tools(prefix string) []stdio.Tool {
|
||||
}
|
||||
}
|
||||
|
||||
// OwnFindings writes a reader's findings into its own state `findings` through its runtime, where the condition keeper
|
||||
// raises and clears conditions from them: the Findings of a Reader whose module keeps the state `findings`. Encode is
|
||||
// the module's: it turns a Finding into the state's key and value as the seat mesh-conditions' contract lays them out.
|
||||
type OwnFindings struct {
|
||||
Encode func(Finding) (key string, value []byte, err error)
|
||||
// WriterFindings writes a views reader's findings through the part's mesh-sdk writer, as kinds of the reader's own
|
||||
// (design 47 G §639, §580): the Findings of a reader, never a writer of its own. Body is the part's: it lays a Finding
|
||||
// out as the finding of the seat `mesh-conditions` (its scope, severity, source and subject are the seat's contract's,
|
||||
// not this package's); the writer's statement covers the reader's open findings beside its observe's.
|
||||
type WriterFindings struct {
|
||||
Writer *Writer
|
||||
Body func(Finding) FindingBody
|
||||
}
|
||||
|
||||
// Put writes one finding.
|
||||
func (o OwnFindings) Put(f Finding) error {
|
||||
key, value, err := o.Encode(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = stdio.State("findings").Put(key, json.RawMessage(value))
|
||||
return err
|
||||
func (o WriterFindings) Put(f Finding) error {
|
||||
return o.Writer.Write(context.Background(), o.Body(f))
|
||||
}
|
||||
|
||||
// Delete clears one finding.
|
||||
func (o OwnFindings) Delete(f Finding) error {
|
||||
key, _, err := o.Encode(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return stdio.State("findings").Delete(key)
|
||||
func (o WriterFindings) Delete(f Finding) error {
|
||||
b := o.Body(f)
|
||||
return o.Writer.Clear(context.Background(), b.Scope, b.ID, b.Kind, "the part no longer observes it")
|
||||
}
|
||||
|
||||
+74
-4
@@ -5,7 +5,9 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -79,6 +81,24 @@ type FindingBody struct {
|
||||
// Key is the finding's key in the state `findings`: `<scope>.<id>.<kind>` (novox/hq ADR 0564 §2).
|
||||
func (f FindingBody) Key() string { return FindingKey(f.Scope, f.ID, f.Kind) }
|
||||
|
||||
// ViewKinds are the finding kinds a views reader writes through the part's writer (design 47 G §639).
|
||||
var ViewKinds = []string{ViewWaitsKind, UnansweredKind, UnsaidKind, TooLargeKind}
|
||||
|
||||
var (
|
||||
sessionForm = regexp.MustCompile(`^[a-z0-9][a-z0-9._-]*@[a-z0-9][a-z0-9-]*$`)
|
||||
seatForm = regexp.MustCompile(`^[a-z][a-z0-9-]*$`)
|
||||
authorForm = regexp.MustCompile(`^forge:[A-Za-z0-9._-]+$`)
|
||||
)
|
||||
|
||||
// formOfWho refuses a finding's `owner` or `waiting-on` of none of the three forms (design 47 G §678): a session
|
||||
// (who@node), a seat's name, or a commit's author (forge:<account>); empty is none.
|
||||
func formOfWho(field, v string) error {
|
||||
if v == "" || sessionForm.MatchString(v) || seatForm.MatchString(v) || authorForm.MatchString(v) {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("the finding's %s %q is none of a session (who@node), a seat's name or a commit's author (forge:<account>)", field, v)
|
||||
}
|
||||
|
||||
// FindingKey is the key of the finding of scope, id and kind.
|
||||
func FindingKey(scope, id, kind string) string { return scope + "." + id + "." + kind }
|
||||
|
||||
@@ -100,8 +120,10 @@ type PendingKey struct {
|
||||
|
||||
// FindingsStated is the body of `findings-stated`: every finding the observer observes now, and what it cannot judge yet.
|
||||
type FindingsStated struct {
|
||||
At time.Time `json:"at"`
|
||||
Asked string `json:"asked,omitempty"`
|
||||
At time.Time `json:"at"`
|
||||
Asked string `json:"asked,omitempty"`
|
||||
// Kinds are the kinds this statement covers (design 47 G §580); none means every kind.
|
||||
Kinds []string `json:"kinds,omitempty"`
|
||||
Findings []FindingBody `json:"findings"`
|
||||
Pending []PendingKey `json:"pending"`
|
||||
}
|
||||
@@ -140,6 +162,10 @@ type WriterConfig struct {
|
||||
Module string
|
||||
// State is the observer's own state `findings`.
|
||||
State FindingState
|
||||
// Kinds are the finding kinds this writer owns (design 47 G §580): it refuses a Write or a Clear of another kind,
|
||||
// naming it, its statement carries them as `kinds` and deletes only keys of them. None given, it owns every kind.
|
||||
// The kinds of a views reader (ViewKinds) are the reader's own and are always the writer's besides.
|
||||
Kinds []string
|
||||
// JS is the bus connection's JetStream; Conn, when given, lets the writer hear the connection made again.
|
||||
JS jetstream.JetStream
|
||||
Conn *nats.Conn
|
||||
@@ -373,6 +399,31 @@ func (w *Writer) loop(ctx context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// owns says whether the writer's statement covers kind: every kind when none were given (design 47 G §580), else its
|
||||
// own and the views reader's.
|
||||
func (w *Writer) owns(kind string) bool {
|
||||
return len(w.cfg.Kinds) == 0 || slices.Contains(w.cfg.Kinds, kind) || slices.Contains(ViewKinds, kind)
|
||||
}
|
||||
|
||||
// statedKinds are the kinds a statement names: none when the writer owns every kind.
|
||||
func (w *Writer) statedKinds() []string {
|
||||
if len(w.cfg.Kinds) == 0 {
|
||||
return nil
|
||||
}
|
||||
k := append(slices.Clone(w.cfg.Kinds), ViewKinds...)
|
||||
slices.Sort(k)
|
||||
return slices.Compact(k)
|
||||
}
|
||||
|
||||
// ownsKey says whether key is one of a kind the writer owns: its last part is the kind (`<scope>.<id>.<kind>`).
|
||||
func (w *Writer) ownsKey(key string) bool {
|
||||
if len(w.cfg.Kinds) == 0 {
|
||||
return true
|
||||
}
|
||||
i := strings.LastIndex(key, ".")
|
||||
return i >= 0 && w.owns(key[i+1:])
|
||||
}
|
||||
|
||||
func (w *Writer) now() time.Time { return w.cfg.Now().UTC() }
|
||||
|
||||
// unsaid is a finding as compared to say whether what it says changed: not the time it was last seen, nor what saw it.
|
||||
@@ -402,6 +453,15 @@ func (w *Writer) complete(f FindingBody) (FindingBody, *FindingBody, error) {
|
||||
if f.Kind == "" || f.Scope == "" || f.ID == "" {
|
||||
return f, nil, fmt.Errorf("a finding needs its kind, scope and id: %q.%q.%q", f.Scope, f.ID, f.Kind)
|
||||
}
|
||||
if !w.owns(f.Kind) {
|
||||
return f, nil, fmt.Errorf("the finding kind %q is not one this writer owns (%v)", f.Kind, w.cfg.Kinds)
|
||||
}
|
||||
if err := formOfWho("owner", f.Owner); err != nil {
|
||||
return f, nil, err
|
||||
}
|
||||
if err := formOfWho("waiting-on", f.WaitingOn); err != nil {
|
||||
return f, nil, err
|
||||
}
|
||||
old, err := w.read(f.Key())
|
||||
if err != nil {
|
||||
return f, nil, err
|
||||
@@ -449,6 +509,9 @@ func (w *Writer) Clear(ctx context.Context, scope, id, kind, why string) error {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
key := FindingKey(scope, id, kind)
|
||||
if !w.owns(kind) {
|
||||
return fmt.Errorf("the finding kind %q is not one this writer owns (%v)", kind, w.cfg.Kinds)
|
||||
}
|
||||
if err := w.cfg.State.Delete(key); err != nil {
|
||||
return fmt.Errorf("clearing the finding %s: %w", key, err)
|
||||
}
|
||||
@@ -468,7 +531,7 @@ func (w *Writer) State(ctx context.Context, asked string) error {
|
||||
return fmt.Errorf("observe: %w", err)
|
||||
}
|
||||
keep := map[string]bool{}
|
||||
body := FindingsStated{At: w.now(), Asked: asked, Findings: []FindingBody{}, Pending: []PendingKey{}}
|
||||
body := FindingsStated{At: w.now(), Asked: asked, Kinds: w.statedKinds(), Findings: []FindingBody{}, Pending: []PendingKey{}}
|
||||
for _, f := range obs.Findings {
|
||||
f, _, err := w.complete(f)
|
||||
if err != nil {
|
||||
@@ -490,7 +553,14 @@ func (w *Writer) State(ctx context.Context, asked string) error {
|
||||
}
|
||||
slices.Sort(keys)
|
||||
for _, k := range keys {
|
||||
if !keep[k] {
|
||||
// a views reader's open findings are stated beside observe's (design 47 G §639)
|
||||
if i := strings.LastIndex(k, "."); !keep[k] && i >= 0 && slices.Contains(ViewKinds, k[i+1:]) {
|
||||
if old, err := w.read(k); err == nil && old != nil {
|
||||
keep[k] = true
|
||||
body.Findings = append(body.Findings, *old)
|
||||
}
|
||||
}
|
||||
if !keep[k] && w.ownsKey(k) {
|
||||
if err := w.cfg.State.Delete(k); err != nil {
|
||||
return fmt.Errorf("deleting the finding %s: %w", k, err)
|
||||
}
|
||||
|
||||
+71
-2
@@ -80,11 +80,13 @@ var subjects = views.WriterSubjects{
|
||||
Stated: "mesh.stream.m.event.findings-stated.n1", Asked: "mesh.seat.s.event.findings-asked",
|
||||
}
|
||||
|
||||
func newRig(t *testing.T) *rig {
|
||||
func newRig(t *testing.T) *rig { return newRigKinds(t) }
|
||||
|
||||
func newRigKinds(t *testing.T, kinds ...string) *rig {
|
||||
r := &rig{t: t, state: &fakeState{m: map[string][]byte{}}, now: time.Date(2026, 11, 11, 12, 0, 0, 0, time.UTC), ch: make(chan struct{}, 100)}
|
||||
r.observe = func(context.Context) (views.Observed, error) { return views.Observed{}, nil }
|
||||
w, err := views.NewWriter(views.WriterConfig{
|
||||
Module: "m", State: r.state, Subjects: subjects,
|
||||
Module: "m", State: r.state, Subjects: subjects, Kinds: kinds,
|
||||
Observe: func(ctx context.Context) (views.Observed, error) { return r.observe(ctx) },
|
||||
Logf: func(f string, a ...any) {
|
||||
r.mu.Lock()
|
||||
@@ -313,3 +315,70 @@ func TestWriterRunsOneActAtATime(t *testing.T) {
|
||||
t.Fatal("the statement deleted the key the write made")
|
||||
}
|
||||
}
|
||||
|
||||
// A writer owns its kinds (design 47 G §580), states them, deletes no key of another kind, and covers a views reader's
|
||||
// findings written through it beside observe's (§639); an owner or waiting-on of no form is refused, naming the field (§678).
|
||||
func TestWriterKindsAndReaderFindings(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
r := newRigKinds(t, "k")
|
||||
other := finding("x", "another bundle's")
|
||||
other.Kind = "other"
|
||||
r.state.m[other.Key()] = []byte(`{}`)
|
||||
if err := r.w.Write(ctx, other); err == nil || !strings.Contains(err.Error(), `"other"`) {
|
||||
t.Errorf("a write of another kind: %v", err)
|
||||
}
|
||||
if err := r.w.Clear(ctx, "module", "x", "other", "why"); err == nil || !strings.Contains(err.Error(), `"other"`) {
|
||||
t.Errorf("a clear of another kind: %v", err)
|
||||
}
|
||||
for field, f := range map[string]views.FindingBody{"owner": {Owner: "Not A Form"}, "waiting-on": {WaitingOn: "x y"}} {
|
||||
b := finding("o", "s")
|
||||
b.Owner, b.WaitingOn = f.Owner, f.WaitingOn
|
||||
if err := r.w.Write(ctx, b); err == nil || !strings.Contains(err.Error(), field) {
|
||||
t.Errorf("%s of no form: %v", field, err)
|
||||
}
|
||||
}
|
||||
for _, v := range []string{"me@node-1", "mesh-catalog", "forge:jochen"} {
|
||||
b := finding("ok", "s")
|
||||
b.Owner = v
|
||||
if err := r.w.Write(ctx, b); err != nil {
|
||||
t.Errorf("owner %q: %v", v, err)
|
||||
}
|
||||
}
|
||||
// a views reader writes through the part's writer, and the statement keeps its open findings
|
||||
rf := views.WriterFindings{Writer: r.w, Body: func(f views.Finding) views.FindingBody {
|
||||
return views.FindingBody{Kind: f.Kind, Scope: "module", ID: f.ID, Severity: "warning", Summary: f.Summary, Source: "views"}
|
||||
}}
|
||||
if err := rf.Put(views.Finding{Kind: views.ViewWaitsKind, ID: "m.o.s", Summary: "waits"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r.observe = func(context.Context) (views.Observed, error) {
|
||||
return views.Observed{Findings: []views.FindingBody{finding("a", "observed")}}, nil
|
||||
}
|
||||
if err := r.w.State(ctx, ""); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, ok := r.state.m["module.m.o.s.view-waits"]; !ok {
|
||||
t.Errorf("the statement deleted the reader's finding: %v", r.state.m)
|
||||
}
|
||||
if _, ok := r.state.m[other.Key()]; !ok {
|
||||
t.Errorf("the statement deleted another bundle's key")
|
||||
}
|
||||
if _, ok := r.state.m["module.ok.k"]; ok {
|
||||
t.Errorf("a key of its own kind neither stated nor pending stays")
|
||||
}
|
||||
var stated views.FindingsStated
|
||||
for _, e := range r.events() {
|
||||
if e.subject == subjects.Stated {
|
||||
_ = json.Unmarshal(e.data, &stated)
|
||||
}
|
||||
}
|
||||
if !slices.Contains(stated.Kinds, "k") || !slices.Contains(stated.Kinds, views.ViewWaitsKind) || len(stated.Findings) != 2 {
|
||||
t.Errorf("the statement: kinds %v, %d findings", stated.Kinds, len(stated.Findings))
|
||||
}
|
||||
if err := rf.Delete(views.Finding{Kind: views.ViewWaitsKind, ID: "m.o.s"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, ok := r.state.m["module.m.o.s.view-waits"]; ok {
|
||||
t.Errorf("the reader's finding was not cleared")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user