A7 V-9: a part's first views say its own build (own-build beside their list, kept at start by a process)

Builds: design 47 G §674, §424, §519
Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
2026-11-12 04:56:21 +01:00
parent fa0a107cf5
commit 46222b9f0c
5 changed files with 107 additions and 7 deletions
+44
View File
@@ -0,0 +1,44 @@
package views
import (
"context"
"encoding/json"
"os"
"git.novox.be/novox/mesh-sdk/go/process"
)
// OwnBuildKey is where a process keeps its own build in its state `first-views`, and the field both answers of a part's
// first views carry beside their list (design 47 G §674, §424, §519): the fingerprint the node-engine composed into
// the part's environment, or OwnBuildUnknown when it carries none.
const OwnBuildKey = "own-build"
// OwnBuildUnknown is what a part's first views say of its own build when its environment carries no fingerprint.
const OwnBuildUnknown = "unknown"
func ownBuildWord(b process.Build) string {
if b.Fingerprint == "" {
return OwnBuildUnknown
}
return b.Fingerprint
}
// ownBuild is the build this reader's bundle was composed with, read from its environment at each answer.
func (r *Reader) ownBuild() process.Build {
getenv := r.Getenv
if getenv == nil {
getenv = os.Getenv
}
return process.ReadOwnBuild(getenv)
}
// KeepOwnBuild writes a process's own build at OwnBuildKey of kv (its state `first-views`) at its start, for its tools
// bundle's status tool to read (design 47 G §674).
func KeepOwnBuild(ctx context.Context, kv interface {
Put(ctx context.Context, key string, value []byte) (uint64, error)
}, b process.Build, logf func(string, ...any)) {
data, _ := json.Marshal(ownBuildWord(b))
if _, err := kv.Put(ctx, OwnBuildKey, data); err != nil && logf != nil {
logf("views: the own build is not kept in %s: %v", StatusState, err)
}
}
+1
View File
@@ -833,6 +833,7 @@ func (r *ProcessReader) Start(ctx context.Context, js jetstream.JetStream, cred
var said func(Status)
if status != nil {
status.Ready(func(kv jetstream.KeyValue) {
KeepOwnBuild(ctx, kv, process.OwnBuild(), r.logf)
if err := hearAgain(ctx, kv, r.Followers, r.logf); err != nil {
r.logf("views: the hand asks of %s's first views are not heard (%v) until it starts again", r.Module, err)
}
+6 -5
View File
@@ -35,10 +35,11 @@ type Reader struct {
// Acts records a re-read by hand before it is asked (nil records nothing
// and the re-read is refused).
Acts HandActs
// Now, Bound and Wait are for tests: zero are the clock, Bound and five seconds.
Now func() time.Time
Bound time.Duration
Wait time.Duration
// Now, Bound, Wait and Getenv are for tests: zero are the clock, Bound, five seconds and os.Getenv.
Getenv func(string) string
Now func() time.Time
Bound time.Duration
Wait time.Duration
mu sync.Mutex
followers map[string]*Follower // by "<owner>.<state>"
@@ -589,7 +590,7 @@ func (r *Reader) Rows(name string, rows func() any) {
// 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()}
out := map[string]any{"views": r.Statuses(), OwnBuildKey: ownBuildWord(r.ownBuild())}
r.mu.Lock()
rows := maps.Clone(r.rows)
r.mu.Unlock()
+9 -2
View File
@@ -125,7 +125,7 @@ func StatusTools(reader, read, again string, st keptStatus, acts HandActs, now f
}
out := []Status{}
for _, k := range keys {
if strings.HasPrefix(k, "again.") {
if strings.HasPrefix(k, "again.") || k == OwnBuildKey {
continue
}
e, err := st.Get(k)
@@ -137,7 +137,14 @@ func StatusTools(reader, read, again string, st keptStatus, acts HandActs, now f
out = append(out, s)
}
}
return map[string]any{"first-views": out}, nil
own := OwnBuildUnknown
if e, err := st.Get(OwnBuildKey); err == nil && e != nil {
var w string
if json.Unmarshal(e.Value, &w) == nil && strings.TrimSpace(w) != "" {
own = w
}
}
return map[string]any{"first-views": out, OwnBuildKey: own}, nil
}},
{Name: again,
Description: fmt.Sprintf("Ask %s's first view of an owner's state again now, by hand (a hand act, its caller and why kept in "+
+47
View File
@@ -5,6 +5,7 @@ import (
"encoding/json"
"errors"
"fmt"
"maps"
"reflect"
"runtime"
"slices"
@@ -1234,3 +1235,49 @@ func TestViews(t *testing.T) {
t.Run("Views", subViews)
t.Run("StdioWriterPublishesAndListensThroughTheModulesOwnPlumbing", subStdioWriterPublishesAndListensThroughTheModulesOwnPlumbing)
}
type putRecorder struct{ puts map[string][]byte }
func (p *putRecorder) Put(_ context.Context, key string, value []byte) (uint64, error) {
p.puts[key] = value
return 1, nil
}
// statusWith is a state `first-views` holding the given values.
type statusWith map[string][]byte
func (s statusWith) Keys() ([]string, error) { return slices.Collect(maps.Keys(s)), nil }
func (s statusWith) Get(k string) (*stdio.StateEntry, error) {
if v, ok := s[k]; ok {
return &stdio.StateEntry{Value: v}, nil
}
return nil, nil
}
func (statusWith) Put(string, any) (uint64, error) { return 0, errors.New("read only") }
// design 47 G §674: both answers of a part's first views say its own build beside their list.
func TestFirstViewsSayTheOwnBuild(t *testing.T) {
for name, c := range map[string]struct{ env, want string }{"a fingerprint": {"abc123", "abc123"}, "none": {"", "unknown"}} {
getenv := func(string) string { return c.env }
// the bundle reader's answer
r := &Reader{Module: "m", Getenv: getenv}
got, _ := r.Tools("m")[0].Run(nil)
if v := got.(map[string]any)[OwnBuildKey]; v != c.want {
t.Errorf("%s: the bundle reader's first views say %v, want %s", name, v, c.want)
}
// the process writes it at its start, and its status tool reads it
rec := &putRecorder{puts: map[string][]byte{}}
KeepOwnBuild(context.Background(), rec, process.ReadOwnBuild(getenv), nil)
st := statusWith{OwnBuildKey: rec.puts[OwnBuildKey]}
read, err := StatusTools("m", "m_first_views", "m_first_view_again", st, nil, time.Now)[0].Run(nil)
m := read.(map[string]any)
if err != nil || m[OwnBuildKey] != c.want || len(m["first-views"].([]Status)) != 0 {
t.Errorf("%s: the status tool says %v, %v, want %s", name, m, err, c.want)
}
}
// a process that kept nothing yet says unknown
read, _ := StatusTools("m", "r", "a", statusWith{}, nil, time.Now)[0].Run(nil)
if read.(map[string]any)[OwnBuildKey] != "unknown" {
t.Errorf("nothing kept: %v", read)
}
}