From 57836416fe89e05e95bd020405115d946c19a55d Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 11 Nov 2026 20:26:21 +0100 Subject: [PATCH] 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 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- go/views/findings.go | 3 +- go/views/reader.go | 30 +++++++--------- go/views/writer.go | 78 ++++++++++++++++++++++++++++++++++++++--- go/views/writer_test.go | 73 ++++++++++++++++++++++++++++++++++++-- 4 files changed, 159 insertions(+), 25 deletions(-) diff --git a/go/views/findings.go b/go/views/findings.go index 01408205..a526f37b 100644 --- a/go/views/findings.go +++ b/go/views/findings.go @@ -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 diff --git a/go/views/reader.go b/go/views/reader.go index fcc76d2c..784b1fc2 100644 --- a/go/views/reader.go +++ b/go/views/reader.go @@ -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") } diff --git a/go/views/writer.go b/go/views/writer.go index 9697dbaf..0d9436a6 100644 --- a/go/views/writer.go +++ b/go/views/writer.go @@ -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`: `..` (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:); 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:)", 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 (`..`). +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) } diff --git a/go/views/writer_test.go b/go/views/writer_test.go index 85fe34c9..11af93a0 100644 --- a/go/views/writer_test.go +++ b/go/views/writer_test.go @@ -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") + } +}