diff --git a/go/handacts/handacts.go b/go/handacts/handacts.go deleted file mode 100644 index 1288986..0000000 --- a/go/handacts/handacts.go +++ /dev/null @@ -1,133 +0,0 @@ -// 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 `.` 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:.`): 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) -} diff --git a/go/handacts/handacts_test.go b/go/handacts/handacts_test.go deleted file mode 100644 index 11b7cea..0000000 --- a/go/handacts/handacts_test.go +++ /dev/null @@ -1,71 +0,0 @@ -package handacts - -import ( - "encoding/json" - "errors" - "reflect" - "strings" - "testing" -) - -// ADR 0565 clauses 10 and 11 (issue 1953 L5-S2, S4, L5b): an act reads the controller's entries unchanged, and a -// recorder records through the seat alone, the controller's verbs being gone, and says loudly when nothing was recorded. -func TestAnActIsRecordedInTheLogThatAnswers(t *testing.T) { - // The shape of mesh-controller internal/link HandAct, as its bucket keeps an entry (handacts.go at 0fbb5ef3). - raw := `{"id":"act-1","at":"2026-10-28T09:00:00Z","by":"person.jochen","verb":"hand-act record","why":"a stuck consumer",` + - `"cause":"consumer-lost","condition":"bus.EVENTS.x.consumer-lost","via":"telegram","ask":"a-1","proofs":["P1"],` + - `"requested-by":"bus.EVENTS.x.consumer-lost","outcome":"done"}` - var a Act - if err := json.Unmarshal([]byte(raw), &a); err != nil { - t.Fatal(err) - } - again, _ := json.Marshal(a) - var want, got map[string]any - _ = json.Unmarshal([]byte(raw), &want) - _ = json.Unmarshal(again, &got) - if !reflect.DeepEqual(want, got) || !a.Repair() || (Act{Cause: CauseDrill}).Repair() || (Act{Verb: VerbDrill}).Repair() { - t.Errorf("the controller's entry reads back as %s", again) - } - noResponders := errors.New("nats: no responders available for request") - for _, c := range []struct { - name string - seat error - ctrl error - log string - refused string - asked string - warrants bool - }{ - {name: "the seat answers", log: LogSeat, asked: "seat:mesh-hand-acts.hand-act"}, - {name: "no holder: nothing recorded, the controller never asked", seat: noResponders, refused: "nothing was recorded", - asked: "seat:mesh-hand-acts.hand-act"}, - {name: "the seat refuses", seat: errors.New("why is required"), refused: "refused, nothing was recorded", - asked: "seat:mesh-hand-acts.hand-act"}, - {name: "a warrant without a holder", seat: noResponders, warrants: true, refused: "nothing was recorded", - asked: "seat:mesh-hand-acts.warranted"}, - {name: "a warrant recorded", warrants: true, log: LogSeat, asked: "seat:mesh-hand-acts.warranted"}, - } { - var asked []string - r := Recorder{Ask: func(key string, body any) (json.RawMessage, error) { - asked = append(asked, key) - if strings.HasPrefix(key, "seat:mesh-hand-acts.") { - return json.RawMessage(`{}`), c.seat - } - return json.RawMessage(`{}`), c.ctrl - }} - var taken Taken - var err error - if c.warrants { - taken, err = r.RecordWarrant("mesh-config", "ask-1") - } else { - taken, err = r.Record(Request{What: "silenced", Why: "known", Cause: "silent", Verb: "conditions silence"}) - } - if (c.refused != "") != (err != nil) || (err != nil && !strings.Contains(err.Error(), c.refused)) || taken.Log != c.log || - strings.Join(asked, ",") != c.asked { - t.Errorf("%s: took %q, %v, asked %v", c.name, taken.Log, err, asked) - } - } - if _, err := (Recorder{}).Record(Request{What: "x"}); err == nil { - t.Error("an act with no why was recorded") - } -} diff --git a/go/packages_list_test.go b/go/packages_list_test.go index 0ede3ba..3858443 100644 --- a/go/packages_list_test.go +++ b/go/packages_list_test.go @@ -24,7 +24,7 @@ import ( // request: does every module share it, or is it a shape more than one part reads? var meshSDKPackages = []string{ ".", "announce", "announcetest", "buildstep", "buscalls", "buscompose", "busstate", "busretry", "calladdress", "catalogue", "channel", - "declaration", "delivery", "forgekey", "handacts", "health", "internal/beside", "keys", "kvlimit", "membership", "motion", + "declaration", "delivery", "forgekey", "health", "internal/beside", "keys", "kvlimit", "membership", "motion", "objectlink", "outward", "overlay", "pernode", "process", "provisions", "sdkversion", "seal", "seatdef", "seatlist", "seatlist/seatlisttest", "testbus", "views", "views/viewstest", } @@ -39,7 +39,7 @@ var clientSeats = []string{"operator-channel", "mesh-vault", "node-engine", "npm var moduleWord = regexp.MustCompile(`\bmesh-(?:delivery|grants|vault|host|config|provisioner|broker|knowledge|enrolment)\b[-a-z]*|\b(?:indexer|assigner|teller|watchman|checker|builder)(?:-views|_read_)`) // notInSDK are the packages that moved to a seat's client or contract (ADR 0627 §1, §4) and may not come back. -var notInSDK = []string{"asks", "asker", "publish", "composer", "parts", "provider"} +var notInSDK = []string{"asks", "asker", "publish", "composer", "parts", "provider", "handacts"} // untilTheJoin is what ADR 0627 still moves at the group's join, each `: `, naming what the checks below // would refuse; it only shrinks. The vault module's name in delivery and provisions stays past this milestone's pin diff --git a/go/testdata/seat-names.txt b/go/testdata/seat-names.txt index e90d10a..3a67be4 100644 --- a/go/testdata/seat-names.txt +++ b/go/testdata/seat-names.txt @@ -37,7 +37,6 @@ delivery/placement.go: mesh-self-check delivery/provided.go: the-private-network delivery/signers.go: mesh-vault delivery/stepasks.go: node-build-node -handacts/handacts.go: mesh-hand-acts health/findings.go: mesh-conditions health/findings.go: mesh-healers health/findings.go: mesh-self-check