// 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 alone: the controller's hand-act and warranted are gone (issue 1953 L5b) and the bus refuses them. 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" ) // 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); never written. Log string `json:"log,omitempty"` } // LogSeat is the log an act is held in: the seat's (the ended controller's log is read from its backup, ADR 0590). const LogSeat = Seat // 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 alone. An act the seat does // not take is an error that says nothing was recorded, never a quiet success. 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) } // 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 } } return r.record("hand-act", body) } // 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.record("warranted", body) } func (r Recorder) record(verb string, body 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 has no holder that answers: nothing was recorded (%w)", Seat, err) } return Taken{}, fmt.Errorf("%s.%s refused, nothing was recorded: %w", Seat, verb, err) }