GATE A-39: every change of a record owes its pull requests a note line and the delivery view, and a comment when it ends failed or held

Builds: design 47 G A-39, A-35, §158, §161 to §167

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
2026-11-11 22:16:16 +01:00
parent 3eee45aa49
commit 14699713b9
5 changed files with 186 additions and 0 deletions
@@ -63,6 +63,7 @@ func main() {
h.init()
h.SaySeat = stdio.SeatPublish
h.Records = &Records{Ledger: sdkLedger{}, Now: h.Now, Retention: h.recordRetention, Say: stdio.SeatPublish, Logf: logf}
h.Records.Changed = h.recordChanged
h.Old = kvOld{}
h.Consumer = &StepConsumer{Records: h.Records, Families: stepFamilies, Now: h.Now, Logf: logf, Taken: h.TakenIn}
l := &listening{now: "not yet: starting"}
@@ -0,0 +1,86 @@
package main
import (
"encoding/json"
"fmt"
"sort"
"strconv"
"strings"
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
)
// What the gate owes the forge for a change of a delivery record (design 47 G A-39, A-35, §158, §161 to §167): every change of
// a record, a step's outcome applied or the record ended, says a line on the commit's note of each member pull request's head
// (`<at> record <key>: <what changed> (<why>)`) and replaces each member's delivery view with what `show` answers for the
// record; a comment is said only when the record ends failed or held, naming why. Each act goes through owe (owed.go): kept in
// the record's acts, said as the seat's event owed, and ended on owed.ended.
// RecordChange is one change of a record: what changed and why; Ended when it is the record's end.
type RecordChange struct {
What, Why string
Ended bool
}
// outcomeWords is what an outcome applied says changed.
func outcomeWords(o RecordOutcome) string {
where := o.Module
if o.Node != "" {
where += " on " + o.Node
}
return strings.TrimSpace(fmt.Sprintf("%s %s %s", where, o.Step, o.Outcome))
}
// recordChanged owes the forge what a change of the record asks, for each of its pull requests. An act that cannot be said is
// kept in the record all the same, said again at the gate's start (§165), and shows in the status as refused.
func (h *Holder) recordChanged(r *Record, c RecordChange) {
if h.Owed == nil {
return
}
now := h.Now().UTC()
line := fmt.Sprintf("%s record %s: %s (%s)", now.Format("2006-01-02T15:04:05Z"), r.Instance, c.What, c.Why)
view, err := json.MarshalIndent(map[string]any{"records": []recordView{viewOf(r)}}, "", " ")
if err != nil {
h.Refuse(fmt.Errorf("the record %s cannot be shown on its pull requests: %w", r.Instance, err))
return
}
var comment string
if c.Ended {
comment = endedFailedOrHeld(r)
}
for _, m := range membersOf(r) {
if m.Number <= 0 || m.Head == "" {
continue
}
pull := m.Repository + "#" + strconv.FormatInt(m.Number, 10)
acts := []seat.Owed{
{Act: seat.OwedActNote, Line: line, Commit: m.Head},
{Act: seat.OwedActView, Text: string(view)},
}
if comment != "" {
acts = append(acts, seat.Owed{Act: seat.OwedActComment, Text: comment})
}
for _, o := range acts {
o.Repository, o.Number, o.Head = m.Repository, m.Number, m.Head
o.ID = actID(o.Act+"."+r.Instance+"."+pull, now, o.Line+o.Text)
if err := h.owe(r, o); err != nil {
h.Refuse(fmt.Errorf("the %s owed on %s for the record %s is not said: %w", o.Act, pull, r.Instance, err))
}
}
}
}
// endedFailedOrHeld is the comment a record ended says when one of its outcomes is failed or held, naming why; "" otherwise.
func endedFailedOrHeld(r *Record) string {
var lines []string
for _, o := range r.Outcomes {
if o.Outcome == OutcomeFailed || o.Outcome == OutcomeHeld {
lines = append(lines, fmt.Sprintf("- %s: %s", outcomeWords(o), firstOf(o.Why, "no reason said")))
}
}
if len(lines) == 0 {
return ""
}
sort.Strings(lines)
return fmt.Sprintf("The delivery record %s ended (%s), with outcomes failed or held:\n%s", r.Instance, r.Ended.Why, strings.Join(lines, "\n"))
}
@@ -0,0 +1,90 @@
package main
import (
"strings"
"testing"
"time"
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
)
type sayerFake struct{ said []seat.Owed }
func (f *sayerFake) Say(o seat.Owed) error { f.said = append(f.said, o); return nil }
// A-39: every change of a record owes a note line and the delivery view for each member pull request, and a comment only
// when the record ends failed or held.
func TestARecordsChangesOweTheNoteTheViewAndWhenFailedOrHeldAComment(t *testing.T) {
at := time.Date(2026, 11, 11, 21, 0, 0, 0, time.UTC)
head := strings.Repeat("1", 40)
setup := func(t *testing.T) (*Holder, *Records, *Record, *sayerFake) {
rs := &Records{Ledger: newLedgerFake(t), Now: func() time.Time { return at }}
f := &sayerFake{}
h := &Holder{Now: func() time.Time { return at }, Owed: f, Records: rs}
rs.Changed = h.recordChanged
r, _, err := rs.Open("47/G", map[string]string{"novox/app@7": head}, false)
if err != nil {
t.Fatal(err)
}
if err := rs.AddEntry(r, RecordEntry{Module: "mesh-x", Fingerprint: "src2:f1", Node: "ace"}); err != nil {
t.Fatal(err)
}
return h, rs, r, f
}
count := func(f *sayerFake, act string) (n int) {
for _, o := range f.said {
if o.Act == act {
n++
}
}
return
}
t.Run("an outcome applied says the note and the view, no comment", func(t *testing.T) {
_, rs, r, f := setup(t)
if _, err := rs.Outcome(r, RecordOutcome{Module: "mesh-x", Fingerprint: "src2:f1", Node: "ace", Step: "build", Outcome: OutcomeBuilt, Why: "built it"}); err != nil {
t.Fatal(err)
}
if len(f.said) != 2 || count(f, seat.OwedActNote) != 1 || count(f, seat.OwedActView) != 1 {
t.Fatalf("said %+v", f.said)
}
for _, o := range f.said {
if o.Repository != "novox/app" || o.Number != 7 || o.Head != head {
t.Fatalf("act %+v", o)
}
if _, ok := r.Acts[o.ID]; !ok {
t.Fatalf("act %s not kept in the record", o.ID)
}
if o.Act == seat.OwedActNote && (o.Line != "2026-11-11T21:00:00Z record "+r.Instance+": mesh-x on ace build built (built it)" || o.Commit != head) {
t.Fatalf("note %+v", o)
}
if o.Act == seat.OwedActView && !strings.Contains(o.Text, r.Instance) {
t.Fatalf("view %q", o.Text)
}
}
// the same outcome again changes nothing and owes nothing
if _, err := rs.Outcome(r, RecordOutcome{Module: "mesh-x", Fingerprint: "src2:f1", Node: "ace", Step: "build", Outcome: OutcomeBuilt, Why: "built it"}); err != nil || len(f.said) != 2 {
t.Fatalf("said again: %v %d", err, len(f.said))
}
})
t.Run("a record ended failed says a comment naming why; ended healthy says none", func(t *testing.T) {
_, rs, r, f := setup(t)
if _, err := rs.Outcome(r, RecordOutcome{Module: "mesh-x", Fingerprint: "src2:f1", Node: "ace", Step: "build", Outcome: OutcomeFailed, Why: "tests red"}); err != nil {
t.Fatal(err)
}
if err := rs.End(r, "never runs", "operator"); err != nil {
t.Fatal(err)
}
if count(f, seat.OwedActComment) != 1 || count(f, seat.OwedActNote) != 2 || count(f, seat.OwedActView) != 2 {
t.Fatalf("said %+v", f.said)
}
for _, o := range f.said {
if o.Act == seat.OwedActComment && (!strings.Contains(o.Text, "tests red") || !strings.Contains(o.Text, "never runs")) {
t.Fatalf("comment %q", o.Text)
}
}
_, rs, r, f = setup(t)
if err := rs.End(r, "by hand", "operator"); err != nil || count(f, seat.OwedActComment) != 0 || count(f, seat.OwedActNote) != 1 {
t.Fatalf("%v said %+v", err, f.said)
}
})
}
@@ -361,6 +361,9 @@ type Records struct {
Say SeatSay
// Logf says what the records could not say; nothing is said when nil.
Logf func(format string, args ...any)
// Changed is told of every change of a record that owes the forge an act (A-39): a step's outcome applied, the record
// ended (owedacts.go); nil owes none.
Changed func(r *Record, c RecordChange)
// held are the records kept, by instance, read once from the ledger (Current) and kept as the gate writes them: the
// gate is their one writer.
@@ -665,6 +668,9 @@ func (rs *Records) Outcome(r *Record, o RecordOutcome) (applied bool, err error)
r.Outcomes = map[string]RecordOutcome{}
}
r.Outcomes[o.Key], r.Sequence = o, seq
if rs.Changed != nil {
rs.Changed(r, RecordChange{What: outcomeWords(o), Why: firstOf(o.Why, "the "+o.Step+" step's event")})
}
return true, nil
}
@@ -233,6 +233,9 @@ func (rs *Records) end(r *Record, why, by string) error {
}
r.Ended, r.Sequence = &e, seq
rs.keep(r)
if rs.Changed != nil {
rs.Changed(r, RecordChange{What: "ended", Why: why, Ended: true})
}
return nil
}