From 203b5d5bb7859cb505132e538ccc83f38535cbd5 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 11 Nov 2026 22:08:21 +0100 Subject: [PATCH] G A-37: views.NewStdioHeldWriter, and the held writer holds the findings a reader writes through it A stdio module's held writer fills Publish and Listen as NewStdioWriter does; HeldWriter.WriteWire holds the finding as Write does, so a views reader's first-view findings are in the held set and in findings-stated. Builds: design 47 G A-37, A-29, A-16 --- go/views/held_writer.go | 10 ++++++++++ go/views/stdio_writer.go | 14 +++++++++++++- go/views/stdio_writer_test.go | 23 +++++++++++++++++++++++ go/views/writer_test.go | 14 +++++++++++++- 4 files changed, 59 insertions(+), 2 deletions(-) diff --git a/go/views/held_writer.go b/go/views/held_writer.go index 2ca92cfd..690b04fd 100644 --- a/go/views/held_writer.go +++ b/go/views/held_writer.go @@ -88,3 +88,13 @@ func (h *HeldWriter[F, C, S, A]) Clear(ctx context.Context, scope, id, kind, why h.mu.Unlock() return h.Writer.Clear(ctx, scope, id, kind, why) } + +// WriteWire is Write of a finding in its wire form, held as Write holds it (design 47 G A-37): a views reader writing through +// the held writer has its findings in the held set, so in the statement. +func (h *HeldWriter[F, C, S, A]) WriteWire(ctx context.Context, finding map[string]any) error { + var f F + if err := fillFrom(&f, finding); err != nil { + return err + } + return h.Write(ctx, f) +} diff --git a/go/views/stdio_writer.go b/go/views/stdio_writer.go index a56a437c..161957ed 100644 --- a/go/views/stdio_writer.go +++ b/go/views/stdio_writer.go @@ -22,6 +22,18 @@ func NewStdioWriter[F, C, S, A any](cfg WriterConfig[F, C, S, A]) (*Writer[F, C, func newStdioWriter[F, C, S, A any](cfg WriterConfig[F, C, S, A], emit func(string, any, ...stdio.EmitOptions) error, subscribe func(string, func(stdio.Envelope) error) error) (*Writer[F, C, S, A], error) { + return NewWriter(withStdioPlumbing(cfg, emit, subscribe)) +} + +// NewStdioHeldWriter is NewHeldWriter for a stdio module: it fills `Publish` and `Listen` as NewStdioWriter does (design 47 +// G A-37, after A-16). +func NewStdioHeldWriter[F, C, S, A any](cfg WriterConfig[F, C, S, A]) (*HeldWriter[F, C, S, A], error) { + return NewHeldWriter(withStdioPlumbing(cfg, stdio.Emit, stdio.SubscribeNew)) +} + +func withStdioPlumbing[F, C, S, A any](cfg WriterConfig[F, C, S, A], + emit func(string, any, ...stdio.EmitOptions) error, + subscribe func(string, func(stdio.Envelope) error) error) WriterConfig[F, C, S, A] { if cfg.Publish == nil { cfg.Publish = func(_ context.Context, subject string, data []byte, id string) error { event, err := eventOfSubject(subject) @@ -50,7 +62,7 @@ func newStdioWriter[F, C, S, A any](cfg WriterConfig[F, C, S, A], return func() { stopped.Store(true) }, nil } } - return NewWriter(cfg) + return cfg } // eventOfSubject is the event of a subject of FindingSubjects: `mesh.stream..event..`. diff --git a/go/views/stdio_writer_test.go b/go/views/stdio_writer_test.go index 3519cea2..c820d16f 100644 --- a/go/views/stdio_writer_test.go +++ b/go/views/stdio_writer_test.go @@ -97,6 +97,29 @@ func subStdioWriterPublishesAndListensThroughTheModulesOwnPlumbing(t *testing.T) if err := w.cfg.Publish(context.Background(), "obs.event", nil, ""); err == nil { t.Fatal("a subject of no event form is refused") } + // the held form fills the same plumbing and states what it holds (design 47 G A-37) + out = nil + h, err := NewHeldWriter(withStdioPlumbing(WriterConfig[swFinding, swCleared, swStated, swAsked]{ + Module: "m", Node: "n1", State: noState{}, Asked: "mesh.seat.s.event.findings-asked", + }, func(event string, body any, o ...stdio.EmitOptions) error { + raw, _ := json.Marshal(body) + out = append(out, said{event, string(raw), o[0]}) + return nil + }, func(string, func(stdio.Envelope) error) error { return nil })) + if err != nil { + t.Fatal(err) + } + if err := h.Write(context.Background(), swFinding{Kind: "k", Scope: "module", ID: "x", Observer: "m", Subject: struct { + Scope string `json:"scope"` + }{"module"}}); err != nil { + t.Fatal(err) + } + if err := h.State(context.Background(), "a"); err != nil { + t.Fatal(err) + } + if len(out) == 0 || out[len(out)-1].event != "findings-stated" || !strings.Contains(out[len(out)-1].body, `"id":"x"`) { + t.Fatalf("a stdio held writer states its findings: %+v", out) + } } type noState struct{} diff --git a/go/views/writer_test.go b/go/views/writer_test.go index a2ca9f00..f5e3e7fc 100644 --- a/go/views/writer_test.go +++ b/go/views/writer_test.go @@ -495,6 +495,13 @@ func subHeldWriterStatesWhatItHoldsFromItsOwnState(t *testing.T) { if err := h.Write(ctx, testFinding{Kind: "k", ID: "no-scope"}); err == nil { t.Fatal("a finding that breaks a rule was held") } + // a views reader writes its findings in the wire form: they are held as well (design 47 G A-37) + var wire map[string]any + rawWire, _ := json.Marshal(finding("wired", "from a reader")) + _ = json.Unmarshal(rawWire, &wire) + if err := h.WriteWire(ctx, wire); err != nil { + t.Fatal(err) + } if err := h.Clear(ctx, "module", "seeded", "k", "no longer"); err != nil { t.Fatal(err) } @@ -504,9 +511,14 @@ func subHeldWriterStatesWhatItHoldsFromItsOwnState(t *testing.T) { } var stated testStated if len(sent) != 1 || sent[0].subject != subjects.Stated || json.Unmarshal(sent[0].data, &stated) != nil || - len(stated.Findings) != 1 || stated.Findings[0].ID != "raised" || stated.Asked != "a1" { + len(stated.Findings) != 2 || stated.Asked != "a1" { t.Fatalf("the statement says %+v after %d events", stated, len(sent)) } + ids := []string{stated.Findings[0].ID, stated.Findings[1].ID} + slices.Sort(ids) + if ids[0] != "raised" || ids[1] != "wired" { + t.Fatalf("the statement says %v", ids) + } } // TestFindingSubjectsAreTheEventsStreamForm runs the tests of this package that were merged to keep the module within its test budget (design 47 G ยง419), each as a subtest.