// 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 `.` 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:.`): 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 }