A recorded build moves only by a person's push (ADR 0242), so that push is the word its upgrade policy asks for; S15 counted it as a repair and wanted a healer for split-dns, words and uplink-verbs. The push now reads what it carries before it is recorded and says so in its kind, and the ten pushes of 2026-10-07 are named so their three warnings clear on the next tick.
352 lines
15 KiB
Go
352 lines
15 KiB
Go
package main
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"errors"
|
||
"flag"
|
||
"fmt"
|
||
"os"
|
||
"slices"
|
||
"sort"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/nats-io/nats.go"
|
||
|
||
"github.com/novox/mesh-controller/internal/broker"
|
||
"github.com/novox/mesh-controller/internal/link"
|
||
)
|
||
|
||
// Acts done by hand, and why (novox/hq to-be 45 §7).
|
||
//
|
||
// **Which verbs ask why.** `plans close` and `plans stop`, `broker consumer-reset` and `hand-act
|
||
// record` refuse without it everywhere: nothing automated runs them, so a call without a reason is a
|
||
// person who has not given one. A named `push` asks for it through the mesh-controller seat, which is
|
||
// how a person or an agent acts by hand on the mesh; at a shell `--why` is recorded when given and not
|
||
// required, because the installer and the lab push by command line as a step of what they do, and a
|
||
// step of a procedure is not a repair. `conditions silence` joins them when the condition store does
|
||
// (Phase 1).
|
||
|
||
// handActVerb is one verb that writes the hand-act log, and whether what it records is a repair.
|
||
type handActVerb struct {
|
||
Verb string
|
||
// Decision says why an act of this verb is a person's decision by design rather than a repair a
|
||
// healer could take over; empty for a repair.
|
||
Decision string
|
||
// DecidedFor limits Decision to these causes; empty, it holds for every act of the verb.
|
||
DecidedFor []string
|
||
// DecidedWhen limits Decision to the acts it answers true for: what the controller read the act to
|
||
// be from what it did (a push's kind), never a word the person gave.
|
||
DecidedWhen func(link.HandAct) bool
|
||
}
|
||
|
||
// causeLeakedInLogs is the cause a rotation after a value was printed into a log gives.
|
||
const causeLeakedInLogs = "leaked-in-logs"
|
||
|
||
// causeDrill is the cause of every act `hand-act drill` records, and the one cause `hand-act record`
|
||
// refuses: a drill has its own verb, so whether an act was a drill is said by the verb a person chose,
|
||
// never by a word typed into a repair's cause (novox/hq issue 292).
|
||
const causeDrill = "drill"
|
||
|
||
// handActVerbs is every verb that writes the hand-act log (novox/hq to-be 45 §7). **S15 reads it**:
|
||
// an act recorded by a verb whose entry names a decision is the mesh working as decided, never a
|
||
// repair, and does not count toward `healer-wanted` — whatever cause it gives. A verb not listed, or
|
||
// listed without a decision, counts, so a new verb is a repair until its entry says otherwise.
|
||
var handActVerbs = []handActVerb{
|
||
// Repairs: each repeated is a healer the mesh lacks. A push by hand is exactly what roll-out by
|
||
// default (ADR 0236) exists to end — except a push that only moved builds a `record` policy held for
|
||
// a person's word (ADR 0242), which the push itself reads from what it carried (recorded_push.go).
|
||
{Verb: "push", Decision: "a recorded build moves only by a person's push: that push is the word its " +
|
||
"upgrade policy asks for (ADR 0242)", DecidedWhen: pushedRecorded},
|
||
{Verb: "plans stop"},
|
||
{Verb: "plans close"},
|
||
// A walk started by a person instead of its delivery's owner (novox/hq ADR 0239): the owner down, or
|
||
// not trusted with it — either is a repair the owner should have made.
|
||
{Verb: "plans go"},
|
||
{Verb: "broker consumer-reset"},
|
||
// Silencing the same condition twice says the condition, or what it watches, wants mending.
|
||
{Verb: "conditions silence"},
|
||
// An act done outside the mesh: the mesh cannot tell a repair from a decision there, so it counts —
|
||
// except a drill recorded through it before `hand-act drill` existed (2026-10-07). It refuses the
|
||
// cause since, so no act recorded through it now carries it.
|
||
{Verb: "hand-act record", Decision: "a drill recorded before `hand-act drill` existed: a person's " +
|
||
"deliberate test, never a repair", DecidedFor: []string{causeDrill}},
|
||
// A drill: something broken on purpose to see the mesh raise and clear it. A person's test, never a
|
||
// repair, however often it is run.
|
||
{Verb: "hand-act drill", Decision: "a drill is a person's deliberate test of the mesh, never a repair"},
|
||
// A person's decisions by design.
|
||
{Verb: "retire approve", Decision: "nothing is retired past its bound without a person (ADR 0230)"},
|
||
{Verb: "retire reject", Decision: "keeping a consumer active is a person's word (ADR 0230)"},
|
||
{Verb: "cleanup delete", Decision: "nothing retired is deleted without a person (ADR 0230)"},
|
||
{Verb: "bus upgrade", Decision: "the bus is never rolled by the mesh: replacing it is a planned step a " +
|
||
"person starts (ADR 0236)"},
|
||
{Verb: "upgrade release-backlog", Decision: "after a release plan failed, the next opens only when a " +
|
||
"person releases it (ADR 0236)"},
|
||
// A leak is judged by a person — which value was exposed, to whom — and its rotation is the answer
|
||
// to that judgement. Several values rotate for one leak, and a leak that recurs is a defect of the
|
||
// module that prints them, an issue against it, not a healer that rotates. A rotation for any other
|
||
// cause — a credential that stopped working — counts: a schedule or a healer could take it over.
|
||
{Verb: "secret rotate", Decision: "a value a person judged disclosed is replaced on their word",
|
||
DecidedFor: []string{causeLeakedInLogs}},
|
||
}
|
||
|
||
// personsDecision is whether an act in the log is a person's decision by design, by the verb that
|
||
// recorded it (handActVerbs).
|
||
func personsDecision(a link.HandAct) bool {
|
||
for _, v := range handActVerbs {
|
||
if v.Verb != a.Verb {
|
||
continue
|
||
}
|
||
return v.Decision != "" && (len(v.DecidedFor) == 0 || slices.Contains(v.DecidedFor, a.Cause)) &&
|
||
(v.DecidedWhen == nil || v.DecidedWhen(a))
|
||
}
|
||
return false
|
||
}
|
||
|
||
// handActFlags are the flags every repairing verb takes.
|
||
type handActFlags struct {
|
||
why, cause, condition *string
|
||
}
|
||
|
||
func addHandActFlags(set *flag.FlagSet) handActFlags {
|
||
return handActFlags{
|
||
why: set.String("why", "", "why this is done by hand — recorded in the hand-act log (novox/hq to-be 45 §7)"),
|
||
cause: set.String("cause", "", "the cause, in a word or a condition's kind; the verb's own name when not given"),
|
||
condition: set.String("condition", "", "the key of the condition this act addresses, if any"),
|
||
}
|
||
}
|
||
|
||
// given is whether a reason was given.
|
||
func (f handActFlags) given() bool { return strings.TrimSpace(*f.why) != "" }
|
||
|
||
// require refuses an act without a reason, before anything is done.
|
||
func (f handActFlags) require(verb string) error {
|
||
if f.given() {
|
||
return nil
|
||
}
|
||
return fmt.Errorf("%s is a repair done by hand, and says why: --why <text> (recorded in the hand-act "+
|
||
"log, novox/hq to-be 45 §7). Nothing was done", verb)
|
||
}
|
||
|
||
// handActConn is the serving controller's connection, for what it reads of the log itself; a
|
||
// command dials its own.
|
||
var handActConn *nats.Conn
|
||
|
||
// onTheBus runs f with a connection to the bus: the serving controller's, or one of its own.
|
||
func onTheBus(f func(*nats.Conn) error) error {
|
||
if handActConn != nil {
|
||
return f(handActConn)
|
||
}
|
||
address, err := broker.BusAddress()
|
||
if err != nil {
|
||
return err
|
||
}
|
||
js, err := broker.Dial(address)
|
||
if err != nil {
|
||
return fmt.Errorf("cannot reach the bus: %w", err)
|
||
}
|
||
defer js.Close()
|
||
return f(js.Conn())
|
||
}
|
||
|
||
// record writes the entry for an act about to be done. **Before the act, and never instead of it**:
|
||
// a log that cannot be written is said loudly, and the repair it was about still happens — a mesh
|
||
// whose bus is down is exactly the mesh somebody is repairing by hand.
|
||
func (f handActFlags) record(ctx context.Context, verb string, args []string) {
|
||
if !f.given() {
|
||
return
|
||
}
|
||
act := link.HandAct{Verb: verb, Args: args, Why: strings.TrimSpace(*f.why),
|
||
Cause: strings.TrimSpace(*f.cause), Condition: strings.TrimSpace(*f.condition)}
|
||
// A push naming one machine says whether it only moves recorded builds (novox/hq issue 301):
|
||
// read from what it carries, before it is sent.
|
||
if verb == "push" && len(args) == 1 && !strings.HasPrefix(args[0], "-") {
|
||
switch carried, why, err := recordedPushOf(ctx, args[0]); {
|
||
case err != nil:
|
||
fmt.Fprintf(os.Stderr, "whether this push only moves recorded builds could not be read, so it is "+
|
||
"recorded as a push by hand: %v\n", err)
|
||
case len(carried) > 0:
|
||
act.Kind, act.Carried = link.KindRecordedBuilds, carried
|
||
default:
|
||
fmt.Printf("a push by hand, not of recorded builds only: %s\n", why)
|
||
}
|
||
}
|
||
err := onTheBus(func(conn *nats.Conn) error {
|
||
written, err := link.RecordHandAct(ctx, conn, act)
|
||
act = written
|
||
return err
|
||
})
|
||
if err != nil {
|
||
fmt.Fprintf(os.Stderr, "this act by hand could NOT be recorded in the hand-act log, and is done anyway: %v\n", err)
|
||
return
|
||
}
|
||
if act.Kind == link.KindRecordedBuilds {
|
||
fmt.Printf("recorded as %s in the hand-act log: a push of recorded builds (%s) by %s, because %q "+
|
||
"— the person's word their upgrade policy asks for, which no healer is wanted for\n", act.ID,
|
||
strings.Join(act.Carried, "; "), act.By, act.Why)
|
||
return
|
||
}
|
||
fmt.Printf("recorded as %s in the hand-act log: %s, because %q (cause: %s)\n", act.ID, act.By, act.Why, act.Cause)
|
||
}
|
||
|
||
// handActCommand is `hand-act record` and `hand-acts`.
|
||
func handActCommand(ctx context.Context, args []string) error {
|
||
if len(args) > 0 && args[0] == "record" {
|
||
set := flag.NewFlagSet("hand-act record", flag.ContinueOnError)
|
||
f := addHandActFlags(set)
|
||
positionals, err := parseAround(set, args[1:])
|
||
if err != nil {
|
||
return err
|
||
}
|
||
what := strings.TrimSpace(strings.Join(positionals, " "))
|
||
if what == "" {
|
||
return errors.New("hand-act record <what was done> --why <text> [--cause <word>] [--condition <key>]")
|
||
}
|
||
if err := f.require("hand-act record"); err != nil {
|
||
return err
|
||
}
|
||
if strings.TrimSpace(*f.cause) == "" {
|
||
return errors.New("hand-act record says the cause too: --cause <word>, the word a second " +
|
||
"act for the same reason will use — it is how a repair done twice is found")
|
||
}
|
||
if strings.EqualFold(strings.TrimSpace(*f.cause), causeDrill) {
|
||
return errors.New("a drill is not recorded as a repair: hand-act drill <what was done> --why <text> " +
|
||
"records it as the person's deliberate test it is, which no healer is wanted for. Nothing was recorded")
|
||
}
|
||
act := link.HandAct{Verb: "hand-act record", Args: []string{what}, Why: strings.TrimSpace(*f.why),
|
||
Cause: strings.TrimSpace(*f.cause), Condition: strings.TrimSpace(*f.condition)}
|
||
return onTheBus(func(conn *nats.Conn) error {
|
||
written, err := link.RecordHandAct(ctx, conn, act)
|
||
if err != nil {
|
||
return fmt.Errorf("the act could not be recorded: %w", err)
|
||
}
|
||
fmt.Printf("recorded as %s: %s did %q, because %q (cause: %s)\n", written.ID, written.By, what,
|
||
written.Why, written.Cause)
|
||
return nil
|
||
})
|
||
}
|
||
if len(args) > 0 && args[0] == "drill" {
|
||
return handActDrill(ctx, args[1:])
|
||
}
|
||
if len(args) > 0 && args[0] != "list" && !strings.HasPrefix(args[0], "-") {
|
||
return errors.New("hand-act record <what> --why <text> --cause <word> | hand-act drill <what> --why <text> " +
|
||
"| hand-acts [--days N] [--json]")
|
||
}
|
||
if len(args) > 0 && args[0] == "list" {
|
||
args = args[1:]
|
||
}
|
||
set := flag.NewFlagSet("hand-acts", flag.ContinueOnError)
|
||
days := set.Int("days", 14, "how many days back")
|
||
asJSON := set.Bool("json", false, "as data")
|
||
if _, err := parseAround(set, args); err != nil {
|
||
return err
|
||
}
|
||
return onTheBus(func(conn *nats.Conn) error {
|
||
now := time.Now()
|
||
acts, err := link.HandActs(ctx, conn, now.Add(-time.Duration(*days)*24*time.Hour))
|
||
if err != nil {
|
||
return err
|
||
}
|
||
repeated := link.RepeatedCauses(repairs(acts), now)
|
||
if *asJSON {
|
||
body, err := json.MarshalIndent(map[string]any{"acts": acts, "repeated": repeated}, "", " ")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
fmt.Println(string(body))
|
||
return nil
|
||
}
|
||
if len(acts) == 0 {
|
||
fmt.Printf("nothing was done by hand in the last %d day(s)\n", *days)
|
||
return nil
|
||
}
|
||
for i := len(acts) - 1; i >= 0; i-- {
|
||
a := acts[i]
|
||
fmt.Printf("%s %s %s %s\n by %s — %s (cause: %s", a.At.Local().Format("2006-01-02 15:04"), a.ID,
|
||
a.Verb, strings.Join(a.Args, " "), a.By, a.Why, a.Cause)
|
||
if a.Condition != "" {
|
||
fmt.Printf(", condition %s", a.Condition)
|
||
}
|
||
fmt.Println(")")
|
||
if pushedRecorded(a) {
|
||
carried := strings.Join(a.Carried, "; ")
|
||
if carried == "" {
|
||
carried = recordedBefore[a.ID]
|
||
}
|
||
fmt.Printf(" a push of recorded builds, no repair: %s\n", carried)
|
||
}
|
||
}
|
||
if len(repeated) > 0 {
|
||
causes := make([]string, 0, len(repeated))
|
||
for c, n := range repeated {
|
||
causes = append(causes, fmt.Sprintf("%s ×%d", c, n))
|
||
}
|
||
sort.Strings(causes)
|
||
fmt.Printf("\ndone by hand more than once in a fortnight — a healer is wanted (to-be 45 S15): %s\n",
|
||
strings.Join(causes, ", "))
|
||
}
|
||
return nil
|
||
})
|
||
}
|
||
|
||
// handActDrill is `hand-act drill`: an act done on purpose to test the mesh — a module stopped, a
|
||
// process killed — recorded so the conditions it raises are read as the drill they are. Its cause is
|
||
// always causeDrill and S15 never counts it (handActVerbs).
|
||
func handActDrill(ctx context.Context, args []string) error {
|
||
set := flag.NewFlagSet("hand-act drill", flag.ContinueOnError)
|
||
why := set.String("why", "", "what the drill tests — recorded in the hand-act log (novox/hq to-be 45 §7)")
|
||
condition := set.String("condition", "", "the key of the condition the drill is meant to raise, if any")
|
||
positionals, err := parseAround(set, args)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
what := strings.TrimSpace(strings.Join(positionals, " "))
|
||
if what == "" {
|
||
return errors.New("hand-act drill <what was done on purpose> --why <what it tests> [--condition <key>]")
|
||
}
|
||
if strings.TrimSpace(*why) == "" {
|
||
return errors.New("a drill says what it tests: --why <text> (recorded in the hand-act log, novox/hq " +
|
||
"to-be 45 §7). Nothing was recorded")
|
||
}
|
||
act := link.HandAct{Verb: "hand-act drill", Args: []string{what}, Why: strings.TrimSpace(*why),
|
||
Cause: causeDrill, Condition: strings.TrimSpace(*condition)}
|
||
return onTheBus(func(conn *nats.Conn) error {
|
||
written, err := link.RecordHandAct(ctx, conn, act)
|
||
if err != nil {
|
||
return fmt.Errorf("the drill could not be recorded: %w", err)
|
||
}
|
||
fmt.Printf("recorded as %s: %s drilled %q, because %q — a drill, which no healer is wanted for\n",
|
||
written.ID, written.By, what, written.Why)
|
||
return nil
|
||
})
|
||
}
|
||
|
||
// repairs are the acts that are not a person's decision by design: what S15 counts.
|
||
func repairs(acts []link.HandAct) []link.HandAct {
|
||
out := make([]link.HandAct, 0, len(acts))
|
||
for _, a := range acts {
|
||
if !personsDecision(a) {
|
||
out = append(out, a)
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
// handActsThisWeek is how many acts were done by hand in the last seven days, for `status`; -1 when
|
||
// the log could not be read, which status says rather than reading as none.
|
||
func handActsThisWeek(ctx context.Context) (int, string) {
|
||
n := -1
|
||
err := onTheBus(func(conn *nats.Conn) error {
|
||
reading, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||
defer cancel()
|
||
acts, err := link.HandActs(reading, conn, time.Now().Add(-7*24*time.Hour))
|
||
n = len(acts)
|
||
return err
|
||
})
|
||
if err != nil {
|
||
return -1, err.Error()
|
||
}
|
||
return n, ""
|
||
}
|