diff --git a/cmd/mesh-controller/handacts.go b/cmd/mesh-controller/handacts.go index cf87327..a3a66f0 100644 --- a/cmd/mesh-controller/handacts.go +++ b/cmd/mesh-controller/handacts.go @@ -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) { diff --git a/cmd/mesh-controller/signals.go b/cmd/mesh-controller/signals.go index a752954..bce3d9a 100644 --- a/cmd/mesh-controller/signals.go +++ b/cmd/mesh-controller/signals.go @@ -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) } diff --git a/cmd/mesh-controller/signals_test.go b/cmd/mesh-controller/signals_test.go index ef48fa8..5155680 100644 --- a/cmd/mesh-controller/signals_test.go +++ b/cmd/mesh-controller/signals_test.go @@ -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) } }