From c995030a96e411c4e37617b1a83a960cf1c23a45 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 12 Nov 2026 01:08:53 +0100 Subject: [PATCH] Fix 9.4a: views: the entry-not-found finding and a reader's first views say added lists (entries by fingerprint) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Builds: design 47 G A-103, A-15, §614 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h --- go/views/findings.go | 18 ++++++++++++++++++ go/views/reader.go | 27 ++++++++++++++++++++++++++- go/views/views_test.go | 14 ++++++++++++++ 3 files changed, 58 insertions(+), 1 deletion(-) diff --git a/go/views/findings.go b/go/views/findings.go index 2d1699c4..d80c2cf8 100644 --- a/go/views/findings.go +++ b/go/views/findings.go @@ -20,6 +20,9 @@ const ( // ViewWaitsKind is a part's finding of a view it has waited for past WaitsBound (design 47 *A part with no view says it // waits*). ViewWaitsKind = "view-waits" + // EntryNotFoundKind is a part's finding of an entry by fingerprint the seat mesh-catalog's `entry` answered it does not hold + // (design 47 G §614, A-15, A-103): scope the part, id the fingerprint. + EntryNotFoundKind = "entry-not-found" ) // viewFinding is what a views part says is wrong with a state it owns or follows, in plain fields: kind and id name it, summary @@ -99,3 +102,18 @@ func viewWaitsFinding(part string, s Status, at time.Time) viewFinding { part, s.Owner, s.State, s.WaitsFor, s.WaitingSince.UTC().Format(time.RFC3339), cmp.Or(s.View, "none")) return viewFinding{kind: ViewWaitsKind, id: part + "." + s.Owner + "." + s.State, summary: summary, headline: fmt.Sprintf("%s waits for a view of %s", part, s.Owner), at: at.UTC()} } + +// WriteEntryNotFound writes the part's finding that the catalog holds no entry for fingerprint, asked since `since` +// (design 47 G A-103); ClearEntryNotFound clears it, when nothing the part holds names the fingerprint or a later ask answers. +func WriteEntryNotFound(w WireWriter, fingerprint string, since, at time.Time) error { + module, _ := w.ModuleNode() + return viewFinding{kind: EntryNotFoundKind, id: fingerprint, + summary: fmt.Sprintf("%s holds a build of fingerprint %s that the catalog holds no entry for (asked since %s): what it needs of the entry stands still", + module, fingerprint, since.UTC().Format(time.RFC3339)), + headline: fmt.Sprintf("%s cannot find the catalog entry of a build", module), at: at.UTC()}.put(w) +} + +// ClearEntryNotFound clears the finding WriteEntryNotFound wrote for fingerprint. +func ClearEntryNotFound(w WireWriter, fingerprint string) error { + return viewFinding{kind: EntryNotFoundKind, id: fingerprint}.drop(w) +} diff --git a/go/views/reader.go b/go/views/reader.go index 1cdf6065..9dcd3380 100644 --- a/go/views/reader.go +++ b/go/views/reader.go @@ -28,6 +28,8 @@ type Reader struct { // says it in the log only. Findings WireWriter Logf func(string, ...any) + + rows map[string]func() any // the lists Rows added to the first views (guarded by mu) // Changed is told of each key a followed state changes, after the picture holds it (state is `.`). Changed func(state, key string, value json.RawMessage, deleted bool) // Acts records a re-read by hand before it is asked (nil records nothing @@ -574,13 +576,36 @@ func (r *Reader) Statuses() []Status { return out } +// Rows adds a list the part's `_first_views` says beside the views' statuses, under name (design 47 G A-103: the +// entries by fingerprint the catalog half's follower keeps). rows is called at each reading. +func (r *Reader) Rows(name string, rows func() any) { + r.mu.Lock() + defer r.mu.Unlock() + if r.rows == nil { + r.rows = map[string]func() any{} + } + r.rows[name] = rows +} + +// firstViews is what `_first_views` answers: the views' statuses and each list added by Rows. +func (r *Reader) firstViews() map[string]any { + out := map[string]any{"views": r.Statuses()} + r.mu.Lock() + rows := maps.Clone(r.rows) + r.mu.Unlock() + for name, f := range rows { + out[name] = f() + } + return out +} + // Tools are the reader's two tools (design 25 § 12.2): `_first_views` says where each first view stands and // `_first_view_again` asks one again by hand (a hand act, counted in the status). func (r *Reader) Tools(prefix string) []stdio.Tool { return []stdio.Tool{ {Name: prefix + "_first_views", Description: "Where each first view this module keeps stands (novox/hq ADR 0615): the owner and its tool, the revision the picture holds, when it was asked and answered, how many asks (by hand among them) and whether its condition is raised. (r)", - Run: func(map[string]any) (any, error) { return map[string]any{"views": r.Statuses()}, nil }}, + Run: func(map[string]any) (any, error) { return r.firstViews(), nil }}, {Name: prefix + "_first_view_again", Description: "Ask a first view again by hand, for the state named (.), or every one with none: a hand act, for a picture the operator doubts. Answers where it stands after. (a)", Input: map[string]any{"type": "object", "properties": map[string]any{ diff --git a/go/views/views_test.go b/go/views/views_test.go index 55a83494..07bdd0ac 100644 --- a/go/views/views_test.go +++ b/go/views/views_test.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "reflect" "runtime" "slices" "strings" @@ -558,6 +559,19 @@ func subViews(t *testing.T) { if err != nil || len(again.(map[string]any)["views"].([]Status)) != 1 || f.Status().ByHand != 1 || acted != 1 { t.Errorf("asked again by hand: %v, %v", again, err) } + // a list added by Rows is said beside the statuses (design 47 G A-103), and the entry-not-found finding is written and cleared + r.Rows("entries", func() any { return []string{"fp1"} }) + seen, _ := r.Tools("reader")[0].Run(nil) + if got := seen.(map[string]any)["entries"]; !reflect.DeepEqual(got, []string{"fp1"}) { + t.Errorf("first views entries = %v", got) + } + nf := &findingsMap{} + if err := WriteEntryNotFound(nf, "fp1", now(), now()); err != nil || nf.m[EntryNotFoundKind+"/fp1"].Summary == "" { + t.Errorf("entry-not-found not written: %+v, %v", nf.m, err) + } + if err := ClearEntryNotFound(nf, "fp1"); err != nil || len(nf.m) != 0 { + t.Errorf("entry-not-found not cleared: %+v, %v", nf.m, err) + } }) t.Run("a process's reader follows the read tool its caller names, reads the state as a read-only bucket, each key's written time handed on, and opens no other", func(t *testing.T) { r := &ProcessReader{Module: "reader", Bound: time.Nanosecond, Wait: time.Millisecond, Now: now,