From 8311ecb84911704fa5d4175d490af05e4b97ae39 Mon Sep 17 00:00:00 2001 From: jochen Date: Sun, 1 Nov 2026 03:33:17 +0100 Subject: [PATCH] The hand-act recorder records through the seat alone, and the watched reader's reads are proved The controller's hand-act and warranted are gone (issue 1953 L5b) and the bus refuses them, so the recorder no longer falls back to them: an act the seat does not take is an error saying nothing was recorded. And ADR 0580 clause 1's reader is shown reading a build by key each time and a manifest once per digest. Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL --- .../frombus/frombustest/frombustest_test.go | 22 +++++++++++ go/handacts/handacts.go | 39 ++++++------------- go/handacts/handacts_test.go | 18 ++++----- 3 files changed, 41 insertions(+), 38 deletions(-) diff --git a/go/compose/frombus/frombustest/frombustest_test.go b/go/compose/frombus/frombustest/frombustest_test.go index 0db70b2..c6e3493 100644 --- a/go/compose/frombus/frombustest/frombustest_test.go +++ b/go/compose/frombus/frombustest/frombustest_test.go @@ -5,7 +5,9 @@ import ( "strings" "testing" + stdio "git.novox.be/novox/mesh-sdk/go" "git.novox.be/novox/mesh-sdk/go/compose/frombus" + "git.novox.be/novox/mesh-sdk/go/compose/fromruntime" "git.novox.be/novox/mesh-sdk/go/delivery" ) @@ -135,6 +137,26 @@ func TestTheProvisionedMeshHandsAPlanWithItsProvision(t *testing.T) { t.Errorf("%s: data items %v held %v: %v", row.name, items, iheld, err) } } + + // ADR 0580 clause 1: the watched reader reads a state it composes from out of the copy, a build by key each time it is + // asked, and a manifest once per digest. + byKey := map[string]int{} + ws := stdio.WatchedStatesOver(func(state string, handle func(stdio.StateChange) error) error { + byKey[state+" watched"]++ + return handle(stdio.StateChange{Key: "placement." + Node, Op: stdio.OpPut, Value: []byte(`{}`), Current: true}) + }, func(state, key string) ([]byte, bool, error) { byKey[state]++; return []byte(key), true, nil }) + w := fromruntime.Watched{WatchedStates: ws} + for range 2 { + _, _, _ = w.Get(frombus.BuildsRead, "restic."+fp) + _, _, _ = w.Get(frombus.ManifestsRead, "sha256-d") + if _, found, _ := w.Get(frombus.PlacementRead, "placement."+Node); !found { + t.Error("the watched reader does not answer the placement from its copy") + } + } + if byKey[frombus.BuildsRead] != 2 || byKey[frombus.ManifestsRead] != 1 || byKey[frombus.PlacementRead] != 0 || + byKey[frombus.PlacementRead+" watched"] != 1 { + t.Errorf("the watched reader read %v, want a build by key each time, a manifest once, the placement watched once", byKey) + } } func planned[P any](plans map[string]P, module string) bool { _, ok := plans[module]; return ok } diff --git a/go/handacts/handacts.go b/go/handacts/handacts.go index 4ce5937..26c1514 100644 --- a/go/handacts/handacts.go +++ b/go/handacts/handacts.go @@ -1,8 +1,7 @@ // 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 and, while no holder answers, the controller's verb until the controller hands its lease away, saying -// which log took the act. +// asks the seat alone: the controller's hand-act and warranted are gone (issue 1953 L5b) and the bus refuses them. package handacts import ( @@ -95,13 +94,13 @@ type Taken struct { Answer json.RawMessage `json:"answer,omitempty"` } -// Recorder records acts for a module (ADR 0565 clause 11): through the seat mesh-hand-acts, and while the seat has no -// holder that answers, through the controller's verb until the controller hands its lease away (the release's order -// gives no seat's holder before its callers; 00-plan.md, "Missing" 1). +// 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) - // LeaseAway says the controller's lease is handed away: then nothing falls back to it. Nil is never handed away. + // LeaseAway is no longer read: nothing falls back to the controller. Kept so a caller that sets it builds; it goes + // once no module sets it. LeaseAway func() bool } @@ -121,38 +120,22 @@ func (r Recorder) Record(q Request) (Taken, error) { body[k] = v } } - controller := map[string]any{"what": q.What, "why": q.Why, "cause": q.Cause} - if q.Condition != "" { - controller["condition"] = q.Condition - } - if q.Cause == "" { - controller["cause"] = q.Verb - } - return r.either("hand-act", body, "hand-act", controller) + 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.either("warranted", body, "warranted", body) + return r.record("warranted", body) } -func (r Recorder) either(verb string, body any, controllerVerb string, controllerBody any) (Taken, error) { +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.%s refused: %w", Seat, verb, err) + if NoResponder(err) { + return Taken{}, fmt.Errorf("%s has no holder that answers: nothing was recorded (%w)", Seat, err) } - if r.LeaseAway != nil && r.LeaseAway() { - return Taken{}, fmt.Errorf("%s has no holder that answers, and the controller handed its lease away: nothing "+ - "was recorded (%w)", Seat, err) - } - answer, cerr := r.Ask("seat:mesh-controller."+controllerVerb, controllerBody) - if cerr != nil { - return Taken{}, fmt.Errorf("%s has no holder that answers (%v), and the controller's %s refused too: %w", Seat, err, - controllerVerb, cerr) - } - return Taken{Log: LogController, Answer: answer}, nil + 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 index fe56282..6ffb90d 100644 --- a/go/handacts/handacts_test.go +++ b/go/handacts/handacts_test.go @@ -8,9 +8,8 @@ import ( "testing" ) -// ADR 0565 clauses 10 and 11 (issue 1953 L5-S2, S4): an act reads the controller's entries unchanged, and a recorder -// records through the seat, through the controller's verb only while the seat has no holder and the lease is the -// controller's, saying which log took it, and refuses to record nowhere. +// 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",` + @@ -39,14 +38,13 @@ func TestAnActIsRecordedInTheLogThatAnswers(t *testing.T) { warrants bool }{ {name: "the seat answers", log: LogSeat, asked: "seat:mesh-hand-acts.hand-act"}, - {name: "no holder yet", seat: noResponders, log: LogController, asked: "seat:mesh-hand-acts.hand-act,seat:mesh-controller.hand-act"}, - {name: "no holder, the lease handed away", seat: noResponders, away: true, refused: "handed its lease away", + {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", asked: "seat:mesh-hand-acts.hand-act"}, - {name: "neither answers", seat: noResponders, ctrl: noResponders, refused: "refused too", - asked: "seat:mesh-hand-acts.hand-act,seat:mesh-controller.hand-act"}, - {name: "a warrant without a holder", seat: noResponders, warrants: true, log: LogController, - asked: "seat:mesh-hand-acts.warranted,seat:mesh-controller.warranted"}, + {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{LeaseAway: func() bool { return c.away }, Ask: func(key string, body any) (json.RawMessage, error) {