Files
mesh-sdk/go/handacts/handacts.go
T
jschoubben 8696aa9ba4 The hand-act recorder records through the seat alone, and the watched reader's reads are proved
The controller's hand-act and warranted are gone (issue 1953 L5b) and the bus refuses them, so the recorder no longer
falls back to them: an act the seat does not take is an error saying nothing was recorded. And ADR 0580 clause 1's
reader is shown reading a build by key each time and a manifest once per digest.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-01 09:53:17 +01:00

142 lines
5.5 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 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 `<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 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:<seat>.<verb>`): stdio.Ask through the runtime.
Ask func(key string, body any) (json.RawMessage, error)
// LeaseAway is no longer read: nothing falls back to the controller. Kept so a caller that sets it builds; it goes
// once no module sets it.
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
}
}
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)
}