gate_record_repair asks the owners' read verbs for the outcomes a record's node entries lack and applies them as the consumer would (milestone 47/G, gate_record_repair)

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ

(Joined from 46d3053c.)
Builds: design 47 G §159 (GATE13)

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
This commit is contained in:
2026-11-11 14:45:57 +01:00
parent 8484d349aa
commit 85023b3b52
5 changed files with 275 additions and 1 deletions
@@ -161,7 +161,7 @@ func TestItClaimsTheDeliverySeatAndFollowsWhatItHandles(t *testing.T) {
// bridge's (ADR 0437 §8). It uses operator-channel only to say its conditions (ADR 0437 §9, issue 1275): the teller asks
// (ADR 0437 §6).
want := []string{"seat:git.pull", "seat:git.statuses", "seat:git.open-pulls", "seat:git.contains", "seat:mesh-hand-acts.hand-act",
TrackerListVerb, tellerAnswersTool, viewReachTool}
TrackerListVerb, WantsEntryVerb, EngineCompositionVerb, tellerAnswersTool, viewReachTool}
if !reflect.DeepEqual(m.Uses, []string{"operator-channel"}) {
t.Fatalf("uses %v: operator-channel, for its conditions alone", m.Uses)
}
@@ -742,6 +742,51 @@ func TestTheRecordsToolsReadThemAndEndOneByHand(t *testing.T) {
t.Fatal("a record ended is ended again")
}
})
t.Run("gate_record_repair asks the owners for the outcomes an entry lacks, applies what they answer, and ends the record", func(t *testing.T) {
rs, r, said := setup(t)
owners := &ownersFake{wanted: "src2:f1", fp: "src2:f1", phase: "running"}
log := &handActsFake{}
if _, err := rs.Repair(r.Instance, "", "jochen@shanks", owners, log); err == nil {
t.Fatal("repaired with no reason")
}
log.refuse = errors.New("no responders")
if _, err := rs.Repair(r.Instance, "the events were lost", "jochen@shanks", owners, log); err == nil || len(owners.asked) != 0 {
t.Fatalf("repaired though the hand-act log refused it: %v, asked %v", err, owners.asked)
}
log.refuse = nil
// The owner says the module runs another fingerprint: the entry is left.
owners.fp = "src2:other"
got, err := rs.Repair(r.Instance, "the events were lost", "jochen@shanks", owners, log)
if err != nil || got["ended"] != false {
t.Fatalf("%v %v", got, err)
}
if back, _ := rs.Read(r.Instance); back.Outcomes[OutcomeKey("mesh-x", "src2:f1", "ace", StepRunning)].Outcome != "" {
t.Fatalf("an outcome the owner did not answer was applied: %+v", back.Outcomes)
}
// The owners answer: wanted and running are applied as the consumer would, the record ends, and the steps with no read
// verb are named.
owners.fp = "src2:f1"
got, err = rs.Repair(r.Instance, "the events were lost", "jochen@shanks", owners, log)
if err != nil || got["ended"] != true {
t.Fatalf("%v %v", got, err)
}
back, _ := rs.Read(r.Instance)
if back.Outcomes[OutcomeKey("mesh-x", "src2:f1", "ace", StepRunning)].Outcome != OutcomeRunning ||
back.Outcomes[OutcomeKey("mesh-x", "src2:f1", "ace", StepWanted)].Outcome != OutcomeWanted || back.Ended == nil {
t.Fatalf("outcomes %+v ended %+v", back.Outcomes, back.Ended)
}
if unread := got["not-readable-yet"].([]string); len(unread) != 4 || !strings.HasPrefix(unread[0], "built:") {
t.Fatalf("not readable yet: %v", unread)
}
if !slices.Contains(*said, meshdelivery.SubjectRecordEnded+" record-ended."+r.Instance) || len(log.took) != 2 {
t.Fatalf("said %v, hand acts %d", *said, len(log.took))
}
// A repair repeated changes nothing.
again, _ := rs.Repair(r.Instance, "again", "jochen@shanks", owners, log)
if lines := again["lines"].([]repairLine); len(lines) != 0 {
t.Fatalf("a second repair did something: %+v", lines)
}
})
t.Run("a removal that keeps the record's operations is an error", func(t *testing.T) {
was := ledgerCall
t.Cleanup(func() { ledgerCall = was })
@@ -877,3 +922,19 @@ func TestTheRecordsLedgerSaysFillingFullAndRoom(t *testing.T) {
}
})
}
// ownersFake answers the owners' reads: the fingerprint the assignment graph names, and the fingerprint and phase of the have.
type ownersFake struct {
wanted, fp, phase string
asked []string
}
func (o *ownersFake) Wanted(node, module string) (string, error) {
o.asked = append(o.asked, "wanted "+node+" "+module)
return o.wanted, nil
}
func (o *ownersFake) Running(node, module string) (string, string, error) {
o.asked = append(o.asked, "running "+node+" "+module)
return o.fp, o.phase, nil
}
@@ -0,0 +1,195 @@
package main
// gate_record_repair (design 47 *The delivery record*, its tools against the four lines): for one record, asks each owner for
// the outcome of each entry that lacks one, through that owner's read verb, and applies the answers as the consumer would. The
// reads that exist as seat verbs are the seat mesh-wants' `entry` (is the node's entry at the fingerprint: wanted) and the seat
// node-engine's `composition` of that node (does the module run at the fingerprint: running). The indexer's take-in read, the
// builder's build read and the checker's check read are no seat verb yet: an entry missing one of those steps is said as
// unreadable, naming the owner, and is left to its event.
import (
"encoding/json"
"errors"
"fmt"
"sort"
"strings"
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient"
)
// The verbs the repair reads through (the manifest invokes them).
const (
WantsEntryVerb = "seat:mesh-wants.entry"
EngineCompositionVerb = "seat:node-engine.composition"
)
// StepWanted is the step whose outcome OutcomeWanted says the assignment graph names the entry on its node.
const StepWanted = "wanted"
// ownerReads are the reads of the owners the repair asks.
type ownerReads interface {
// Wanted is the fingerprint the assignment graph names for the module on the node; empty when it names none.
Wanted(node, module string) (fingerprint string, err error)
// Running is the fingerprint and phase of the module's have on the node, from the node-engine seat's verb composition.
Running(node, module string) (fingerprint, phase string, err error)
}
// seatOwners reads the owners through their seats.
type seatOwners struct{ ask asking }
func (s seatOwners) call(verb string, in map[string]any) (json.RawMessage, error) {
raw, err := s.ask(verb, in)
if err != nil {
return nil, err
}
answer, output, ok := answerOf(raw)
if !ok {
return nil, fmt.Errorf("%s refused: %s", verb, lastLine(output))
}
return answer, nil
}
func (s seatOwners) Wanted(node, module string) (string, error) {
answer, err := s.call(WantsEntryVerb, map[string]any{"node": node, "module": module})
if err != nil {
return "", err
}
var a struct {
Entry struct {
Fp string `json:"fp"`
} `json:"entry"`
}
if err := json.Unmarshal(answer, &a); err != nil {
return "", fmt.Errorf("the entry of %s on %s is not readable: %s", module, node, clip(string(answer), 200))
}
return a.Entry.Fp, nil
}
func (s seatOwners) Running(node, module string) (string, string, error) {
answer, err := s.call(EngineCompositionVerb+"@"+node, map[string]any{"module": module})
if err != nil {
return "", "", err
}
var a struct {
Have *struct {
Fp string `json:"fp"`
Phase string `json:"phase"`
} `json:"have"`
}
if err := json.Unmarshal(answer, &a); err != nil {
return "", "", fmt.Errorf("the composition of %s on %s is not readable: %s", module, node, clip(string(answer), 200))
}
if a.Have == nil {
return "", "", nil
}
return a.Have.Fp, a.Have.Phase, nil
}
// unreadable are the steps whose owners offer no read verb yet, with the owner and the read it lacks.
var unreadable = map[string]string{
OutcomeTakenIn: "the indexer has no read of what it took in",
OutcomeBuilt: "the builder has no read of a build by entry",
OutcomeChecked: "the checker has no read of a check by entry",
OutcomeComposed: "the assigner has no read of a composition by entry",
}
// repairLine is what the repair did for one entry and step.
type repairLine struct {
Entry string `json:"entry"`
Step string `json:"step"`
Did string `json:"did"`
Because string `json:"because,omitempty"`
}
// Repair asks the owners for each outcome the record's node entries lack, applies what they answer as the consumer would
// (an outcome with its why naming the read), and ends the record if every present node then runs its entries. Entries on
// no node (the catalog half's) are read for the steps no node-less owner offers: said unreadable.
func (rs *Records) Repair(instance, why, by string, owners ownerReads, acts handActs) (map[string]any, error) {
if strings.TrimSpace(why) == "" {
return nil, errors.New("say why the record is repaired by hand")
}
r, err := rs.Read(instance)
if err != nil {
return nil, err
}
if r == nil {
return nil, fmt.Errorf("no delivery record %s is kept", instance)
}
if owners == nil {
return nil, errors.New("the owners cannot be asked from here, so nothing was repaired")
}
if acts == nil {
return nil, errors.New("the hand-act log cannot be reached here, so nothing was repaired")
}
if _, err := acts.Record(meshhandactsclient.Request{What: "asked the owners for the outcomes the delivery record " + instance + " lacks",
Why: why, Cause: "gate_record_repair", Verb: "record-repair"}); err != nil {
return nil, fmt.Errorf("the hand act was not recorded through mesh-hand-acts, so nothing was repaired: %w", err)
}
keys := make([]string, 0, len(r.Entries))
for k := range r.Entries {
keys = append(keys, k)
}
sort.Strings(keys)
var lines []repairLine
apply := func(e RecordEntry, step, outcome, because string) {
o := RecordOutcome{Module: e.Module, Fingerprint: e.Fingerprint, Node: e.Node, Step: step, Outcome: outcome,
Why: because, At: rs.Now(), Event: "repair." + OutcomeKey(e.Module, e.Fingerprint, e.Node, step)}
applied, err := rs.Outcome(r, o)
switch {
case err != nil:
lines = append(lines, repairLine{Entry: e.Key, Step: step, Did: "failed", Because: err.Error()})
case applied:
lines = append(lines, repairLine{Entry: e.Key, Step: step, Did: "applied", Because: because})
}
}
for _, k := range keys {
e := r.Entries[k]
if e.Node == "" {
continue
}
for _, step := range []string{StepWanted, StepRunning} {
if _, has := r.Outcomes[OutcomeKey(e.Module, e.Fingerprint, e.Node, step)]; has {
continue
}
switch step {
case StepWanted:
fp, err := owners.Wanted(e.Node, e.Module)
switch {
case err != nil:
lines = append(lines, repairLine{Entry: k, Step: step, Did: "unread", Because: err.Error()})
case fp != e.Fingerprint:
lines = append(lines, repairLine{Entry: k, Step: step, Did: "left", Because: fmt.Sprintf("the assignment graph names %q on %s", fp, e.Node)})
default:
apply(e, step, OutcomeWanted, "read from the seat mesh-wants: the entry of "+e.Module+" on "+e.Node+" names this fingerprint")
}
case StepRunning:
fp, phase, err := owners.Running(e.Node, e.Module)
switch {
case err != nil:
lines = append(lines, repairLine{Entry: k, Step: step, Did: "unread", Because: err.Error()})
case fp != e.Fingerprint || phase != "running":
lines = append(lines, repairLine{Entry: k, Step: step, Did: "left", Because: fmt.Sprintf("its have on %s is %q at %q", e.Node, phase, fp)})
default:
apply(e, step, OutcomeRunning, "read from the seat node-engine's composition on "+e.Node+": its have runs this fingerprint")
}
}
}
}
steps := make([]string, 0, len(unreadable))
for s := range unreadable {
steps = append(steps, s)
}
sort.Strings(steps)
var unread []string
for _, s := range steps {
unread = append(unread, s+": "+unreadable[s])
}
ended := false
if r.Ended == nil {
if ended, err = rs.EndIfRun(r); err != nil {
return nil, err
}
}
rs.keep(r)
return map[string]any{"instance": instance, "by": by, "lines": lines, "ended": ended, "not-readable-yet": unread, "record": viewOf(r)}, nil
}
@@ -179,6 +179,21 @@ func recordTools(h *Holder, acts handActs) []stdio.Tool {
}
return map[string]any{"families": h.Consumer.Heard()}, nil
}},
{Name: "gate_record_repair",
Description: "Do by hand what the consumer does itself, for one delivery record: ask the owners for the outcome of each " +
"node entry that lacks one, through their read verbs (the seat mesh-wants' entry for the wanted step, the seat " +
"node-engine's composition of that node for the running step), and apply the answers as the consumer would; an " +
"entry whose owner says otherwise is left, a read that failed is said, and the record ends when every present node " +
"runs its entries. The steps whose owners offer no read verb yet are named. Recorded in the hand-act log first.",
Input: map[string]any{"type": "object", "properties": map[string]any{
"instance": str("the record's instance"), "why": str("why it is repaired by hand, in plain words")},
"required": []string{"instance", "why"}},
Run: func(a map[string]any) (any, error) {
if h.Records == nil {
return none()
}
return h.Records.Repair(strArg(a, "instance"), strArg(a, "why"), stdio.CallerOf(a), seatOwners{ask: stdio.Ask}, acts)
}},
{Name: "gate_record_end",
Description: "End a delivery record left open by hand (a node gone for good, an entry that never runs), with the " +
"reason; recorded in the hand-act log of the seat mesh-hand-acts first, and said as record-ended.",
+3
View File
@@ -298,6 +298,8 @@
"seat:git.contains",
"seat:mesh-hand-acts.hand-act",
"seat:issue-tracker.list",
"seat:mesh-wants.entry",
"seat:node-engine.composition",
"mesh-delivery-teller.teller_answers",
"mesh-delivery-view.view_reach"
],
@@ -336,6 +338,7 @@
"gate_consumer",
"gate_record",
"gate_record_end",
"gate_record_repair",
"gate_records"
],
"config": {