Fix 9.4a: views: the entry-not-found finding and a reader's first views say added lists (entries by fingerprint)

Builds: design 47 G A-103, A-15, §614

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
2026-11-12 01:08:53 +01:00
parent 9545fad65b
commit c995030a96
3 changed files with 58 additions and 1 deletions
+18
View File
@@ -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)
}
+26 -1
View File
@@ -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 `<owner>.<state>`).
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 `<prefix>_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 `<prefix>_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): `<prefix>_first_views` says where each first view stands and
// `<prefix>_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 (<owner>.<state>), 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{
+14
View File
@@ -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,