G-10 §479: a per-branch state's first view is named <owner>.<state>.<branch>

FollowBranch on the Reader and the ProcessReader, BranchState names the view with the
branch escaped as a key's name is; one row per branch in the first views.

Builds: design 47 G §479

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
This commit is contained in:
2026-11-11 12:10:08 +01:00
parent 66ad4956d5
commit eed96854ae
3 changed files with 49 additions and 0 deletions
+7
View File
@@ -104,6 +104,13 @@ func (r *ProcessReader) Follow(owner, state, tool string) (*Picture, error) {
return p, nil
}
// FollowBranch is Follow for a state kept per branch: the view of one branch is named `<owner>.<state>.<branch>` (the
// branch escaped as a key's name is, BranchState) and its picture `<owner>_<state>.<branch>`, so a process follows
// several branches of one state, each its own view (design 47 G §479). Call it before Start.
func (r *ProcessReader) FollowBranch(owner, state, branch, tool string) (*Picture, error) {
return r.Follow(owner, BranchState(state, branch), tool)
}
// Adopt takes a follower a seat's client made (its Ask a seat's verb, such as the catalog half's view) as one of the
// process's first views, so the client's callers write no glue: the reader sets its conditions (the unanswered first
// view and view-waits, in the reader's findings), its log and clock, and its picture (`<owner>_<state>`) hands each
+12
View File
@@ -12,6 +12,7 @@ import (
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/keys"
)
// Reader is a module's pictures of the owners' states it used to read (novox/hq ADR 0615 §1): it
@@ -86,6 +87,17 @@ func (r *Reader) Follow(owner, state, tool string) *Follower {
return f
}
// BranchState is the name of a per-branch state's view: `<state>.<branch>`, the branch escaped as a key's name is (design
// 47 G §479), so a reader names the view `<owner>.<state>.<branch>`. A state that is not per branch keeps its name.
func BranchState(state, branch string) string { return state + "." + keys.EscapeName(branch) }
// FollowBranch is Follow for a state kept per branch: the view of one branch is named `<owner>.<state>.<branch>`
// (BranchState), so a reader follows several branches of one state, each its own view and its own row of
// `<module>_first_views` (design 47 G §479). Call it before Start.
func (r *Reader) FollowBranch(owner, state, branch, tool string) *Follower {
return r.Follow(owner, BranchState(state, branch), tool)
}
// Adopt takes a follower a seat's client made (its Ask a seat's verb, such as the catalog half's view) as one of the
// reader's first views, so the client's callers write no glue: the reader sets its conditions (the unanswered first view
// and view-waits, in the reader's findings), its log and clock, and hands each change to Watch, Changed and the
+30
View File
@@ -595,6 +595,36 @@ func TestViews(t *testing.T) {
// A seat's client makes the follower (its Ask a seat's verb) and both readers adopt it, so its callers write no glue:
// the reader's findings (unanswered, view-waits), Keys, Get, Watch, statuses and tools, and the seat's event handed to
// the client's handler.
// Design 47 G §479: a per-branch state's view is named <owner>.<state>.<branch>, several branches in one reader.
t.Run("a per-branch state is followed once per branch, each view its own row named with its escaped branch", func(t *testing.T) {
if got := BranchState("catalog-revisions", "feat/x y"); got != "catalog-revisions.feat/x=20y" {
t.Errorf("BranchState = %q", got)
}
r := &Reader{Module: "reader", Now: now}
a, b := r.FollowBranch("s", "catalog-revisions", "main", "s.read"), r.FollowBranch("s", "catalog-revisions", "feat/x", "s.read")
r.Follow("s", "plain", "s.read")
if a.State != "catalog-revisions.main" || b.State != "catalog-revisions.feat/x" {
t.Errorf("states %q %q", a.State, b.State)
}
for _, n := range []string{"s.catalog-revisions.main", "s.catalog-revisions.feat/x", "s.plain"} {
if !r.Follows(n) {
t.Errorf("%s is not followed", n)
}
}
if r.Follows("s.catalog-revisions") {
t.Errorf("the unnamed per-branch state is followed")
}
if st := r.Statuses(); len(st) != 3 || st[0].State != "catalog-revisions.main" || st[1].State != "catalog-revisions.feat/x" || st[2].State != "plain" {
t.Errorf("rows = %+v", st)
}
pr := &ProcessReader{Module: "reader"}
p1, err1 := pr.FollowBranch("s", "catalog-revisions", "main", "s.read")
p2, err2 := pr.FollowBranch("s", "catalog-revisions", "feat/x", "s.read")
if err1 != nil || err2 != nil || p1 == p2 || p1.Bucket() != "s_catalog-revisions.main" || pr.Picture("s_catalog-revisions.feat/x") != p2 {
t.Errorf("pictures %v %v %v", p1, p2, err1)
}
})
t.Run("a follower a seat's client made is adopted: the reader's findings, reads and tools hold it, and its seat event goes to the client", func(t *testing.T) {
ended := make(chan bool, 1)
kept := waitBound