159 lines
6.3 KiB
Go
159 lines
6.3 KiB
Go
// Package handacts is the hand-act log's contract (novox/hq ADR 0565 clauses 10 and 11, issue 1953): the layout of an act,
|
|
// moved unchanged from the controller's (mesh-controller internal/link HandAct) so one reader reads both logs alike; the
|
|
// names of the seat mesh-hand-acts and of what its holder keeps; and the recorder every module records through, which
|
|
// asks the seat and, while no holder answers, the controller's verb until the controller hands its lease away, saying
|
|
// which log took the act.
|
|
package handacts
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// The seat, its holder and what it keeps (ADR 0565 clause 10).
|
|
const (
|
|
Seat = "mesh-hand-acts"
|
|
Module = "mesh-hand-acts"
|
|
// ActsLog is the log of every act, kept 400 days; WarrantsState one key `<asker>.<ask>` per warrant, made once.
|
|
ActsLog = "acts"
|
|
WarrantsState = "warrants"
|
|
// EventRecorded is said on the seat for every act, with the act as its body.
|
|
EventRecorded = "act-recorded"
|
|
// ControllerActs is the controller's log as a reader names it, read until its lease ends.
|
|
ControllerActs = "mesh-controller.hand-acts"
|
|
)
|
|
|
|
// KeptFor is how long an act is kept.
|
|
const KeptFor = 400 * 24 * time.Hour
|
|
|
|
// The verbs an act is recorded under, as the controller's log names them.
|
|
const (
|
|
VerbRecord = "hand-act record"
|
|
VerbDrill = "hand-act drill"
|
|
VerbWarrant = "warrant"
|
|
// CauseDrill is a drill's cause, never counted as a repair (S15); CauseOperatorAnswer an act the operator chose.
|
|
CauseDrill = "drill"
|
|
CauseOperatorAnswer = "operator-answer"
|
|
// KindRecordedBuilds is a push that only moved recorded builds: no repair (ADR 0242).
|
|
KindRecordedBuilds = "recorded-builds"
|
|
)
|
|
|
|
// Act is one entry of the log, the controller's HandAct unchanged.
|
|
type Act struct {
|
|
ID string `json:"id"`
|
|
At time.Time `json:"at"`
|
|
// By is who: the caller the runtime proved.
|
|
By string `json:"by"`
|
|
// Verb and Args are the act as given.
|
|
Verb string `json:"verb"`
|
|
Args []string `json:"arguments,omitempty"`
|
|
Why string `json:"why"`
|
|
// Cause is the condition kind, or a word the person gives; the verb's own name when neither.
|
|
Cause string `json:"cause"`
|
|
Condition string `json:"condition,omitempty"`
|
|
// Kind is what the act was read to be from what it did, never a word a person gives.
|
|
Kind string `json:"kind,omitempty"`
|
|
Carried []string `json:"carried,omitempty"`
|
|
// Via, Ask, Proofs and RequestedBy are an act the operator chose on a warrant.
|
|
Via string `json:"via,omitempty"`
|
|
Ask string `json:"ask,omitempty"`
|
|
Proofs []string `json:"proofs,omitempty"`
|
|
RequestedBy string `json:"requested-by,omitempty"`
|
|
// Outcome is what came of an act recorded after it was done.
|
|
Outcome string `json:"outcome,omitempty"`
|
|
// Log is which log holds it, set when it is read (LogSeat or LogController); never written.
|
|
Log string `json:"log,omitempty"`
|
|
}
|
|
|
|
// The logs an act may be held in.
|
|
const (
|
|
LogSeat = Seat
|
|
LogController = "mesh-controller"
|
|
)
|
|
|
|
// Repair says the act counts toward a cause repeated (S15): not a drill, not an operator's answer, not recorded builds.
|
|
func (a Act) Repair() bool {
|
|
return a.Cause != CauseDrill && a.Verb != VerbDrill && a.Cause != CauseOperatorAnswer && a.Kind != KindRecordedBuilds
|
|
}
|
|
|
|
// Request is one act a recorder records: what, why, its cause, the condition it addresses, and its verb in the log (a
|
|
// repair when empty).
|
|
type Request struct {
|
|
What string `json:"what"`
|
|
Why string `json:"why"`
|
|
Cause string `json:"cause,omitempty"`
|
|
Condition string `json:"condition,omitempty"`
|
|
Verb string `json:"verb,omitempty"`
|
|
}
|
|
|
|
// Taken is which log took an act, and what its holder answered.
|
|
type Taken struct {
|
|
Log string `json:"log"`
|
|
Answer json.RawMessage `json:"answer,omitempty"`
|
|
}
|
|
|
|
// Recorder records acts for a module (ADR 0565 clause 11): through the seat mesh-hand-acts, and while the seat has no
|
|
// holder that answers, through the controller's verb until the controller hands its lease away (the release's order
|
|
// gives no seat's holder before its callers; 00-plan.md, "Missing" 1).
|
|
type Recorder struct {
|
|
// Ask calls a seat verb by its address (`seat:<seat>.<verb>`): stdio.Ask through the runtime.
|
|
Ask func(key string, body any) (json.RawMessage, error)
|
|
// LeaseAway says the controller's lease is handed away: then nothing falls back to it. Nil is never handed away.
|
|
LeaseAway func() bool
|
|
}
|
|
|
|
// NoResponder says err is the bus finding nobody to answer: the seat has no holder now.
|
|
func NoResponder(err error) bool {
|
|
return err != nil && strings.Contains(err.Error(), "no responders")
|
|
}
|
|
|
|
// Record records one act.
|
|
func (r Recorder) Record(q Request) (Taken, error) {
|
|
if strings.TrimSpace(q.What) == "" || strings.TrimSpace(q.Why) == "" {
|
|
return Taken{}, errors.New("a hand act says what was done and why: nothing was recorded")
|
|
}
|
|
body := map[string]any{"what": q.What, "why": q.Why}
|
|
for k, v := range map[string]string{"cause": q.Cause, "condition": q.Condition, "verb": q.Verb} {
|
|
if v != "" {
|
|
body[k] = v
|
|
}
|
|
}
|
|
controller := map[string]any{"what": q.What, "why": q.Why, "cause": q.Cause}
|
|
if q.Condition != "" {
|
|
controller["condition"] = q.Condition
|
|
}
|
|
if q.Cause == "" {
|
|
controller["cause"] = q.Verb
|
|
}
|
|
return r.either("hand-act", body, "hand-act", controller)
|
|
}
|
|
|
|
// RecordWarrant records what the asking module did on the operator's warrant for its ask.
|
|
func (r Recorder) RecordWarrant(asker, ask string) (Taken, error) {
|
|
body := map[string]any{"asker": asker, "ask": ask}
|
|
return r.either("warranted", body, "warranted", body)
|
|
}
|
|
|
|
func (r Recorder) either(verb string, body any, controllerVerb string, controllerBody any) (Taken, error) {
|
|
answer, err := r.Ask("seat:"+Seat+"."+verb, body)
|
|
if err == nil {
|
|
return Taken{Log: LogSeat, Answer: answer}, nil
|
|
}
|
|
if !NoResponder(err) {
|
|
return Taken{}, fmt.Errorf("%s.%s refused: %w", Seat, verb, err)
|
|
}
|
|
if r.LeaseAway != nil && r.LeaseAway() {
|
|
return Taken{}, fmt.Errorf("%s has no holder that answers, and the controller handed its lease away: nothing "+
|
|
"was recorded (%w)", Seat, err)
|
|
}
|
|
answer, cerr := r.Ask("seat:mesh-controller."+controllerVerb, controllerBody)
|
|
if cerr != nil {
|
|
return Taken{}, fmt.Errorf("%s has no holder that answers (%v), and the controller's %s refused too: %w", Seat, err,
|
|
controllerVerb, cerr)
|
|
}
|
|
return Taken{Log: LogController, Answer: answer}, nil
|
|
}
|