Files
mesh-sdk/go/handacts/handacts.go
T
jschoubben d15c1bc814 ADR 0625 §3, ADR 0627 §4: mesh-sdk imports the seat set in catalogue alone, and names no client seat outside it
buscompose, delivery's builder and step asks, handacts, health and provisions take their seat names from catalogue.
The package-list test refuses any other contracts import, the client seats' names outside catalogue, and the moved
packages (asks, asker, publish) by name; what the group's join still moves (asks, asker, the vault module's name in
delivery and provisions) is listed and only shrinks.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 13:20:35 +01:00

136 lines
5.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 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"
"git.novox.be/novox/mesh-sdk/go/catalogue"
)
// The seat, its holder and what it keeps (ADR 0565 clause 10).
const (
Seat = catalogue.HandActsSeat
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"
)
// 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:<seat>.<verb>`): 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)
}