diff --git a/modules/mesh-conditions/cmd/mesh-conditions/compare.go b/modules/mesh-conditions/cmd/mesh-conditions/compare.go deleted file mode 100644 index e1583e7a..00000000 --- a/modules/mesh-conditions/cmd/mesh-conditions/compare.go +++ /dev/null @@ -1,185 +0,0 @@ -package main - -// **Built dark, compared, then switched by the operator** (novox/hq ADR 0564 clause 9; issue 1952 C5, C8): `compare` -// lists the open keys of the keeper and of the controller, each difference with its reason; the kinds that end with the -// controller, the kinds only the keeper raises and the keys held while their module is delivered are listed apart and -// not counted. `take-over` is refused (issue 1126): the keeper is the one source since the controller ended by its -// unassignment; a yes to an ask raised before still writes the take-over key naming the keeper. `hand-back` is gone (issue 774): nothing -// reads the take-over key any more, so it is answered as gone and changes nothing. - -import ( - "encoding/json" - "errors" - "fmt" - "sort" - "strings" - "time" - - stdio "git.novox.be/novox/mesh-sdk/go" - "git.novox.be/novox/mesh-sdk/go/asks" - "git.novox.be/novox/mesh-sdk/go/health" -) - -// Difference is one key the two sides do not agree on, or one listed apart. -type Difference struct { - Key string `json:"key"` - Kind string `json:"kind"` - Reason string `json:"reason"` -} - -// Comparison is compare's answer. -type Comparison struct { - Differences []Difference `json:"differences"` - // Apart are the keys not counted: of kinds that end with the controller, raised only by the keeper, or held. - Apart []Difference `json:"apart"` - Same int `json:"same"` - At time.Time `json:"at"` -} - -// Empty says nothing differs. -func (c Comparison) Empty() bool { return len(c.Differences) == 0 } - -// Compare is the keeper's open conditions against the controller's. -func (k *Keeper) Compare() (Comparison, error) { - mine, err := k.Open() - if err != nil { - return Comparison{}, err - } - k.mu.Lock() - k.init() - theirs := map[string]health.Condition{} - for key, c := range k.ctrl { - theirs[key] = c - } - heldNow := map[string]held{} - for key, h := range k.held { - heldNow[key] = h - } - k.mu.Unlock() - out := Comparison{Differences: []Difference{}, Apart: []Difference{}, At: k.Now().UTC()} - open := map[string]health.Condition{} - for _, c := range mine { - open[c.Key] = c - } - keys := map[string]bool{} - for key := range open { - keys[key] = true - } - for key := range theirs { - keys[key] = true - } - for key := range heldNow { - keys[key] = true - } - sorted := make([]string, 0, len(keys)) - for key := range keys { - sorted = append(sorted, key) - } - sort.Strings(sorted) - for _, key := range sorted { - c, inMine := open[key] - t, inTheirs := theirs[key] - kind := c.Kind - if !inMine { - kind = t.Kind - } - home, homed := k.homeOf(kind) - if h, ok := heldNow[key]; ok { - out.Apart = append(out.Apart, Difference{Key: key, Kind: kind, - Reason: fmt.Sprintf("held while %s is delivered on %s (version %s)", h.Module, h.Node, short(h.Version))}) - continue - } - switch { - case homed && (home.Carried || home.Replaced): - why := "ends with the controller: no longer carried (issue 1126)" - if home.Replaced { - why = "ends with the controller: its fault is observed otherwise" - } - if home.Why != "" { - why += " (" + home.Why + ")" - } - out.Apart = append(out.Apart, Difference{Key: key, Kind: kind, Reason: why}) - case inMine && !inTheirs && (!homed || !home.Controller): - out.Apart = append(out.Apart, Difference{Key: key, Kind: kind, Reason: "keeper only: the controller never raises this kind"}) - case inMine && !inTheirs: - out.Differences = append(out.Differences, Difference{Key: key, Kind: kind, - Reason: "open at the keeper, not at the controller"}) - case !inMine && !homed: - out.Differences = append(out.Differences, Difference{Key: key, Kind: kind, - Reason: fmt.Sprintf("open at the controller; no home is decided for the kind %s (ADR 0564's gap)", kind)}) - case !inMine: - out.Differences = append(out.Differences, Difference{Key: key, Kind: kind, - Reason: fmt.Sprintf("open at the controller; %s has not written its finding", home.Observer)}) - case c.Severity != t.Severity: - out.Differences = append(out.Differences, Difference{Key: key, Kind: kind, - Reason: fmt.Sprintf("%s at the keeper, %s at the controller", c.Severity, t.Severity)}) - default: - out.Same++ - } - } - return out, nil -} - -// Asks is the operator channel as the keeper asks it: an approve ask under its own name, and where one stands. -type Asks interface { - Ask(a asks.Ask) error -} - -// TakeOver is refused: the keeper is the one source of conditions since the controller ended (issue 1126). -func (k *Keeper) TakeOver(a Asks, by, why string) (map[string]any, error) { - if strings.TrimSpace(why) == "" { - return nil, errors.New("a take-over says why") - } - return nil, errors.New("the keeper is the one source already: the controller ended by its unassignment (issue 1126)") -} - -// Warranted is the act on the operator's answer: a yes, compare still empty, writes the take-over key naming the keeper. -func (k *Keeper) Warranted(o asks.Option, w asks.Warrant) error { - if o.ID != "yes" { - k.Logf("the take-over %s was answered %s: alarms stay with the controller", w.Ask, o.ID) - return nil - } - cmp, err := k.Compare() - if err != nil { - return err - } - if !cmp.Empty() { - return fmt.Errorf("the take-over %s was approved, and compare now shows %d difference(s): not switched", w.Ask, - len(cmp.Differences)) - } - return k.writeTakeover(health.Takeover{Follow: health.FollowKeeper, At: k.Now().UTC(), By: w.Says(), Ask: w.Ask}) -} - -// HandBack is gone (issue 774): the readers of conditions hear the keeper alone and nothing reads the take-over key, so -// a hand-back written there would answer success and do nothing (ADR 0564 clause 9). It is answered as gone, naming the -// record, and changes nothing. -func (k *Keeper) HandBack(by, why string) (map[string]any, error) { - return nil, errors.New("hand-back is gone: the readers of conditions hear the condition keeper alone, and nothing " + - "reads the take-over key, so there is no controller to hand them back to (ADR 0564 clause 9)") -} - -// writeTakeover writes the take-over key, and keeps it in history as the record of the switch. -func (k *Keeper) writeTakeover(t health.Takeover) error { - raw, err := health.EncodeTakeover(t) - if err != nil { - return err - } - _, rev, found, err := k.Store.Get(TakeoverState, health.TakeoverKey) - if err != nil { - return err - } - if found { - _, err = k.Store.Update(TakeoverState, health.TakeoverKey, json.RawMessage(raw), rev) - } else { - _, err = k.Store.Create(TakeoverState, health.TakeoverKey, json.RawMessage(raw)) - } - if errors.Is(err, stdio.ErrChanged) { - return fmt.Errorf("the take-over key moved under this write: read it again and ask again") - } - if err != nil { - return err - } - k.Takeover(t) - k.Logf("the readers of conditions follow the %s from now (%s)", t.Follow, t.By) - return nil -} diff --git a/modules/mesh-conditions/cmd/mesh-conditions/hold.go b/modules/mesh-conditions/cmd/mesh-conditions/hold.go index 1a26a5ba..c95d106f 100644 --- a/modules/mesh-conditions/cmd/mesh-conditions/hold.go +++ b/modules/mesh-conditions/cmd/mesh-conditions/hold.go @@ -309,7 +309,7 @@ func short(fp string) string { return fp } -// Held is every key held now, for the read tool and compare. +// Held is every key held now, for the read tool. func (k *Keeper) Held() []held { k.mu.Lock() defer k.mu.Unlock() diff --git a/modules/mesh-conditions/cmd/mesh-conditions/hold_test.go b/modules/mesh-conditions/cmd/mesh-conditions/hold_test.go index 9beaa64c..ef555a03 100644 --- a/modules/mesh-conditions/cmd/mesh-conditions/hold_test.go +++ b/modules/mesh-conditions/cmd/mesh-conditions/hold_test.go @@ -73,17 +73,6 @@ func TestADeliveryHoldsItsOwnFindingsUntilItEnds(t *testing.T) { if got := r.changes(); got != "raised "+other+", raised "+machine { t.Errorf("%s: during the delivery said %q: another module's and the machine's at once, nothing of its own", c.name, got) } - // C8: compare lists the held keys as held, never as a difference. - cmp, _ := r.k.Compare() - held := 0 - for _, d := range cmp.Apart { - if strings.HasPrefix(d.Reason, "held while mesh-controller is delivered on novox") { - held++ - } - } - if held != 1 { - t.Errorf("%s: compare lists %d held, %+v", c.name, held, cmp) - } // Its own finding goes unless it stands. if !c.standing { _ = r.k.Finding(watchman, own, nil, true) diff --git a/modules/mesh-conditions/cmd/mesh-conditions/homes.go b/modules/mesh-conditions/cmd/mesh-conditions/homes.go index c900b8cf..a1c25a10 100644 --- a/modules/mesh-conditions/cmd/mesh-conditions/homes.go +++ b/modules/mesh-conditions/cmd/mesh-conditions/homes.go @@ -1,12 +1,11 @@ package main -// **Where each condition kind lives after the take-over** (novox/hq ADR 0564 clauses 4, 6, 7 and 9, the Context's probe -// table; issue 1952 C3, C5): data, so a kind's home is a row and never code. `compare` reads it to tell a difference from -// a kind that ends with the controller or that the controller never raises, and the keeper reads it to clear, once, what -// it carried of the kinds that ended with the controller (EndCarried, issue 1126). +// **Where each condition kind the controller raised lives now** (novox/hq ADR 0564 clauses 4, 6, 7 and 9, the Context's +// probe table; issue 1952 C3, C5): data, so a kind's home is a row and never code. The keeper reads it to clear, once, +// what it carried of the kinds that ended with the controller (EndCarried, issue 1126); `compare`, which read it too, +// went with the controller (ADR 0590). // -// The rows are ADR 0564's and the lead's table of 2026-10-29, by the controller's source of each kind. A kind with no -// row is named by `compare` as "no home decided". +// The rows are ADR 0564's and the lead's table of 2026-10-29, by the controller's source of each kind. // Home is where one condition kind lives. type Home struct { diff --git a/modules/mesh-conditions/cmd/mesh-conditions/keeper.go b/modules/mesh-conditions/cmd/mesh-conditions/keeper.go index daabadb3..e72cd426 100644 --- a/modules/mesh-conditions/cmd/mesh-conditions/keeper.go +++ b/modules/mesh-conditions/cmd/mesh-conditions/keeper.go @@ -28,12 +28,9 @@ const ( Seat = "mesh-conditions" ConditionsState = health.ConditionsState - TakeoverState = health.TakeoverState HistoryState = "history" HeldState = "held" BoundsState = "bounds" - // AskedState keeps the keeper's asks of the operator (go/asker). - AskedState = "asked" ) // HistoryKeptFor is how long a transition is kept (clause 3). @@ -94,7 +91,7 @@ type Keeper struct { Say func(e health.Event) error Now func() time.Time Logf func(format string, a ...any) - // Homes says, by kind, where each controller kind lives after the take-over (homes.go); Homes when nil. + // Homes says, by kind, where each kind the controller raised lives now (homes.go); Homes when nil. Homes map[string]Home // Bound is a delivery's hold bound; delivery.DefaultDeliveryBound when zero (hold.go). Bound time.Duration @@ -103,10 +100,6 @@ type Keeper struct { findings map[string]map[string]health.Finding blind map[string]string attempts map[string]health.Attempt - // ctrl is the controller's conditions as Controller hands them: compare's other side alone, never a condition's - // input (issue 1126). Nothing feeds it since issue 774; it goes with compare. - ctrl map[string]health.Condition - takeover health.Takeover wants map[string]delivery.Want haves map[string]delivery.Have // holders are, by `.`, the module the placement says holds the seat there. @@ -125,11 +118,10 @@ type Keeper struct { func (k *Keeper) init() { if k.findings == nil { - k.findings, k.blind, k.attempts, k.ctrl = map[string]map[string]health.Finding{}, map[string]string{}, - map[string]health.Attempt{}, map[string]health.Condition{} + k.findings, k.blind, k.attempts = map[string]map[string]health.Finding{}, map[string]string{}, + map[string]health.Attempt{} k.wants, k.haves, k.stuck, k.held, k.cleared = map[string]delivery.Want{}, map[string]delivery.Have{}, map[string]string{}, map[string]held{}, map[string]clearedMemo{} - k.takeover = health.Takeover{V: health.TakeoverVersion, Follow: health.FollowController} k.passed, k.outOfHold, k.holders = map[string]string{}, map[string]string{}, map[string]string{} k.placement = delivery.NewPlacementKeys() } @@ -209,26 +201,6 @@ func (k *Keeper) Attempt(key string, value []byte, deleted bool) error { return k.Settle(condition) } -// Controller takes one change of the controller's `conditions` bucket, for compare alone: no condition is carried from -// it (issue 1126). -func (k *Keeper) Controller(key string, value []byte, deleted bool) error { - k.mu.Lock() - k.init() - if deleted { - delete(k.ctrl, key) - } else { - var c health.Condition - if err := json.Unmarshal(value, &c); err != nil { - k.mu.Unlock() - k.Logf("the controller's condition %s is not readable, so compare does not list it: %v", key, err) - return nil - } - k.ctrl[key] = c - } - k.mu.Unlock() - return nil -} - // endedWhy is why a condition of a kind that ended with the controller is cleared. const endedWhy = "its kind ended with the controller, which was unassigned (issue 1126): no finding raises it again" @@ -273,14 +245,6 @@ func (k *Keeper) EndCarried() (int, error) { return n, nil } -// Takeover takes the keeper's own take-over key as read. -func (k *Keeper) Takeover(t health.Takeover) { - k.mu.Lock() - defer k.mu.Unlock() - k.init() - k.takeover = t -} - // Settle derives the condition key should be and writes the difference (clause 2), retried while another write moves // it under this one. func (k *Keeper) Settle(key string) error { diff --git a/modules/mesh-conditions/cmd/mesh-conditions/keeper_test.go b/modules/mesh-conditions/cmd/mesh-conditions/keeper_test.go index d061f720..01367614 100644 --- a/modules/mesh-conditions/cmd/mesh-conditions/keeper_test.go +++ b/modules/mesh-conditions/cmd/mesh-conditions/keeper_test.go @@ -10,7 +10,6 @@ import ( "time" stdio "git.novox.be/novox/mesh-sdk/go" - "git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/health" ) @@ -242,28 +241,6 @@ func TestTheKeeperCarriesNoControllerConditionAndClearsWhatItCarriedOnce(t *test t.Fatal(err) } r.said = nil - until := r.now.Add(time.Hour) - for _, c := range []health.Condition{ - {Key: "core.controller.deaf", Kind: "controller-deaf", Source: "S4", Subject: health.Subject{Scope: health.ScopeCore, ID: "controller"}, - Severity: health.Urgent, Summary: "deaf", Resolver: "self"}, - {Key: key, Kind: "silent", Severity: health.Urgent, Resolver: health.ResolverOperator, - Tried: []health.Tried{{At: r.now, What: "x", Outcome: "escalated", By: "healer H1"}}, - Silenced: &health.Silence{Until: until}}, - } { - raw, _ := json.Marshal(c) - if err := r.k.Controller(c.Key, raw, false); err != nil { - t.Fatal(err) - } - if _, err := r.k.Resync(); err != nil { - t.Fatal(err) - } - } - if _, open := r.open("core.controller.deaf"); open || r.changes() != "" { - t.Error("the controller's condition was carried") - } - if c, _ := r.open(key); c.Severity != health.Warning || c.Resolver != health.ResolverSelf || len(c.Tried) != 0 || c.Silenced != nil { - t.Errorf("the controller's escalation reached the keeper's condition: %+v", c) - } // Carried while the controller ran: two of kinds that ended with it, still open in conditions. for _, c := range []health.Condition{ {Key: "core.controller.deaf", Kind: "controller-deaf", Severity: health.Urgent, Resolver: "self", Count: 1}, @@ -285,51 +262,10 @@ func TestTheKeeperCarriesNoControllerConditionAndClearsWhatItCarriedOnce(t *test } } -// ADR 0564 clause 9 (issue 1952 C5, C8; the lead 2026-10-29): compare counts only what both sides can raise. A key held -// while its module is delivered, a kind that ends with the controller and a kind the controller never raises are listed -// apart; a kind with no home decided is a difference naming the gap; take-over is refused (issue 1126), and -// hand-back is gone (issue 774, review g1): answered as gone, naming its record, and writing nothing. -func TestCompareCountsOnlyWhatBothSidesRaise(t *testing.T) { - r := newRig() - put := func(key, kind string) []byte { - raw, _ := json.Marshal(health.Condition{Key: key, Kind: kind, Severity: health.Warning, Source: "x"}) - return raw - } - key, raw := finding(t, watchman, health.ScopeMachine, "g14", "silent", "S1", health.Warning, r.now) - _ = r.k.Finding(watchman, key, raw, false) - _ = r.k.Controller(key, put(key, "silent"), false) - only, onlyRaw := finding(t, delivery.AssignerModule, health.ScopeSeat, "mesh-placement.ace", "states-unbound", "say", health.Warning, r.now) - _ = r.k.Finding(delivery.AssignerModule, only, onlyRaw, false) - _ = r.k.Controller("core.controller.deaf", put("core.controller.deaf", "controller-deaf"), false) - _ = r.k.Controller("delivery.x.mystery", put("delivery.x.mystery", "mystery"), false) - _ = r.k.Controller("bus.x.consumer-wrong", put("bus.x.consumer-wrong", "consumer-wrong"), false) - _ = r.k.Controller("seat.vault.ace.holder-silent", put("seat.vault.ace.holder-silent", "holder-silent"), false) - _ = r.k.Controller("bus.nats.upgrade-failed", put("bus.nats.upgrade-failed", "bus-upgrade-failed"), false) - cmp, err := r.k.Compare() - if err != nil { - t.Fatal(err) - } - reasons := map[string]string{} - for _, d := range append(append([]Difference(nil), cmp.Differences...), cmp.Apart...) { - reasons[d.Key] = d.Reason - } - for key, says := range map[string]string{ - only: "keeper only", - "core.controller.deaf": "carried", - "delivery.x.mystery": "no home is decided for the kind mystery", - "bus.x.consumer-wrong": "mesh-self-check has not written its finding", - // A kind of a probe the operator dropped (2026-10-30) is carried, never missing. - "seat.vault.ace.holder-silent": "no longer carried (issue 1126) (dropped 2026-10-30 by the operator", - // The controller's own bus upgrade ends with its lease (the operator, 2026-10-31; issue 1952). - "bus.nats.upgrade-failed": "ends with the controller", - } { - if !strings.Contains(reasons[key], says) { - t.Errorf("%s: %q, want it to say %q", key, reasons[key], says) - } - } - if cmp.Same != 1 || len(cmp.Differences) != 2 { - t.Errorf("compare: %+v", cmp) - } +// ADR 0564 (issue 1952 C5; the lead 2026-10-29): every kind the controller raised has its home in the table, and the +// kinds that ended with it are marked carried, which EndCarried clears once (issue 1126). compare, take-over and +// hand-back went with the controller (ADR 0590): there is no other side to compare and nothing to hand back to. +func TestEveryControllerKindHasItsHome(t *testing.T) { // The lead's table of 2026-10-29: each kind has its home, lease-split and image-missing do not end, and the 22 // that end are carried; with the 11 kinds of the probes the operator dropped on 2026-10-30, 33. carried := 0 @@ -355,14 +291,4 @@ func TestCompareCountsOnlyWhatBothSidesRaise(t *testing.T) { if len(Homes) != 84 || carried != 34 { // 19 of ADR 0564, the 63 of the table, plan-failed and stalled t.Errorf("the table holds %d kinds, %d carried", len(Homes), carried) } - if _, err := r.k.TakeOver(nil, "jochen", "now"); err == nil || !strings.Contains(err.Error(), "one source already") { - t.Errorf("take-over: %v", err) - } - if _, err := r.k.HandBack("jochen", "back"); err == nil || !strings.Contains(err.Error(), "hand-back is gone") || - !strings.Contains(err.Error(), "ADR 0564 clause 9") { - t.Errorf("hand-back: %v, want it answered as gone naming its record", err) - } - if _, _, found, _ := r.k.Store.Get(TakeoverState, health.TakeoverKey); found { - t.Error("hand-back or take-over wrote the take-over key") - } } diff --git a/modules/mesh-conditions/cmd/mesh-conditions/main.go b/modules/mesh-conditions/cmd/mesh-conditions/main.go index c41a63ee..a70504ec 100644 --- a/modules/mesh-conditions/cmd/mesh-conditions/main.go +++ b/modules/mesh-conditions/cmd/mesh-conditions/main.go @@ -13,8 +13,6 @@ import ( "time" stdio "git.novox.be/novox/mesh-sdk/go" - "git.novox.be/novox/mesh-sdk/go/asker" - "git.novox.be/novox/mesh-sdk/go/asks" "git.novox.be/novox/mesh-sdk/go/health" ) @@ -90,15 +88,6 @@ func Keep(k *Keeper) { } } } - if e, err := stdio.State(TakeoverState).Get(health.TakeoverKey); err == nil { - var raw []byte - if e != nil { - raw = e.Value - } - if t, err := health.DecodeTakeover(raw, e != nil); err == nil { - k.Takeover(t) - } - } said := func(err error) error { if err != nil { logf("%v", err) @@ -134,20 +123,11 @@ func Keep(k *Keeper) { func main() { k := NewKeeper() - client := asker.New(AskedState) - client.Module = Module go func() { <-stdio.Served() Keep(k) - if err := client.Listen(k.Warranted, func(h asker.Handled, w asks.Warrant, err error) { - if err != nil { - logf("the answer to %s (%s): %v", w.Ask, h, err) - } - }); err != nil { - logf("the operator's answers cannot be heard: %v", err) - } }() - if err := stdio.Serve("", Tools(k, client)); err != nil { + if err := stdio.Serve("", Tools(k)); err != nil { logf("%v", err) os.Exit(1) } diff --git a/modules/mesh-conditions/cmd/mesh-conditions/manifest_test.go b/modules/mesh-conditions/cmd/mesh-conditions/manifest_test.go index d22707dd..79ccbe5c 100644 --- a/modules/mesh-conditions/cmd/mesh-conditions/manifest_test.go +++ b/modules/mesh-conditions/cmd/mesh-conditions/manifest_test.go @@ -49,11 +49,10 @@ func TestTheManifestSaysWhatTheCodeDoes(t *testing.T) { ttl[HistoryState], ttl[BoundsState], keyTTL[BoundsState]) } // Clause 12: each state with the tool that reads it, each act with the tool that starts it. - readBy := map[string]string{ConditionsState: Seat + ".conditions", HistoryState: HistoryTool, TakeoverState: TakeoverTool, - HeldState: HeldTool, BoundsState: HeldTool, AskedState: Seat + ".take-over", "a resync": Seat + ".resync", - "what it reads": InputsTool} + readBy := map[string]string{ConditionsState: Seat + ".conditions", HistoryState: HistoryTool, + HeldState: HeldTool, BoundsState: HeldTool, "a resync": Seat + ".resync", "what it reads": InputsTool} tools := map[string]bool{} - for _, tool := range Tools(&Keeper{}, nil) { + for _, tool := range Tools(&Keeper{}) { tools[tool.Name] = true } for name := range ttl { diff --git a/modules/mesh-conditions/cmd/mesh-conditions/tools.go b/modules/mesh-conditions/cmd/mesh-conditions/tools.go index d62b6e11..a1324aad 100644 --- a/modules/mesh-conditions/cmd/mesh-conditions/tools.go +++ b/modules/mesh-conditions/cmd/mesh-conditions/tools.go @@ -17,14 +17,13 @@ import ( ) // Verbs are the seat's verbs, as the manifest claims them. -var Verbs = []string{"conditions", "silence", "compare", "take-over", "hand-back", "resync"} +var Verbs = []string{"conditions", "silence", "resync"} // The keeper's own read tools. const ( - HistoryTool = "conditions_history" - HeldTool = "conditions_held" - TakeoverTool = "conditions_takeover" - InputsTool = "conditions_inputs" + HistoryTool = "conditions_history" + HeldTool = "conditions_held" + InputsTool = "conditions_inputs" ) // briefEvidence is how many observations a condition carries in a list: its newest (the controller's, issue 314). @@ -50,7 +49,7 @@ func str(description string) map[string]any { } // Tools are what the runtime serves. -func Tools(k *Keeper, a Asks) []stdio.Tool { +func Tools(k *Keeper) []stdio.Tool { return []stdio.Tool{ {Name: Seat + ".conditions", Description: "What is wrong with the mesh now: every open condition, urgent first, then " + "oldest, raised from the observers' findings and cleared when the finding goes, never by hand. Given a key, that " + @@ -70,19 +69,6 @@ func Tools(k *Keeper, a Asks) []stdio.Tool { } return k.Silence(strArg(args, "key"), d, stdio.CallerOf(args), strArg(args, "why")) }}, - {Name: Seat + ".compare", Description: "The keeper's open conditions against the controller's, each difference with " + - "its reason; what ends with the controller, what only the keeper raises and what is held listed apart.", - Run: func(map[string]any) (any, error) { return k.Compare() }}, - {Name: Seat + ".take-over", Description: "Refused: the keeper is the one source of conditions since the " + - "controller ended by its unassignment (issue 1126); kept as the seat's verb until the seat drops it.", - Input: obj(map[string]any{"why": str("why now")}, "why"), - Run: func(args map[string]any) (any, error) { - return k.TakeOver(a, stdio.CallerOf(args), strArg(args, "why")) - }}, - {Name: Seat + ".hand-back", Description: "Gone: the readers of conditions hear the keeper alone, so there is nothing " + - "to hand back; answered as gone and changes nothing (ADR 0564 clause 9).", - Input: obj(map[string]any{"why": str("why")}, "why"), - Run: func(args map[string]any) (any, error) { return k.HandBack(stdio.CallerOf(args), strArg(args, "why")) }}, {Name: Seat + ".resync", Description: "Read every finding again and raise and clear what differs.", Run: func(map[string]any) (any, error) { n, err := k.Resync() @@ -99,16 +85,8 @@ func Tools(k *Keeper, a Asks) []stdio.Tool { bounds, err := k.Store.Keys(BoundsState) return map[string]any{"held": k.Held(), "bounds": bounds}, err }}, - {Name: TakeoverTool, Description: "The take-over key: which source the router, the grants part and mesh-watcher hear.", - Run: func(map[string]any) (any, error) { - raw, _, found, err := k.Store.Get(TakeoverState, health.TakeoverKey) - if err != nil { - return nil, err - } - return health.DecodeTakeover(raw, found) - }}, {Name: InputsTool, Description: "What the keeper reads, as it stands: each observer's findings counted, an observer it " + - "cannot read and why, the healers' attempts, the controller's conditions and whether its lease is handed away.", + "cannot read and why, the healers' attempts, the wants and the haves.", Run: func(map[string]any) (any, error) { return k.Inputs(), nil }}, } } @@ -212,7 +190,6 @@ func (k *Keeper) Inputs() map[string]any { findings[o] = len(fs) } return map[string]any{"findings": findings, "blind": k.blind, "attempts": len(k.attempts), - "controller-conditions": len(k.ctrl), "follow": k.takeover.Follow, "wants": len(k.wants), "haves": len(k.haves)} } diff --git a/modules/mesh-conditions/cmd/mesh-conditions/whole_integration_test.go b/modules/mesh-conditions/cmd/mesh-conditions/whole_integration_test.go index b788bafb..637685bd 100644 --- a/modules/mesh-conditions/cmd/mesh-conditions/whole_integration_test.go +++ b/modules/mesh-conditions/cmd/mesh-conditions/whole_integration_test.go @@ -266,7 +266,7 @@ func TestTheKeeperWholeRaisesAndClearsOnTheBus(t *testing.T) { for _, o := range Observers { buckets = append(buckets, o+"_findings") } - for _, s := range []string{ConditionsState, HistoryState, TakeoverState, HeldState, BoundsState, AskedState} { + for _, s := range []string{ConditionsState, HistoryState, HeldState, BoundsState} { buckets = append(buckets, Module+"_"+s) } for _, b := range buckets { diff --git a/modules/mesh-conditions/module.json b/modules/mesh-conditions/module.json index 7a02f96e..72000202 100644 --- a/modules/mesh-conditions/module.json +++ b/modules/mesh-conditions/module.json @@ -9,9 +9,6 @@ "serves": [ "conditions", "silence", - "compare", - "take-over", - "hand-back", "resync" ] } @@ -38,13 +35,11 @@ "name": "history", "ttl-seconds": 7776000 }, - "takeover", "held", { "name": "bounds", "key-ttl": true - }, - "asked" + } ], "own-secrets": { "broker": "${dir:state}/broker" diff --git a/modules/mesh-config/README.md b/modules/mesh-config/README.md index fb841a1e..e5fffe1a 100644 --- a/modules/mesh-config/README.md +++ b/modules/mesh-config/README.md @@ -66,14 +66,9 @@ Each directory or archive this module declares says how it treats a file it did - `mesh-config_declared`: each placed module's declarations at the trunk, with the manifest and commit they come from, written only when they differ. -- `mesh-config_takeover`: the key `switch` names the source that holds: none (declarations from the modules' own - manifests, the list from the indexer's `predefined`, pull requests' config from the controller's `pull-declares`), - or `indexer` after the operator's take-over (pull requests' config from the indexer's `config-candidates`; the - controller's `pull-declares` refused). A switch still naming `controller` (the old hand-back) is read as `indexer` and - said gone. Each take-over ask is kept beside it. -- `config_compare` lists every difference between each open pull request's candidate at the indexer and what the - controller told by `pull-declares`; the controller's mirrors are no longer read (ADR 0565 §4, issue 774). - `config_take_over` asks the operator's Approve, refused while any difference stands and switched on the Approve only - if none stands then. `config_hand_back` is gone: it answers so and changes nothing. `config_declared` and `config_resync` read and write again what it keeps. +- Its own sources hold always (novox/hq ADR 0590): declarations from the modules' own manifests, the list from the + indexer's `predefined`, pull requests' config from the indexer's `config-candidates`. The controller's + `pull-declares`, the take-over, compare and hand-back went with the controller. `config_declared` and + `config_resync` read and write again what it keeps. - A trusted value is set only through propose and the operator's Approve: no controller relay is trusted (issue 1126). - Its sets, clears and warrants are recorded in the hand-act log of the seat `mesh-hand-acts` alone. diff --git a/modules/mesh-config/cmd/mesh-config/main.go b/modules/mesh-config/cmd/mesh-config/main.go index 1311eb3d..93b271a8 100644 --- a/modules/mesh-config/cmd/mesh-config/main.go +++ b/modules/mesh-config/cmd/mesh-config/main.go @@ -124,7 +124,7 @@ func resolving(c *Config) { // A build's manifest is written before a want names the build, so a want, a trunk module or a placement moving is // every change of what a module declares where (issue 1953); the builds themselves are read, not watched. for _, read := range []string{PlacementRead, WantsRead, ModulesRead, IndexerPredefinedRead, - CandidatesRead, TakeoverState} { + CandidatesRead} { // Asked again until the runtime takes it, each give-up said (mesh-sdk's Follow). go stdio.State(read).Follow("", poke) } diff --git a/modules/mesh-config/cmd/mesh-config/manifest_test.go b/modules/mesh-config/cmd/mesh-config/manifest_test.go index 19e66dab..7c3b73a9 100644 --- a/modules/mesh-config/cmd/mesh-config/manifest_test.go +++ b/modules/mesh-config/cmd/mesh-config/manifest_test.go @@ -31,10 +31,10 @@ func TestEveryStateAndActHasItsTool(t *testing.T) { via := map[string]string{ ValuesState: "config list", ResolvedState: "config list", PredefinedState: "config list", ProposalsState: "config proposals", PullsState: "config list", KeptState: "config list", HistoryLog: "config history", - DeclaredState: DeclaredTool, TakeoverState: DeclaredTool, - PlacementRead: CompareTool, WantsRead: CompareTool, BuildsRead: CompareTool, ModulesRead: DeclaredTool, - ManifestsRead: DeclaredTool, IndexerPredefinedRead: CompareTool, CandidatesRead: CompareTool, - "a resolution": ResyncTool, "the indexer's candidates taken": ResyncTool, "a take-over decided": TakeOverTool, + DeclaredState: DeclaredTool, + PlacementRead: "config list", WantsRead: "config list", BuildsRead: "config list", ModulesRead: DeclaredTool, + ManifestsRead: DeclaredTool, IndexerPredefinedRead: "config list", CandidatesRead: ResyncTool, + "a resolution": ResyncTool, "the indexer's candidates taken": ResyncTool, } served := map[string]bool{} for _, tool := range Tools(&Config{}) { diff --git a/modules/mesh-config/cmd/mesh-config/proposals.go b/modules/mesh-config/cmd/mesh-config/proposals.go index 18619024..d6cb304d 100644 --- a/modules/mesh-config/cmd/mesh-config/proposals.go +++ b/modules/mesh-config/cmd/mesh-config/proposals.go @@ -758,7 +758,6 @@ func (c *Config) Decide() ([]string, error) { for _, r := range ready { said = append(said, c.decide(r.p, r.w)) } - said = append(said, c.decideTakeovers()...) c.recordWarrants(now) return said, nil } diff --git a/modules/mesh-config/cmd/mesh-config/pulls.go b/modules/mesh-config/cmd/mesh-config/pulls.go index 8c0647a6..22849bd8 100644 --- a/modules/mesh-config/cmd/mesh-config/pulls.go +++ b/modules/mesh-config/cmd/mesh-config/pulls.go @@ -136,59 +136,15 @@ type PullDeclaresAnswer struct { Unsaid []string `json:"unsaid,omitempty"` } -// PullDeclares keeps what a pull request's head declares, from the controller alone, and judges again the values -// held for it: a value whose key is declared the same (by digest) and still passes is kept at the new head; any -// other is dropped, recorded and raised, and must be proposed again (ADR 0502 clause 6). -func (c *Config) PullDeclares(call Call) (*PullDeclaresAnswer, error) { - if call.Caller != ControllerCaller { - return nil, fmt.Errorf("pull-declares is the controller's: it reads a pull request's manifests and tells "+ - "mesh-config what the head declares; %s is not the controller", call.Caller) - } - if sw, err := c.source(); err != nil { - return nil, err - } else if sw.Source == SourceIndexer { - return nil, errors.New("pull-declares is refused: since the take-over mesh-config reads a pull request's config " + - "declarations from the indexer's config-candidates (novox/hq ADR 0565 §4)") - } - if _, _, _, err := parsePull(call.Pull); err != nil { - return nil, err - } - head := strings.TrimSpace(call.Head) - if head == "" { - return nil, errors.New("name the head the declarations are read at") - } - raw, err := json.Marshal(call.Declares) - if err != nil { - return nil, fmt.Errorf("the declarations cannot be read: %v", err) - } - declares := map[string]map[string]Declaration{} - if call.Declares != nil { - if err := json.Unmarshal(raw, &declares); err != nil { - return nil, fmt.Errorf("declares is the modules' declarations, a key to a declaration for each module: %v", err) - } - } - for m, keys := range declares { - if !nodeOrModule.MatchString(m) { - return nil, fmt.Errorf("%q is not a module's name", m) - } - for k, d := range keys { - if !keyName.MatchString(k) { - return nil, fmt.Errorf("%q is not a config key", k) - } - if !d.HasDefault() { - d.Default = nil - } - keys[k] = d - } - } - pull := strings.TrimSpace(call.Pull) - c.mu.Lock() - defer c.mu.Unlock() - return c.declarePull(pull, head, declares) +// PullDeclares is gone: a pull request's config declarations are read from the indexer's config-candidates +// (takeCandidates), and the controller that told them is not placed (novox/hq ADR 0565 §4, ADR 0590). +func (c *Config) PullDeclares(Call) (*PullDeclaresAnswer, error) { + return nil, errors.New("pull-declares is gone: mesh-config reads a pull request's config declarations from the " + + "indexer's config-candidates (novox/hq ADR 0565 §4, ADR 0590)") } -// declarePull keeps what a pull request's head declares and judges again the values held for it; from the -// controller's pull-declares, or after the take-over from the indexer's config candidates. The caller holds the lock. +// declarePull keeps what a pull request's head declares and judges again the values held for it, from the indexer's +// config candidates. The caller holds the lock. func (c *Config) declarePull(pull, head string, declares map[string]map[string]Declaration) (*PullDeclaresAnswer, error) { if err := c.Store.Put(PullsState, pullKey(pull), PullDecl{Pull: pull, Head: head, Declares: declares, At: c.now()}); err != nil { diff --git a/modules/mesh-config/cmd/mesh-config/pulls_test.go b/modules/mesh-config/cmd/mesh-config/pulls_test.go index 820a13b4..341b0bfb 100644 --- a/modules/mesh-config/cmd/mesh-config/pulls_test.go +++ b/modules/mesh-config/cmd/mesh-config/pulls_test.go @@ -51,13 +51,21 @@ func (f *standForge) Pull(owner, repo string, n int) (bool, bool, string, error) return st[0], st[1], f.head, nil } +// tell keeps what a pull request's head declares as the indexer's candidate is taken (takeCandidates). func tell(t *testing.T, c *Config, pull, head string, declares map[string]any) *PullDeclaresAnswer { t.Helper() - a, err := c.Do(Call{Action: "pull-declares", Pull: pull, Head: head, Declares: declares, Caller: ControllerCaller}) - if err != nil { - t.Fatalf("pull-declares: %v", err) + raw, _ := json.Marshal(declares) + d := map[string]map[string]Declaration{} + if err := json.Unmarshal(raw, &d); err != nil { + t.Fatal(err) } - return a.(*PullDeclaresAnswer) + c.mu.Lock() + defer c.mu.Unlock() + a, err := c.declarePull(pull, head, d) + if err != nil { + t.Fatalf("declare: %v", err) + } + return a } // holdIt proposes media-gid=2000 for n8n on ace held for the pull request and has the operator approve it. @@ -75,21 +83,15 @@ func holdIt(t *testing.T, c *Config, m *memStore, r *standIn) string { func keptCount(m *memStore) int { k, _ := m.Keys(KeptState); return len(k) } -// 1826/K1: mesh-config takes what a pull request declares from the controller alone, and keeps it per pull request. -func TestAPullRequestsDeclarationsAreTakenFromTheControllerAlone(t *testing.T) { +// 1826/K1: mesh-config keeps what a pull request declares per pull request; pull-declares is gone (ADR 0590). +func TestAPullRequestsDeclarationsAreKeptPerPullRequest(t *testing.T) { c, m, _, _ := aHoldingMesh(t) - for _, caller := range []string{"g14.node-tools", Unproven, ""} { + for _, caller := range []string{"g14.node-tools", Unproven, "", ControllerCaller} { if _, err := c.Do(Call{Action: "pull-declares", Pull: thePull, Head: "abc1234", Declares: n8nPullDeclares(), - Caller: caller}); err == nil || !strings.Contains(err.Error(), "controller") { + Caller: caller}); err == nil || !strings.Contains(err.Error(), "config-candidates") { t.Errorf("pull-declares from %q: %v", caller, err) } } - for _, bad := range []Call{{Pull: "mesh-catalog#434", Head: "a"}, {Pull: thePull}, {Pull: "a/b#0", Head: "a"}} { - bad.Action, bad.Caller = "pull-declares", ControllerCaller - if _, err := c.Do(bad); err == nil { - t.Errorf("pull-declares %+v was taken", bad) - } - } a := tell(t, c, thePull, "abc1234def", n8nPullDeclares()) if len(a.Modules) != 1 || a.Modules[0] != "n8n" { t.Errorf("answered %+v", a) diff --git a/modules/mesh-config/cmd/mesh-config/sources.go b/modules/mesh-config/cmd/mesh-config/sources.go index 39ee2d70..e8286bef 100644 --- a/modules/mesh-config/cmd/mesh-config/sources.go +++ b/modules/mesh-config/cmd/mesh-config/sources.go @@ -365,10 +365,6 @@ func (c *Config) Reconcile() (Resolution, error) { c.mu.Lock() defer c.mu.Unlock() var r Resolution - if sw, serr := c.source(); serr == nil && sw.Gone != "" { - r.SourceGone = sw.Gone - c.logf("[mesh-config] %s", sw.Gone) - } published, err := c.publish() if err != nil { c.logf("[mesh-config] the list of predefined keys is not published: %v", err) diff --git a/modules/mesh-config/cmd/mesh-config/switch_tools.go b/modules/mesh-config/cmd/mesh-config/switch_tools.go index 6ae3b66b..51475b1f 100644 --- a/modules/mesh-config/cmd/mesh-config/switch_tools.go +++ b/modules/mesh-config/cmd/mesh-config/switch_tools.go @@ -1,7 +1,7 @@ package main -// The tools of the switch (novox/hq ADR 0565 §4, §15): compare, take-over and hand-back, served as the seat's verbs -// and as the module's own tools beside them, the read of `declared` and resync. +// The module's own tools beside the seat's verb (novox/hq ADR 0565 §15): the read of `declared` and resync. compare, +// take-over and hand-back went with the controller (ADR 0590). import ( "strings" @@ -11,56 +11,17 @@ import ( // The switch's tools. const ( - CompareTool = "config_compare" - TakeOverTool = "config_take_over" - HandBackTool = "config_hand_back" DeclaredTool = "config_declared" ResyncTool = "config_resync" ) // SeatVerbs are the seat's verbs, as the manifest claims them. -var SeatVerbs = []string{Verb, CompareVerb, TakeOverVerb, HandBackVerb} +var SeatVerbs = []string{Verb} -func switchTools(c *Config) []stdio.Tool { - own := ownSwitchTools(c) - byName := map[string]stdio.Tool{} - for _, t := range own { - byName[t.Name] = t - } - for verb, tool := range map[string]string{CompareVerb: CompareTool, TakeOverVerb: TakeOverTool, HandBackVerb: HandBackTool} { - t := byName[tool] - t.Name = Seat + "." + verb - own = append(own, t) - } - return own -} +func switchTools(c *Config) []stdio.Tool { return ownSwitchTools(c) } func ownSwitchTools(c *Config) []stdio.Tool { - why := map[string]any{"type": "object", "properties": map[string]any{ - "why": map[string]any{"type": "string", "description": "why, in a sentence"}}, "required": []string{"why"}} - whyOf := func(a map[string]any) string { s, _ := a["why"].(string); return strings.TrimSpace(s) } return []stdio.Tool{ - {Name: CompareTool, - Description: "Every difference between the indexer's records and the controller's pull-declares (novox/hq ADR 0565 §4): " + - "each open pull request's config (the indexer's candidate against the controller's pull-declares), each " + - "with its reason. The controller's mirrors are no longer read, so nothing else is compared. The " + - "take-over is refused while any stands.", - Run: func(map[string]any) (any, error) { return c.Compare() }}, - {Name: TakeOverTool, - Description: "Ask the operator's Approve, under mesh-config's own name, to switch config to its own sources: " + - "the indexer's list of predefined keys and pull request config, refusing the controller's pull-declares. " + - "Refused while compare shows a difference; on Approve switched if nothing differs then.", - Input: why, - Run: func(a map[string]any) (any, error) { - return c.TakeOver(Call{Why: whyOf(a), Caller: stdio.CallerOf(a)}) - }}, - {Name: HandBackTool, - Description: "Gone: the controller's records end with its lease and config no longer reads them (novox/hq ADR 0565 §4, " + - "issue 774); answers that, and changes nothing.", - Input: why, - Run: func(a map[string]any) (any, error) { - return c.HandBack(Call{Why: whyOf(a), Caller: stdio.CallerOf(a)}) - }}, {Name: DeclaredTool, Description: "Each placed module's declarations as config keeps them (its state declared): the manifest and " + "commit they come from and how many keys, or one module whole; and which source holds now.", @@ -72,7 +33,7 @@ func ownSwitchTools(c *Config) []stdio.Tool { }}, {Name: ResyncTool, Description: "Read every source again and write what differs: the published list of predefined keys, the " + - "resolved values, declared, and after the take-over the indexer's config candidates.", + "resolved values, declared, and the indexer's config candidates.", Run: func(map[string]any) (any, error) { return c.Resync() }}, } } diff --git a/modules/mesh-config/cmd/mesh-config/takeover.go b/modules/mesh-config/cmd/mesh-config/takeover.go index eefae9ad..26ca344b 100644 --- a/modules/mesh-config/cmd/mesh-config/takeover.go +++ b/modules/mesh-config/cmd/mesh-config/takeover.go @@ -1,95 +1,44 @@ package main -// Config's records out of the controller (novox/hq ADR 0565 §3, §4, §13, §15, and the progressive insight of -// 2026-10-29): mesh-config keeps each placed module's declarations in its own state `declared`; `compare` lists every -// difference between each open pull request's candidate at the indexer and what the controller told by pull-declares; -// `take-over` asks the operator's Approve under mesh-config's own name and, refused while anything differs, writes the -// key `takeover.switch` naming the indexer. +// Config's records out of the controller (novox/hq ADR 0565 §3, §15, and the progressive insight of 2026-10-29): +// mesh-config keeps each placed module's declarations in its own state `declared`, reads the list of predefined keys +// from the indexer's `predefined` and a pull request's config from the indexer's `config-candidates`. // -// **The controller's records are no longer read** (issue 774, ADR 0571 §13): its mirrors `declared-config` and -// `predefined` are gone from the reads, so declarations come from each placed module's own manifests and the list of -// predefined keys from the indexer's `predefined`, whatever the switch says. `hand-back` is answered as gone (ADR 0565 -// §4: it ends with the lease), and a switch that still names the controller is read as the indexer, said so. -// -// **Which source holds** is the key `switch` alone, read by mesh-config alone: -// - none written: a pull request's declarations from the controller's pull-declares; -// - `indexer` (taken over): a pull request's from the indexer's `config-candidates`; pull-declares refused; -// - `controller` (the old hand-back): gone, read as `indexer`. +// **Its own sources hold always** (ADR 0590, issue 1126): the controller ended by its unassignment, so its pull-declares, +// the take-over that switched away from them, compare and hand-back are gone, and the switch is the indexer's whatever +// a key once said. import ( "bytes" "encoding/json" "errors" "fmt" - "sort" - "strings" - "time" - "git.novox.be/novox/mesh-sdk/go/asks" "git.novox.be/novox/mesh-sdk/go/delivery" ) -// The states and reads of the switch. +// The states and reads. const ( // DeclaredState is each placed module's declarations at the trunk, one key per module (ADR 0565 §3). DeclaredState = "declared" - // TakeoverState holds the key SwitchKey and each take-over ask, `ask.`. - TakeoverState = "takeover" - SwitchKey = "switch" // IndexerPredefinedRead and CandidatesRead are the indexer's states (ADR 0565 §2). IndexerPredefinedRead = delivery.IndexerModule + "." + delivery.IndexerPredefinedState CandidatesRead = delivery.IndexerModule + "." + delivery.IndexerConfigCandidatesState ) -// The sources the switch names. -const ( - SourceInterim = "interim" - SourceIndexer = "indexer" - // SourceController is a switch the old hand-back wrote; the controller's records are gone, so it is read as the indexer. - SourceController = "controller" -) +// SourceIndexer is the one source of a pull request's config and of the predefined list: the indexer's. +const SourceIndexer = "indexer" -// TakeOverVerb and HandBackVerb are the seat's verbs. -const ( - TakeOverVerb = "take-over" - HandBackVerb = "hand-back" - CompareVerb = "compare" -) - -// Switch is the key `takeover.switch`: the source that holds, who chose it, why and on which ask. +// Switch is which source holds: the indexer's, always, since the controller ended (ADR 0590). type Switch struct { - Source string `json:"source"` - At time.Time `json:"at"` - By string `json:"by"` - Why string `json:"why"` - Ask string `json:"ask,omitempty"` - // Gone says the stored switch named the controller, whose records config no longer reads (ADR 0565 §4); the switch - // returned is then the indexer's. Never stored. - Gone string `json:"-"` + Source string `json:"source"` + Why string `json:"why"` } -// goneSource is what is said of a switch that still names the controller. -const goneSource = "the switch names the controller, which is gone: config no longer reads its records (novox/hq ADR 0565 §4, " + - "issue 774) and reads the indexer's" - -// source is the source that holds now. +// source is the source that holds: the indexer's. func (c *Config) source() (Switch, error) { - e, err := c.Store.Get(TakeoverState, SwitchKey) - if err != nil { - return Switch{}, fmt.Errorf("the take-over's switch cannot be read: %w", err) - } - if e == nil { - return Switch{Source: SourceInterim}, nil - } - var s Switch - if err := json.Unmarshal(e.Value, &s); err != nil || (s.Source != SourceIndexer && s.Source != SourceController) { - return Switch{}, fmt.Errorf("the take-over's switch is not one: %s", e.Value) - } - if s.Source == SourceController { - s.Source, s.Gone = SourceIndexer, goneSource - } - return s, nil + return Switch{Source: SourceIndexer, Why: "config reads its own sources since the controller ended by its unassignment (novox/hq ADR 0590)"}, nil } // ---- declared -------------------------------------------------------------------------------------------- @@ -254,368 +203,6 @@ func declaresDigest(d map[string]map[string]Declaration) string { return string(raw) } -// ---- compare ---------------------------------------------------------------------------------------------- - -// Difference is one thing the indexer's records and the controller's pull-declares say differently, and why. -type Difference struct { - What string `json:"what"` - Module string `json:"module,omitempty"` - Node string `json:"node,omitempty"` - Key string `json:"key,omitempty"` - Pull string `json:"pull,omitempty"` - Own json.RawMessage `json:"own,omitempty"` - Why string `json:"why"` -} - -// The kinds of difference. -const ( - DiffPull = "pull" -) - -// Comparison is what compare answers. -type Comparison struct { - Source string `json:"source"` - Differences []Difference `json:"differences"` - Count int `json:"count"` - Said string `json:"said"` - // Gone is said when the stored switch still names the controller. - Gone string `json:"gone,omitempty"` -} - -// Compare lists every difference (ADR 0565 §4 and its insight). The caller holds no lock. -func (c *Config) Compare() (*Comparison, error) { - c.mu.Lock() - defer c.mu.Unlock() - return c.compare() -} - -// compare is Compare; the caller holds the lock. The controller's mirrors are no longer read (issue 774, ADR 0565 §4), -// so the declarations, the list and the resolved values have no second side: what remains to compare is each open pull -// request's candidate at the indexer against what the controller told by pull-declares. -func (c *Config) compare() (*Comparison, error) { - sw, err := c.source() - if err != nil { - return nil, err - } - var own Sources - if err := c.readPulls(&own); err != nil { - return nil, fmt.Errorf("mesh-config's own pull request records cannot be read: %w", err) - } - var diffs []Difference - - // Each open pull request: the indexer's candidate against what the controller told by pull-declares. - cands, cerr := c.candidates() - if cerr != nil { - diffs = append(diffs, Difference{What: DiffPull, Why: "the indexer's config candidates cannot be read: " + cerr.Error()}) - } - settled := map[string]string{} // a pull request the forge says is no longer open → how it ended - for _, k := range unionKeys(cands, own.Pulls) { - a, aok := cands[k] - b, bok := own.Pulls[k] - isSettled, forgeNote := c.pullSettled(firstNonEmpty(a.Pull, b.Pull), settled) - if isSettled { - continue // a merged or closed pull request is no candidate to agree on (the operator, 2026-11-01) - } - before := len(diffs) - switch { - case !aok: - diffs = append(diffs, Difference{What: DiffPull, Pull: b.Pull, Why: "the controller told its declarations and the indexer holds no candidate"}) - case !bok: - diffs = append(diffs, Difference{What: DiffPull, Pull: a.Pull, Why: "the indexer holds a candidate and the controller told nothing"}) - case a.Head != b.Head: - diffs = append(diffs, Difference{What: DiffPull, Pull: a.Pull, Why: fmt.Sprintf("the indexer's candidate is at %s, "+ - "the controller told %s", short(a.Head), short(b.Head))}) - case declaresDigest(a.Declares) != declaresDigest(b.Declares): - diffs = append(diffs, Difference{What: DiffPull, Pull: a.Pull, Why: "the two declare its config otherwise"}) - } - if forgeNote != "" && len(diffs) > before { - diffs[len(diffs)-1].Why += "; " + forgeNote // the row stays because the forge was not heard: say so in the row - } - } - - sort.SliceStable(diffs, func(i, j int) bool { - a, b := diffs[i], diffs[j] - return a.What+a.Module+a.Node+a.Key+a.Pull < b.What+b.Module+b.Node+b.Key+b.Pull - }) - if diffs == nil { - diffs = []Difference{} - } - cmp := &Comparison{Source: sw.Source, Differences: diffs, Count: len(diffs), Gone: sw.Gone} - if len(diffs) == 0 { - cmp.Said = "nothing differs: the take-over may be asked (the controller's mirrors are no longer read, so only the pull requests are compared)" - } else { - cmp.Said = fmt.Sprintf("%d difference(s): the take-over is refused while any stands", len(diffs)) - } - return cmp, nil -} - -func firstNonEmpty(a, b string) string { - if a != "" { - return a - } - return b -} - -// pullSettled is whether the forge says a pull request merged or closed. Without a forge, or when it does not say, the -// pull request counts as open, so its row stays (failure is loud). -func (c *Config) pullSettled(pull string, memo map[string]string) (bool, string) { - if c.Forge == nil || pull == "" { - return false, "" - } - if v, ok := memo[pull]; ok { - return v != "" && !strings.HasPrefix(v, forgeSilent), forgeNoteOf(v) - } - owner, repo, n, err := parsePull(pull) - if err != nil { - return false, "" - } - open, merged, _, err := c.Forge.Pull(owner, repo, n) - switch { - case err != nil: - c.logf("[mesh-config] compare: the forge does not say whether %s is open (%v); its row stays", pull, err) - memo[pull] = forgeSilent + err.Error() - case open: - memo[pull] = "" - case merged: - memo[pull] = "merged" - default: - memo[pull] = "closed" - } - v := memo[pull] - return v != "" && !strings.HasPrefix(v, forgeSilent), forgeNoteOf(v) -} - -// forgeSilent marks a memo entry for a pull request the forge could not be asked about; the rest of the entry is why. -const forgeSilent = "forge-silent: " - -// forgeNoteOf is the words a row carries when the forge was not heard on its pull request, or "". -func forgeNoteOf(v string) string { - if why, ok := strings.CutPrefix(v, forgeSilent); ok { - return "the forge does not say whether the pull request is open (" + why + ")" - } - return "" -} - -func unionKeys[A, B any](a map[string]A, b map[string]B) []string { - seen := map[string]bool{} - for k := range a { - seen[k] = true - } - for k := range b { - seen[k] = true - } - return sortedKeys(seen) -} - -// ---- take-over and hand-back --------------------------------------------------------------------------------- - -// TakeoverAsk is one take-over asked of the operator, `takeover.ask.`: what the ask is composed from again, and -// where it stands. -type TakeoverAsk struct { - ID string `json:"id"` - From string `json:"from"` - Why string `json:"why"` - At time.Time `json:"at"` - State string `json:"state"` - Outcome string `json:"outcome,omitempty"` - Decided time.Time `json:"decided,omitzero"` - Answered string `json:"answered,omitempty"` - Recorded bool `json:"recorded,omitempty"` -} - -func askKey(id string) string { return "ask." + id } - -// act is what an answer to the take-over binds. -func (t TakeoverAsk) act(option string) asks.Act { - return asks.Act{"verb": Seat + "." + TakeOverVerb, "option": option, "ask": t.ID, "from": t.From, - "at": t.At.UTC().Format(time.RFC3339Nano)} -} - -// ask is the take-over's ask, composed from it alone. -func (t TakeoverAsk) ask() asks.Ask { - why, _ := sayable("Why: " + t.Why) - a := asks.Ask{ID: t.ID, Headline: "Switch config to its own sources", - Explanation: strings.Join([]string{ - "mesh-config would read each pull request's config from the delivery's " + - "indexer, no longer from the controller. Nothing any node resolves changes: compare showed no difference.", - point(why)}, "\n"), - Who: asks.Operator, Expires: t.At.Add(ProposalLasts), - OnExpiry: "nothing changes; a pull request's config keeps coming from the controller's pull-declares", About: "config.takeover", - Details: "Read what was compared with mesh-config's compare verb, through the mesh MCP server. hand-back is gone."} - for _, o := range []struct{ id, label, does string }{ - {OptionApprove, "Approve", "config switches to its own sources at once, if nothing differs then"}, - {OptionDecline, "Decline", "nothing changes"}, - } { - binds, _ := asks.ActDigest(t.act(o.id)) - a.Options = append(a.Options, asks.Option{ID: o.id, Label: o.label, Does: o.does, Level: asks.Approve, Binds: binds}) - } - return a -} - -// TakeoverAnswer is what take-over and hand-back answer. -type TakeoverAnswer struct { - Said string `json:"said"` - Ask string `json:"ask,omitempty"` - Source string `json:"source"` -} - -// TakeOver asks the operator's Approve to switch to the own sources, refused while compare shows a difference. -func (c *Config) TakeOver(call Call) (*TakeoverAnswer, error) { - if strings.TrimSpace(call.Why) == "" { - return nil, errors.New("say why the take-over is asked now: it is shown to the operator") - } - if call.Caller == "" || call.Caller == Unproven { - return nil, errors.New("the call names no proven caller") - } - if c.Asks == nil { - return nil, errors.New("the operator cannot be asked from here, so nothing is switched") - } - c.mu.Lock() - defer c.mu.Unlock() - sw, err := c.source() - if err != nil { - return nil, err - } - if sw.Source == SourceIndexer { - return &TakeoverAnswer{Source: sw.Source, Said: "config reads its own sources already, since " + - sw.At.UTC().Format(time.RFC3339) + " (" + sw.By + ")"}, nil - } - cmp, err := c.compare() - if err != nil { - return nil, err - } - if cmp.Count > 0 { - first := cmp.Differences[0] - return nil, fmt.Errorf("the take-over is refused: compare shows %d difference(s), the first %s %s%s%s%s: %s. "+ - "Nothing was asked", cmp.Count, first.What, first.Module, first.Node, first.Key, first.Pull, first.Why) - } - now := c.now() - var open []string - _ = c.each(TakeoverState, func(key string, raw []byte) error { - var t TakeoverAsk - if strings.HasPrefix(key, "ask.") && json.Unmarshal(raw, &t) == nil && t.State == ProposalWaiting && - now.Before(t.At.Add(ProposalLasts)) { - open = append(open, t.ID) - } - return nil - }) - if len(open) > 0 { - return nil, fmt.Errorf("the take-over is asked already, as %s, and waits for the operator's answer", open[0]) - } - t := TakeoverAsk{ID: "takeover-" + newProposalID(), From: call.Caller, Why: strings.TrimSpace(call.Why), At: now.UTC(), - State: ProposalWaiting} - ask := t.ask() - if err := ask.Check(now); err != nil { - return nil, err - } - if err := c.Store.Put(TakeoverState, askKey(t.ID), t); err != nil { - return nil, fmt.Errorf("the take-over could not be kept, so the operator was not asked: %w", err) - } - if err := c.Asks.Publish(asks.AskSubject(Module), ask, ask.ID); err != nil { - t.State, t.Outcome = ProposalUnasked, "the ask could not be published: "+err.Error() - _ = c.Store.Put(TakeoverState, askKey(t.ID), t) - return nil, fmt.Errorf("the operator could not be asked: %w", err) - } - return &TakeoverAnswer{Ask: t.ID, Source: sw.Source, Said: fmt.Sprintf("the operator is asked %q under "+ - "mesh-config's own name; nothing changes until they approve, and on Approve config switches if nothing differs "+ - "then", ask.Headline)}, nil -} - -// decideTakeovers acts on each take-over ask the router has closed. The caller holds the lock. -func (c *Config) decideTakeovers() []string { - var said []string - var waiting []TakeoverAsk - _ = c.each(TakeoverState, func(key string, raw []byte) error { - var t TakeoverAsk - if strings.HasPrefix(key, "ask.") && json.Unmarshal(raw, &t) == nil && t.State == ProposalWaiting { - waiting = append(waiting, t) - } - return nil - }) - now := c.now() - for _, t := range waiting { - keep := func(state, outcome string) string { - t.State, t.Outcome, t.Decided = state, outcome, now - _ = c.Store.Put(TakeoverState, askKey(t.ID), t) - return "take-over " + t.ID + ": " + outcome - } - e, err := c.Asks.Record(t.ID) - if err != nil || e == nil { - if err == nil && now.After(t.At.Add(ProposalLasts+time.Hour)) { - said = append(said, keep(ProposalEnded, "the router keeps no record of its ask; nothing was switched")) - } - continue - } - var rec routerRecord - if json.Unmarshal(e.Value, &rec) != nil || rec.State == "open" || rec.Warrant == nil { - continue - } - w := *rec.Warrant - if w.Asker != Module || w.Ask != t.ID { - said = append(said, keep(ProposalRefused, "the router's record is for another ask; nothing was switched")) - continue - } - if w.Outcome != asks.OutcomeChosen { - said = append(said, keep(ProposalEnded, "the ask "+string(w.Outcome)+"; nothing was switched")) - continue - } - option, err := w.For(Module, t.ask()) - if err == nil { - err = option.Performs(t.act(option.ID)) - } - if err != nil { - said = append(said, keep(ProposalRefused, "the answer is refused: "+err.Error())) - continue - } - t.Answered = w.Says() - if option.ID != OptionApprove { - said = append(said, keep(ProposalDeclined, "declined by the operator; nothing was switched")) - continue - } - cmp, err := c.compare() - switch { - case err != nil: - said = append(said, keep(ProposalRefused, "approved, and compare could not be read, so nothing was switched: "+err.Error())) - continue - case cmp.Count > 0: - said = append(said, keep(ProposalRefused, fmt.Sprintf("approved, and refused: compare shows %d difference(s) "+ - "now; nothing was switched, ask again once it is empty", cmp.Count))) - continue - } - sw := Switch{Source: SourceIndexer, At: now.UTC(), By: approvedTakeover(w), Why: t.Why, Ask: t.ID} - if err := c.Store.Put(TakeoverState, SwitchKey, sw); err != nil { - said = append(said, fmt.Sprintf("take-over %s: approved, and the switch could not be written (%v); read again "+ - "on the next look", t.ID, err)) - continue - } - if err := c.Asks.Warranted(t.ID); err == nil { - t.Recorded = true - } - said = append(said, keep(ProposalApproved, "approved by the operator: config reads its own sources from now")) - } - return said -} - -func approvedTakeover(w asks.Warrant) string { - if w.By == nil { - return "the operator" - } - return fmt.Sprintf("the %s, as %s identity %s", w.By.Who, w.By.Kind, w.By.Identity) -} - -// HandBack is answered as gone: the controller's records end with its lease, and config no longer reads them (novox/hq -// ADR 0565 §4, issue 774). Nothing is written. -func (c *Config) HandBack(call Call) (*TakeoverAnswer, error) { - if strings.TrimSpace(call.Why) == "" { - return nil, errors.New("say why config hands back: it is kept with the switch") - } - if call.Caller == "" || call.Caller == Unproven { - return nil, errors.New("the call names no proven caller") - } - return nil, errors.New("hand-back is gone: the controller's records end with its lease and config no longer reads " + - "them, so there is nothing to hand back to (novox/hq ADR 0565 §4); nothing was changed") -} - // Declared is the read of `declared`: every module's key, or one whole. func (c *Config) Declared(module string) (any, error) { if module != "" { @@ -641,7 +228,7 @@ func (c *Config) Declared(module string) (any, error) { } // Resync reads every source again and writes what differs: the published list, the resolved values, `declared`, and -// after the take-over the indexer's config candidates (ADR 0565 §15). +// the indexer's config candidates (ADR 0565 §15). func (c *Config) Resync() (any, error) { r, err := c.Reconcile() if err != nil { diff --git a/modules/mesh-config/cmd/mesh-config/takeover_test.go b/modules/mesh-config/cmd/mesh-config/takeover_test.go index 91ed0105..f0c0bbad 100644 --- a/modules/mesh-config/cmd/mesh-config/takeover_test.go +++ b/modules/mesh-config/cmd/mesh-config/takeover_test.go @@ -2,11 +2,8 @@ package main import ( "encoding/json" - "errors" - "slices" "strings" "testing" - "time" "git.novox.be/novox/mesh-sdk/go/delivery" ) @@ -18,7 +15,7 @@ const ( oldDeclared = `{"module":"old","settings":{"workers":{"kind":"preference","default":4,"why":"two per core"}}}` ) -// anAgreeingMesh is aProposingMesh with the indexer's predefined list in place and no open pull request: compare is empty. +// anAgreeingMesh is aProposingMesh with the indexer's predefined list in place and no open pull request. func anAgreeingMesh(t *testing.T) (*Config, *memStore, *standIn) { t.Helper() c, m, r := aProposingMesh(t) @@ -47,80 +44,11 @@ func candidate(t *testing.T, m *memStore, pull, head, declares string) { m.mirror(t, CandidatesRead, key, `{"v":1,"pull":"`+pull+`","head":"`+head+`","declares":`+declares+`}`) } -func kinds(cmp *Comparison) []string { - var out []string - for _, d := range cmp.Differences { - k := d.What + ":" + d.Module + d.Pull + d.Key - if d.Node != "" { - k += "@" + d.Node - } - if !slices.Contains(out, k) { - out = append(out, k) - } - } - return out -} - -// compare lists every difference with its reason — a pull request's config at the indexer against what the controller -// told — and the take-over is refused while one stands (ADR 0565 §4 and its insight of 2026-10-29; issue 1953 L5-C4). -// The controller's mirrors are no longer read, so no declaration, list or value is compared (issue 774, S9). -func TestCompareListsEveryDifferenceAndTheTakeOverWaitsForNone(t *testing.T) { - cases := []struct { - name string - change func(t *testing.T, m *memStore) - want []string - }{ - {"all agree", func(*testing.T, *memStore) {}, nil}, - {"a pull request only the indexer holds", func(t *testing.T, m *memStore) { - candidate(t, m, "novox/mesh-catalog#9", "h1", `{"dunst":{"gap":{"kind":"","type":"number","default":2}}}`) - }, []string{"pull:novox/mesh-catalog#9"}}, - {"a candidate of a merged pull request is no difference", func(t *testing.T, m *memStore) { - candidate(t, m, "novox/mesh-catalog#11", "h1", `{"dunst":{"gap":{"kind":"","type":"number","default":2}}}`) - candidate(t, m, "novox/mesh-catalog#10", "h2", `{"dunst":{"gap":{"kind":"","type":"number","default":2}}}`) - }, []string{"pull:novox/mesh-catalog#10"}}, - {"a forge that cannot be asked keeps the row and says why in it", func(t *testing.T, m *memStore) { - candidate(t, m, "novox/mesh-catalog#12", "h1", `{"dunst":{"gap":{"kind":"","type":"number","default":2}}}`) - }, []string{"pull:novox/mesh-catalog#12"}}, - } - for _, tc := range cases { - c, m, r := anAgreeingMesh(t) - c.Forge = &standForge{state: map[string][2]bool{"novox/mesh-catalog#11": {false, true}}} - if strings.Contains(tc.name, "forge that cannot be asked") { - c.Forge = &standForge{err: errors.New("forge down")} - } - tc.change(t, m) - cmp, err := c.Compare() - if err != nil { - t.Fatalf("%s: %v", tc.name, err) - } - if got := kinds(cmp); !slices.Equal(got, tc.want) { - t.Errorf("%s: compare lists %v, want %v (%+v)", tc.name, got, tc.want, cmp.Differences) - } - for _, d := range cmp.Differences { - if d.Why == "" { - t.Errorf("%s: a difference without its reason: %+v", tc.name, d) - } - if strings.Contains(tc.name, "forge that cannot be asked") && !strings.Contains(d.Why, "forge down") { - t.Errorf("%s: the row does not say the forge was not heard: %q", tc.name, d.Why) - } - } - a, err := c.TakeOver(Call{Why: "compare is empty", Caller: "person.jochen"}) - if len(tc.want) > 0 { - if err == nil || !strings.Contains(err.Error(), "refused") || len(r.subjects) != 0 { - t.Errorf("%s: the take-over was asked while compare differs: %v", tc.name, err) - } - continue - } - if err != nil || a.Ask == "" || len(r.subjects) != 1 { - t.Errorf("%s: the take-over was not asked: %v", tc.name, err) - } - } -} - -// The operator's Approve switches config to the indexer: pull-declares is refused, the list and the pull requests' -// config come from the indexer; hand-back is gone, and a switch naming the controller is read as the indexer; declared is written once per manifest (ADR 0565 §3, §4, §13). -func TestTheTakeOverAndTheHandBack(t *testing.T) { - c, m, r := anAgreeingMesh(t) +// ADR 0590 (issue 1126): config reads its own sources always, with no take-over: declared is written once per +// manifest, pull-declares is refused naming the indexer's candidates, and the list and a pull request's config come from +// the indexer. compare, take-over and hand-back went with the controller. +func TestConfigReadsItsOwnSourcesAlways(t *testing.T) { + c, m, _ := anAgreeingMesh(t) steps := []struct { name string do func() (string, error) @@ -132,22 +60,7 @@ func TestTheTakeOverAndTheHandBack(t *testing.T) { again, _ := c.Reconcile() return strings.Join(first.Declared, ",") + "|" + strings.Join(again.Declared, ","), err }, "dunst|"}, - {"pull-declares taken before the take-over", func() (string, error) { - a, err := c.PullDeclares(Call{Caller: ControllerCaller, Pull: "novox/mesh-catalog#7", Head: "h0", - Declares: map[string]any{}}) - return answered(a), err - }, "keeps what"}, - {"the take-over is asked", func() (string, error) { - _ = m.Delete(PullsState, "novox.mesh-catalog.7") // compare holds the told pull request against no candidate - a, err := c.TakeOver(Call{Why: "the release is delivered", Caller: "person.jochen"}) - return answered(a), err - }, "is asked"}, - {"the operator approves: switched to the indexer", func() (string, error) { - r.choose(t, r.lastAsk(t), OptionApprove, nil) - out, err := c.Decide() - return strings.Join(out, "; "), err - }, "reads its own sources from now"}, - {"pull-declares refused after it, naming the indexer", func() (string, error) { + {"pull-declares refused, naming the indexer", func() (string, error) { _, err := c.PullDeclares(Call{Caller: ControllerCaller, Pull: "novox/mesh-catalog#8", Head: "h1", Declares: map[string]any{}}) return "", err @@ -164,15 +77,6 @@ func TestTheTakeOverAndTheHandBack(t *testing.T) { res, err := c.Reconcile() return strings.Join(res.Candidates, "; "), err }, "from the indexer"}, - {"hand-back is gone, naming the record (774/S9/AC1)", func() (string, error) { - _, err := c.HandBack(Call{Why: "a difference was found after the switch", Caller: "person.jochen"}) - return "", err - }, "ADR 0565 §4"}, - {"a switch naming the controller is read as gone, and the indexer holds (774/S9/AC2)", func() (string, error) { - m.mirror(t, TakeoverState, SwitchKey, `{"source":"controller","by":"person.jochen","why":"old hand-back"}`) - res, err := c.Reconcile() - return res.SourceGone + "|" + string(m.resolved(t, "g14", "dunst", "width").Value), err - }, "is gone: config no longer reads its records"}, } for _, s := range steps { got, err := s.do() @@ -184,44 +88,7 @@ func TestTheTakeOverAndTheHandBack(t *testing.T) { t.Errorf("%s: said %q, want %q", s.name, text, s.want) } } - if len(m.handActs) != 0 { - t.Errorf("a hand-back that is gone recorded an act: %v", m.handActs) - } - if e, _ := m.Get(DeclaredState, "dunst"); e == nil || !strings.Contains(string(e.Value), `"manifest":"`) || !strings.Contains(string(e.Value), `"commit":"c1"`) { - t.Errorf("declared holds no dunst with its manifest: %v", e) - } - // An Approve that finds compare differing at the answer switches nothing. - c2, m2, r2 := anAgreeingMesh(t) - if _, err := c2.TakeOver(Call{Why: "now", Caller: "person.jochen"}); err != nil { - t.Fatal(err) - } - candidate(t, m2, "novox/mesh-catalog#9", "h9", `{"dunst":{"gap":{"kind":"","type":"number","default":2}}}`) - r2.choose(t, r2.lastAsk(t), OptionApprove, nil) - out, _ := c2.Decide() - if sw, _ := c2.source(); sw.Source != SourceInterim || !strings.Contains(strings.Join(out, ""), "difference") { - t.Errorf("an Approve while compare differs switched: %s, %v", sw.Source, out) + if sw, _ := c.source(); sw.Source != SourceIndexer { + t.Errorf("the source is %q", sw.Source) } } - -// lastAsk is the newest ask the router took. -func (r *standIn) lastAsk(t *testing.T) string { - t.Helper() - r.mu.Lock() - defer r.mu.Unlock() - var newest string - var at time.Time - for id, a := range r.asks { - if newest == "" || a.Expires.After(at) { - newest, at = id, a.Expires - } - } - if newest == "" { - t.Fatal("the router took no ask") - } - return newest -} - -func answered(a any) string { - raw, _ := json.Marshal(a) - return string(raw) -} diff --git a/modules/mesh-config/module.json b/modules/mesh-config/module.json index f8ae2d62..d62def76 100644 --- a/modules/mesh-config/module.json +++ b/modules/mesh-config/module.json @@ -7,10 +7,7 @@ "name": "mesh-config", "scope": "mesh", "serves": [ - "config", - "compare", - "take-over", - "hand-back" + "config" ] } ], @@ -40,9 +37,6 @@ "mesh-delivery-indexer.config-candidates" ], "tools": [ - "config_compare", - "config_take_over", - "config_hand_back", "config_declared", "config_resync" ], @@ -53,8 +47,7 @@ "proposals", "pulls", "kept", - "declared", - "takeover" + "declared" ], "logs": [ "history" diff --git a/modules/mesh-hand-acts/cmd/mesh-hand-acts/acts.go b/modules/mesh-hand-acts/cmd/mesh-hand-acts/acts.go index 289e97fe..83306052 100644 --- a/modules/mesh-hand-acts/cmd/mesh-hand-acts/acts.go +++ b/modules/mesh-hand-acts/cmd/mesh-hand-acts/acts.go @@ -30,11 +30,10 @@ import ( // The seat, the log, the states and the event, as mesh-sdk's handacts names them. const ( - Module = handacts.Module - Seat = handacts.Seat - ActsLog = handacts.ActsLog - WarrantsState = handacts.WarrantsState - ControllerActs = handacts.ControllerActs + Module = handacts.Module + Seat = handacts.Seat + ActsLog = handacts.ActsLog + WarrantsState = handacts.WarrantsState // RouterRecords is the router's record of every ask, `.` (ADR 0259): the warrant is read there. RouterRecords = "messenger.asks" RecordedEvent = handacts.EventRecorded diff --git a/modules/mesh-hand-acts/cmd/mesh-hand-acts/acts_test.go b/modules/mesh-hand-acts/cmd/mesh-hand-acts/acts_test.go index e0add639..b95c2d56 100644 --- a/modules/mesh-hand-acts/cmd/mesh-hand-acts/acts_test.go +++ b/modules/mesh-hand-acts/cmd/mesh-hand-acts/acts_test.go @@ -248,8 +248,8 @@ func TestHandActsReadsItsOwnLogAlone(t *testing.T) { put(ActsLog, VerbWarrant, CauseOperatorAnswer, "the operator", time.Hour) put(ActsLog, VerbWarrant, CauseOperatorAnswer, "the operator", 2*time.Hour) put(ActsLog, VerbRecord, "config", "ace.mesh-config", 30*24*time.Hour) - put(ControllerActs, VerbRecord, "never-read", "ace.old", time.Hour) - store.keysErr[ControllerActs] = errors.New("the controller's bucket must not be read") + put(controllerActs, VerbRecord, "never-read", "ace.old", time.Hour) + store.keysErr[controllerActs] = errors.New("the controller's bucket must not be read") cases := []struct { name string days int @@ -291,12 +291,12 @@ func TestHandActsReadsItsOwnLogAlone(t *testing.T) { // read and write nothing of the controller's bucket nor of the log (774/S10; research 072 row 26). func TestTheCopyOfTheControllersHandActsIsOver(t *testing.T) { l, store, _, now := newLog() - store.keysErr[ControllerActs] = errors.New("the controller's bucket must not be read") + store.keysErr[controllerActs] = errors.New("the controller's bucket must not be read") st, err := l.Copy("test", true) if err != nil || st.Done || !strings.Contains(st.Said, "no longer read") { t.Errorf("without a marker: %+v, %v", st, err) } - _ = store.Put(CopiedState, CopyKey, CopyMarker{At: *now, By: "start", Source: ControllerActs, Found: 606, Copied: 606}) + _ = store.Put(CopiedState, CopyKey, CopyMarker{At: *now, By: "start", Source: controllerActs, Found: 606, Copied: 606}) st, err = l.Copy("test", false) if err != nil || !st.Done || st.Marker.Copied != 606 || !strings.Contains(st.Said, "no longer read") || len(store.logKeys(ActsLog)) != 0 { t.Errorf("with a marker: %+v, %v", st, err) @@ -305,3 +305,6 @@ func TestTheCopyOfTheControllersHandActsIsOver(t *testing.T) { t.Errorf("copied says %+v, %v", s, err) } } + +// controllerActs is the controller's hand-act log as it was named: a test reads it never. +const controllerActs = "mesh-controller.hand-acts"