Merge pull request 'S15: a hand act a person decides by design is no repair, read from the verb that recorded it (hq to-be 45 §7)' (#94) from fix/s15-a-persons-decision-is-no-repair into main
This commit was merged in pull request #94.
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -27,6 +28,62 @@ import (
|
||||
// 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
|
||||
}
|
||||
|
||||
// causeLeakedInLogs is the cause a rotation after a value was printed into a log gives.
|
||||
const causeLeakedInLogs = "leaked-in-logs"
|
||||
|
||||
// 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.
|
||||
{Verb: "push"},
|
||||
{Verb: "plans stop"},
|
||||
{Verb: "plans close"},
|
||||
{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.
|
||||
{Verb: "hand-act record"},
|
||||
// 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))
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// handActFlags are the flags every repairing verb takes.
|
||||
type handActFlags struct {
|
||||
why, cause, condition *string
|
||||
@@ -144,7 +201,7 @@ func handActCommand(ctx context.Context, args []string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
repeated := link.RepeatedCauses(acts, now)
|
||||
repeated := link.RepeatedCauses(repairs(acts), now)
|
||||
if *asJSON {
|
||||
body, err := json.MarshalIndent(map[string]any{"acts": acts, "repeated": repeated}, "", " ")
|
||||
if err != nil {
|
||||
@@ -179,6 +236,17 @@ func handActCommand(ctx context.Context, args []string) error {
|
||||
})
|
||||
}
|
||||
|
||||
// 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) {
|
||||
|
||||
@@ -572,21 +572,14 @@ func watchLease(f *signalFacts) []conditions.Observation {
|
||||
// handActsWithin is how far back a repeated cause counts (S15).
|
||||
const handActsWithin = 14 * 24 * time.Hour
|
||||
|
||||
// personsDecision are the causes of hand acts that are a person's decision by design, never a repair a
|
||||
// healer could take over: approving or rejecting a retirement, and deleting what was retired (novox/hq
|
||||
// ADR 0230 — nothing is retired past the bound or deleted without a person). Repeated, they are the
|
||||
// mesh working as decided, not a healer wanted.
|
||||
var personsDecision = map[string]bool{kindRetireWaiting: true, kindCleanupWaiting: true}
|
||||
|
||||
// watchHandActs is S15: a cause recorded by hand twice within a fortnight is a healer wanted, named by
|
||||
// the cause. **A heal is never a hand act** (healers.go), so a cause a healer exists for and a person
|
||||
// still repaired twice says the healer is not enough — its reach or its budget — and is said so.
|
||||
// still repaired twice says the healer is not enough — its reach or its budget — and is said so. **An act
|
||||
// that is a person's decision by design is no repair** (handActVerbs), so it never counts; one counted
|
||||
// before this was so clears on the next tick, as any condition the row no longer sees.
|
||||
func watchHandActs(f *signalFacts) []conditions.Observation {
|
||||
recent := make([]link.HandAct, 0, len(f.handActs))
|
||||
for _, a := range f.handActs {
|
||||
if personsDecision[a.Cause] {
|
||||
continue
|
||||
}
|
||||
for _, a := range repairs(f.handActs) {
|
||||
if f.now.Sub(a.At) <= handActsWithin {
|
||||
recent = append(recent, a)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,9 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -161,16 +164,135 @@ func TestARepeatedHandActNamesItsCauseAndItsHealer(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// **Approving a retirement and deleting what was retired are a person's decision by design** (ADR
|
||||
// 0230): repeated, they are not a healer wanted.
|
||||
func TestARetirementDecisionRepeatedWantsNoHealer(t *testing.T) {
|
||||
// actOf is an act in the log recorded by a verb, with its cause.
|
||||
func actOf(at time.Time, verb, cause string) link.HandAct {
|
||||
a := actByHand(at, cause)
|
||||
a.Verb, a.Args = verb, nil
|
||||
return a
|
||||
}
|
||||
|
||||
// **An act a person decides by design is no repair** (handActVerbs): approving or rejecting a
|
||||
// retirement and deleting what was retired (ADR 0230), a bus upgrade and a backlog released (ADR 0236),
|
||||
// and a rotation after a leak — repeated, none is a healer wanted, whatever cause it gives.
|
||||
func TestAPersonsDecisionRepeatedWantsNoHealer(t *testing.T) {
|
||||
now := time.Date(2026, 10, 6, 12, 0, 0, 0, time.UTC)
|
||||
for _, c := range []struct{ verb, cause string }{
|
||||
{"retire approve", kindRetireWaiting}, {"retire reject", kindRetireWaiting},
|
||||
{"cleanup delete", kindCleanupWaiting}, {"bus upgrade", "bus-upgrade"},
|
||||
{"bus upgrade", "a word the person chose"}, {"upgrade release-backlog", "upgrade release-backlog"},
|
||||
{"secret rotate", causeLeakedInLogs},
|
||||
} {
|
||||
f := calm(now)
|
||||
f.handActs = []link.HandAct{actOf(now.Add(-26*time.Hour), c.verb, c.cause),
|
||||
actOf(now.Add(-time.Hour), c.verb, c.cause)}
|
||||
if got := watchHandActs(f); len(got) != 0 {
|
||||
t.Errorf("%s (cause %s) repeated asked for a healer: %+v", c.verb, c.cause, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// **What repairs still counts**: a push by hand above all (ADR 0236 exists to end it), a rotation for
|
||||
// any cause but a leak, an act recorded outside the mesh whatever cause it names, and a verb the table
|
||||
// does not know.
|
||||
func TestARepairRepeatedStillWantsAHealer(t *testing.T) {
|
||||
now := time.Date(2026, 10, 6, 12, 0, 0, 0, time.UTC)
|
||||
for _, c := range []struct{ verb, cause string }{
|
||||
{"push", "push"}, {"plans close", "plans close"}, {"conditions silence", "consumer-behind"},
|
||||
{"secret rotate", "stopped-working"}, {"hand-act record", "bus-upgrade"},
|
||||
{"hand-act record", kindCleanupWaiting}, {"a verb nobody listed", "x"},
|
||||
} {
|
||||
f := calm(now)
|
||||
f.handActs = []link.HandAct{actOf(now.Add(-26*time.Hour), c.verb, c.cause),
|
||||
actOf(now.Add(-time.Hour), c.verb, c.cause)}
|
||||
if got := watchHandActs(f); len(got) != 1 || got[0].Kind != "healer-wanted" {
|
||||
t.Errorf("%s (cause %s) repeated wanted no healer: %+v", c.verb, c.cause, got)
|
||||
}
|
||||
}
|
||||
// A decision does not make up the second of a cause a repair recorded once.
|
||||
f := calm(now)
|
||||
f.handActs = []link.HandAct{actByHand(now.Add(-2*time.Hour), kindRetireWaiting),
|
||||
actByHand(now.Add(-time.Hour), kindRetireWaiting), actByHand(now.Add(-time.Hour), kindCleanupWaiting),
|
||||
actByHand(now.Add(-time.Minute), kindCleanupWaiting)}
|
||||
f.handActs = []link.HandAct{actOf(now.Add(-26*time.Hour), "hand-act record", "bus-upgrade"),
|
||||
actOf(now.Add(-time.Hour), "bus upgrade", "bus-upgrade")}
|
||||
if got := watchHandActs(f); len(got) != 0 {
|
||||
t.Fatalf("a person's decision asked for a healer: %+v", got)
|
||||
t.Errorf("one repair and one decision asked for a healer: %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
// **A healer-wanted already open for a decision clears on the next tick** — the log of 2026-10-06:
|
||||
// a bus upgrade recorded after the fact and one through its verb, and two rotations after a leak.
|
||||
func TestAHealerWantedOpenForADecisionClearsOnTheNextTick(t *testing.T) {
|
||||
now := time.Date(2026, 10, 6, 18, 0, 0, 0, time.UTC)
|
||||
store := conditions.NewInMemory()
|
||||
k := conditions.NewKeeper(t.Context(), conditions.Options{Store: store, History: store,
|
||||
Teller: &conditions.Told{}, Now: func() time.Time { return now }})
|
||||
defer k.Close(context.Background())
|
||||
// What the build before this one raised, as it raised it.
|
||||
var before []conditions.Observation
|
||||
for _, cause := range []string{"bus-upgrade", causeLeakedInLogs} {
|
||||
before = append(before, conditions.Observation{Scope: conditions.ScopeMesh, ID: "hand-acts." + cause,
|
||||
Token: "healer-wanted", Kind: "healer-wanted", Severity: conditions.Warning, Summary: "repaired twice"})
|
||||
}
|
||||
if err := k.Reconcile(t.Context(), "S15", before); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if open, _ := k.Open(t.Context()); len(open) != 2 {
|
||||
t.Fatalf("the conditions of the build before were not open: %+v", open)
|
||||
}
|
||||
f := calm(now)
|
||||
f.handActs = []link.HandAct{actOf(now.Add(-11*time.Hour), "hand-act record", "bus-upgrade"),
|
||||
actOf(now.Add(-8*time.Hour), "secret rotate", causeLeakedInLogs),
|
||||
actOf(now.Add(-8*time.Hour), "secret rotate", causeLeakedInLogs),
|
||||
actOf(now.Add(-time.Hour), "bus upgrade", "bus-upgrade")}
|
||||
w := &watchdogs{keeper: k, started: now.Add(-time.Hour)}
|
||||
w.see(t.Context(), f)
|
||||
if open, _ := k.Open(t.Context()); len(open) != 0 {
|
||||
t.Fatalf("a healer-wanted for a person's decision stayed open: %+v", open)
|
||||
}
|
||||
}
|
||||
|
||||
// **Every verb that writes the hand-act log is in handActVerbs**, so whether it is a repair is said
|
||||
// where S15 reads it, not left to a default nobody chose.
|
||||
func TestEveryVerbThatRecordsAHandActIsInTheTable(t *testing.T) {
|
||||
listed := map[string]bool{}
|
||||
for _, v := range handActVerbs {
|
||||
if listed[v.Verb] {
|
||||
t.Errorf("%s is in the table twice", v.Verb)
|
||||
}
|
||||
listed[v.Verb] = true
|
||||
if len(v.DecidedFor) > 0 && v.Decision == "" {
|
||||
t.Errorf("%s limits a decision it does not state", v.Verb)
|
||||
}
|
||||
}
|
||||
files, err := filepath.Glob("*.go")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
literal := regexp.MustCompile(`(?:\.record\(ctx, |HandAct\{Verb: |RetireApproved: |RetireRejected: |RetireDeleted: )"([^"]+)"\s*[,})]`)
|
||||
composed := regexp.MustCompile(`\.record\(ctx, "([^"]+ )"\s*\+`)
|
||||
found := 0
|
||||
for _, name := range files {
|
||||
if strings.HasSuffix(name, "_test.go") {
|
||||
continue
|
||||
}
|
||||
body, err := os.ReadFile(name)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, m := range literal.FindAllStringSubmatch(string(body), -1) {
|
||||
found++
|
||||
if !listed[m[1]] {
|
||||
t.Errorf("%s records %q, which handActVerbs does not list", name, m[1])
|
||||
}
|
||||
}
|
||||
// "plans " + stop|close, "retire " + approve|reject: some listed verb begins with it.
|
||||
for _, m := range composed.FindAllStringSubmatch(string(body), -1) {
|
||||
found++
|
||||
if !slices.ContainsFunc(handActVerbs, func(v handActVerb) bool { return strings.HasPrefix(v.Verb, m[1]) }) {
|
||||
t.Errorf("%s records %q…, which no verb of handActVerbs begins with", name, m[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
if found < 12 {
|
||||
t.Fatalf("found %d recording verbs, fewer than the table's own: the search no longer sees them", found)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user