A call run on its warrant answers {answer, approval}, the tool's own answer and the line
naming the approval, as a process holder's gated-ended says it. The runner then calls
mesh-hand-acts.warranted, each try a kept call open while the ask lacks recorded, and on
its answer writes recorded; past the kept call's bound it is asked again when the hand-act
seat's holder is heard again and at each start, for every acted, unrecorded ask within its
48 hours. Two bound keys per run in the runner's key-ttl state bound raise call-unanswered
at five minutes and say an unrecorded run an hour before its key's end; the answer clears
the finding.
Builds: design 46 7-approval-mark §16, §13 (the log line an hour before 48 hours); lead answers A-157, A-158
Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
183 lines
4.8 KiB
Go
183 lines
4.8 KiB
Go
package firstview
|
|
|
|
import (
|
|
"context"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
|
|
catalogseat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
|
|
"git.novox.be/novox/mesh-sdk/go/seatdef"
|
|
|
|
"github.com/novox/mesh-tools/node-tools/internal/approval"
|
|
)
|
|
|
|
// Findings is the runner's held finding writer, the one the gate writes its findings through too (design 46 7-approval-mark
|
|
// §4, §16, §19); nil when it was not made.
|
|
func (v *Views) Findings() approval.FindingWriter {
|
|
if v.deps.Findings == nil {
|
|
return nil
|
|
}
|
|
return v.deps.Findings
|
|
}
|
|
|
|
// ApprovalCatalog is the catalog half as the gate reads its marks (design 46 7-approval-mark §5): the entries the branch
|
|
// follower holds, an entry by fingerprint asked of `mesh-catalog.entry` when no branch lists it, and the seat definitions
|
|
// the seat list holds (defs).
|
|
func (v *Views) ApprovalCatalog(defs func() map[string]seatdef.Definition) approval.Catalog {
|
|
return approvalCatalog{v: v, defs: defs}
|
|
}
|
|
|
|
type approvalCatalog struct {
|
|
v *Views
|
|
defs func() map[string]seatdef.Definition
|
|
}
|
|
|
|
func (c approvalCatalog) Ready() error {
|
|
if c.v.branches == nil {
|
|
return errNotFollowed
|
|
}
|
|
return c.v.branches.Ready()
|
|
}
|
|
|
|
func (c approvalCatalog) Revision() string {
|
|
if c.v.branches == nil {
|
|
return ""
|
|
}
|
|
for _, branch := range c.v.branches.Followed() {
|
|
if cat, ok := c.v.branches.Catalog(branch); ok {
|
|
if half, ok := cat.View(); ok {
|
|
return strconv.FormatInt(half.Revision.Revision, 10)
|
|
}
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (c approvalCatalog) Served(fingerprint string) (catalogseat.CatalogEntry, bool, string) {
|
|
if c.v.branches == nil {
|
|
return catalogseat.CatalogEntry{}, false, errNotFollowed.Error()
|
|
}
|
|
e, state := c.v.branches.Resolve(context.Background(), fingerprint)
|
|
return e, state == meshcatalogclient.EntryAnswered, string(state)
|
|
}
|
|
|
|
func (c approvalCatalog) Newest(module string) (catalogseat.CatalogEntry, bool) {
|
|
if c.v.branches == nil {
|
|
return catalogseat.CatalogEntry{}, false
|
|
}
|
|
e, ok, err := c.v.branches.Entry(module)
|
|
return e, ok && err == nil
|
|
}
|
|
|
|
func (c approvalCatalog) Definitions() map[string]seatdef.Definition {
|
|
if c.defs == nil {
|
|
return nil
|
|
}
|
|
return c.defs()
|
|
}
|
|
|
|
type notFollowed struct{}
|
|
|
|
func (notFollowed) Error() string { return "the catalog half is not followed by this runner" }
|
|
|
|
var errNotFollowed error = notFollowed{}
|
|
|
|
// heardOne wakes every wait on the picture.
|
|
func (v *Views) heardOne() {
|
|
v.mu.Lock()
|
|
defer v.mu.Unlock()
|
|
if v.heard != nil {
|
|
close(v.heard)
|
|
}
|
|
v.heard = make(chan struct{})
|
|
}
|
|
|
|
// heardNext is closed at the next event heard.
|
|
func (v *Views) heardNext() <-chan struct{} {
|
|
v.mu.Lock()
|
|
defer v.mu.Unlock()
|
|
if v.heard == nil {
|
|
v.heard = make(chan struct{})
|
|
}
|
|
return v.heard
|
|
}
|
|
|
|
// WaitCatalog waits within the bound for the catalog half's first view of every watched branch (design 46 7-approval-mark
|
|
// §19), waking at each event the runner hears and at each first view answered; nil once it is read, else why not.
|
|
func (v *Views) WaitCatalog(within time.Duration) error {
|
|
c := approvalCatalog{v: v}
|
|
timer := time.NewTimer(within)
|
|
defer timer.Stop()
|
|
for {
|
|
next := v.heardNext()
|
|
err := c.Ready()
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
var ready []<-chan struct{}
|
|
if v.branches != nil {
|
|
for _, branch := range v.branches.Followed() {
|
|
if cat, ok := v.branches.Catalog(branch); ok {
|
|
select {
|
|
case <-cat.Ready(): // read already: waits on nothing
|
|
default:
|
|
ready = append(ready, cat.Ready())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if !waitAny(next, timer.C, ready) {
|
|
return c.Ready()
|
|
}
|
|
}
|
|
}
|
|
|
|
// waitAny waits for next or one of ready, true, or for the bound, false.
|
|
func waitAny(next <-chan struct{}, bound <-chan time.Time, ready []<-chan struct{}) bool {
|
|
woke := make(chan struct{}, 1)
|
|
stop := make(chan struct{})
|
|
defer close(stop)
|
|
for _, r := range ready {
|
|
go func(r <-chan struct{}) {
|
|
select {
|
|
case <-r:
|
|
woke <- struct{}{}
|
|
case <-stop:
|
|
}
|
|
}(r)
|
|
}
|
|
select {
|
|
case <-next:
|
|
case <-woke:
|
|
case <-bound:
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// OnSeatHolder calls f whenever the assigner's seat-holders view changes for the seat (lead answer A-157: the gate asks
|
|
// mesh-hand-acts.warranted again when the hand-act seat's holder is heard again).
|
|
func (v *Views) OnSeatHolder(seat string, f func()) {
|
|
v.mu.Lock()
|
|
defer v.mu.Unlock()
|
|
if v.onKey == nil {
|
|
v.onKey = map[string][]func(){}
|
|
}
|
|
k := "seat-holders " + seat
|
|
v.onKey[k] = append(v.onKey[k], f)
|
|
}
|
|
|
|
// heardKey calls the hearers of a key of an owner's state just changed: a seat's holders, under the seat's own key and
|
|
// its `<seat>.<node>` keys.
|
|
func (v *Views) heardKey(state, key string) {
|
|
seat, _, _ := strings.Cut(key, ".")
|
|
v.mu.Lock()
|
|
fs := append([]func(){}, v.onKey[state+" "+seat]...)
|
|
v.mu.Unlock()
|
|
for _, f := range fs {
|
|
go f()
|
|
}
|
|
}
|