GATE A-39, A-29, A-37, A-30, A-40, §592: the gate's findings go through one held writer over its own findings state, approval-carried-open is written for each approval carried and cleared by the seat git's pull-merged and pull-closed
Builds: design 47 G A-39, A-29, A-37, A-30, A-17, A-40, §542, §592 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions"
|
||||
gitseat "git.novox.be/novox/mesh-delivery/contracts/go/seats/git"
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
"git.novox.be/novox/mesh-sdk/go/keys"
|
||||
"git.novox.be/novox/mesh-sdk/go/views"
|
||||
)
|
||||
|
||||
// The gate's own findings (design 47 G A-39, A-29, A-37, A-30, §592): they go through one held writer over the gate's own state
|
||||
// `findings`, whose three events the manifest declares. The first is `approval-carried-open`: for each `approval-carried` the
|
||||
// gate says, a carried Approve nobody uses is seen, and cleared when the seat `git` says that pull request merged or closed.
|
||||
|
||||
// Module is the gate's module name, the observer of its findings.
|
||||
const Module = "mesh-delivery-gate"
|
||||
|
||||
// The scope and the kind of the finding a carried approval nobody uses is (design 47 §592).
|
||||
const (
|
||||
FindingScope = "gate"
|
||||
ApprovalCarriedOpen = "approval-carried-open"
|
||||
)
|
||||
|
||||
// pullMergedEvent and pullClosedEvent are the seat git's events that end a pull request, as the gate's manifest consumes them.
|
||||
var (
|
||||
pullMergedEvent = declared(gitseat.EventPullMerged)
|
||||
pullClosedEvent = declared(gitseat.EventPullClosed)
|
||||
)
|
||||
|
||||
// FindingWriter is what the gate holds of the one held writer (views.NewStdioHeldWriter over its state `findings`).
|
||||
type FindingWriter interface {
|
||||
Write(ctx context.Context, f meshconditions.Finding) error
|
||||
Clear(ctx context.Context, scope, id, kind, why string) error
|
||||
Held() []meshconditions.Finding
|
||||
}
|
||||
|
||||
// NewFindingWriter makes the gate's held writer over the state findings, hearing findings-asked on the subject of the
|
||||
// generated contract of the seat that defines it (A-17). It is started once the runtime serves (A-40).
|
||||
func NewFindingWriter() (*views.HeldWriter[meshconditions.Finding, meshconditions.FindingCleared, meshconditions.FindingsStated, meshconditions.FindingsAskedEvent], error) {
|
||||
return views.NewStdioHeldWriter(views.WriterConfig[meshconditions.Finding, meshconditions.FindingCleared, meshconditions.FindingsStated, meshconditions.FindingsAskedEvent]{
|
||||
Asked: meshconditions.SubjectFindingsAsked, Module: Module, State: stdio.State("findings"), Kinds: []string{ApprovalCarriedOpen}})
|
||||
}
|
||||
|
||||
// approvalID is the id of a pull request's finding: its repository and number, each escaped as a key's name is.
|
||||
func approvalID(repository string, number int64) string {
|
||||
return keys.EscapeName(repository) + "." + keys.EscapeName(strconv.FormatInt(number, 10))
|
||||
}
|
||||
|
||||
// approvalOpen writes the finding for an `approval-carried` the gate says, naming the head the Approve named, unless it holds it.
|
||||
func (h *Holder) approvalOpen(repository string, number int, head string) error {
|
||||
if h.Findings == nil {
|
||||
return nil
|
||||
}
|
||||
id := approvalID(repository, int64(number))
|
||||
for _, f := range h.Findings.Held() {
|
||||
if f.Scope == FindingScope && f.ID == id && f.Kind == ApprovalCarriedOpen {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
at := h.Now().UTC()
|
||||
pull := fmt.Sprintf("%s#%d", repository, number)
|
||||
return h.Findings.Write(context.Background(), meshconditions.Finding{Scope: FindingScope, ID: id, Kind: ApprovalCarriedOpen,
|
||||
Subject: meshconditions.Subject{Scope: FindingScope, ID: id},
|
||||
Severity: meshconditions.FindingSeverityWarning, Resolver: "operator",
|
||||
Summary: fmt.Sprintf("the Approve of %s at %s, given before G, is carried and no merge or close of that pull request followed", pull, shortOf(head, 12)),
|
||||
Headline: "An Approve carried over waits for its pull request",
|
||||
Explanation: "The operator's Approve of this pull request, given before the delivery's switch, counts as its approval step. Nobody has merged or closed the pull request since.",
|
||||
Needs: "merge the pull request through the seat git's merge-pull, or close it; the finding clears when the seat git says either.",
|
||||
Source: "approval-carried", Observer: Module, At: at, Since: at})
|
||||
}
|
||||
|
||||
// pullEnded clears the finding of a pull request the seat git said merged or closed; a pull request with none changes nothing.
|
||||
func (h *Holder) pullEnded(body []byte, why string) error {
|
||||
if h.Findings == nil {
|
||||
return nil
|
||||
}
|
||||
var p struct {
|
||||
Repository string `json:"repository"`
|
||||
Number int64 `json:"number"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &p); err != nil || p.Repository == "" || p.Number <= 0 {
|
||||
h.Refuse(fmt.Errorf("a %s of the seat git is not readable: %v", why, err))
|
||||
return nil
|
||||
}
|
||||
id := approvalID(p.Repository, p.Number)
|
||||
for _, f := range h.Findings.Held() {
|
||||
if f.Scope == FindingScope && f.ID == id && f.Kind == ApprovalCarriedOpen {
|
||||
return h.Findings.Clear(context.Background(), FindingScope, id, ApprovalCarriedOpen, "the pull request was "+why)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions"
|
||||
mdc "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
"git.novox.be/novox/mesh-sdk/go/views"
|
||||
)
|
||||
|
||||
// memState is the gate's state `findings` in memory.
|
||||
type memState struct {
|
||||
mu sync.Mutex
|
||||
m map[string][]byte
|
||||
}
|
||||
|
||||
func (s *memState) Get(key string) (*stdio.StateEntry, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if v, ok := s.m[key]; ok {
|
||||
return &stdio.StateEntry{Key: key, Value: v}, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
func (s *memState) Put(key string, value any) (uint64, error) {
|
||||
raw, _ := json.Marshal(value)
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.m[key] = raw
|
||||
return 1, nil
|
||||
}
|
||||
func (s *memState) Delete(key string) error { s.mu.Lock(); delete(s.m, key); s.mu.Unlock(); return nil }
|
||||
func (s *memState) Keys() (k []string, _ error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
for key := range s.m {
|
||||
k = append(k, key)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type oldFake map[string][]byte
|
||||
|
||||
func (o oldFake) Keys(string) (k []string, _ error) {
|
||||
for key := range o {
|
||||
k = append(k, key)
|
||||
}
|
||||
return
|
||||
}
|
||||
func (o oldFake) Get(_, key string) ([]byte, uint64, error) { return o[key], 1, nil }
|
||||
|
||||
// The gate's own finding (A-39, A-29, A-37, §592): written through the held writer for each approval carried, once, and cleared
|
||||
// when the seat git says that pull request merged or closed; the writer's events are the manifest's.
|
||||
func TestTheGatesApprovalCarriedOpenIsWrittenThroughItsHeldWriterAndClearedByTheSeatGit(t *testing.T) {
|
||||
at := time.Date(2026, 11, 11, 21, 0, 0, 0, time.UTC)
|
||||
head := strings.Repeat("1", 40)
|
||||
state := &memState{m: map[string][]byte{}}
|
||||
var mu sync.Mutex
|
||||
var said []string
|
||||
w, err := views.NewHeldWriter(views.WriterConfig[meshconditions.Finding, meshconditions.FindingCleared, meshconditions.FindingsStated, meshconditions.FindingsAskedEvent]{
|
||||
Asked: meshconditions.SubjectFindingsAsked, Module: Module, State: state, Kinds: []string{ApprovalCarriedOpen}, Node: "ace",
|
||||
Now: func() time.Time { return at },
|
||||
Publish: func(_ context.Context, subject string, _ []byte, _ string) error {
|
||||
mu.Lock()
|
||||
said = append(said, subject)
|
||||
mu.Unlock()
|
||||
return nil
|
||||
},
|
||||
Listen: func(context.Context, string, func([]byte)) (func(), error) { return func() {}, nil }})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := w.Start(context.Background()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
merge, merr := mdc.EncodeMerge(Merge{V: 1, Group: "feat/x", Ask: "merge.feat_x.0123456789ab", Approved: map[string]string{"novox/app@7": head, "novox/web@8": head, "novox/db@9": head},
|
||||
Pulls: []MergePull{{Head: mdc.Head{Repository: "novox/db", Number: 9, Head: head}, State: mdc.MergeMerged, Commit: "c0ffee"}}}.stored())
|
||||
h := &Holder{Logf: t.Logf, Now: func() time.Time { return at }, Old: oldFake{"feat_x": merge}, Findings: w, SaySeat: func(string, any, string) error { return nil }}
|
||||
if merr != nil {
|
||||
t.Fatal(merr)
|
||||
}
|
||||
if err := h.carryApprovals(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
held := w.Held()
|
||||
if len(held) != 2 {
|
||||
t.Fatalf("held %+v", held)
|
||||
}
|
||||
for _, f := range held {
|
||||
if f.Kind != ApprovalCarriedOpen || f.Scope != FindingScope || f.Observer != Module || !strings.Contains(f.Summary, head[:12]) {
|
||||
t.Fatalf("finding %+v", f)
|
||||
}
|
||||
}
|
||||
if id := approvalID("novox/app", 7); held[0].ID != id && held[1].ID != id {
|
||||
t.Fatalf("no finding with id %s: %+v", id, held)
|
||||
}
|
||||
n := len(said)
|
||||
if err := h.carryApprovals(); err != nil || len(said) != n {
|
||||
t.Fatalf("said again: %v, %d events (was %d)", err, len(said), n)
|
||||
}
|
||||
// the seat git says one merged and the other closed; one it does not know changes nothing
|
||||
if err := h.pullEnded([]byte(`{"repository":"novox/app","number":7}`), "merged"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := h.pullEnded([]byte(`{"repository":"novox/other","number":1}`), "merged"); err != nil || len(w.Held()) != 1 {
|
||||
t.Fatalf("held %+v: %v", w.Held(), err)
|
||||
}
|
||||
if err := h.pullEnded([]byte(`{"repository":"novox/web","number":8}`), "closed"); err != nil || len(w.Held()) != 0 {
|
||||
t.Fatalf("held %+v: %v", w.Held(), err)
|
||||
}
|
||||
got := strings.Join(said, " ")
|
||||
for _, ev := range []string{"finding-written", "finding-cleared"} {
|
||||
if !strings.Contains(got, ev) {
|
||||
t.Fatalf("no %s said: %v", ev, said)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,8 @@ type Holder struct {
|
||||
Consumer *StepConsumer
|
||||
// SaySeat says an event of the seat mesh-delivery (recordevents.go); nil says none.
|
||||
SaySeat SeatSay
|
||||
// Findings is the gate's one held writer of its own findings (findings.go); nil writes none.
|
||||
Findings FindingWriter
|
||||
// Say says a condition to the operator, and Conditions keeps which the gate raised (conditions.go); nil says none.
|
||||
Say func(c oc.Condition) error
|
||||
Conditions ConditionStore
|
||||
|
||||
@@ -6,7 +6,7 @@ import "git.novox.be/novox/mesh-sdk/go/announce"
|
||||
// issue 1790): the module says it itself, from its code, before it serves, and a reader is granted what it said
|
||||
// (clause 3). A state is readable when another module's manifest reads it (keeps_test.go names the readers). keeps_test.go holds the list to the
|
||||
// manifest's `state`, and keeps_integration_test.go that the module announces it on a bus.
|
||||
var Kept = []announce.State{{Name: "conditions"}, {Name: "deliveries"}, {Name: "groups"}, {Name: "merges", Readable: true}, {Name: "owed", Readable: true}, {Name: "source", Readable: true}}
|
||||
var Kept = []announce.State{{Name: "conditions"}, {Name: "deliveries"}, {Name: "findings"}, {Name: "groups"}, {Name: "merges", Readable: true}, {Name: "owed", Readable: true}, {Name: "source", Readable: true}}
|
||||
|
||||
// announceKept says Kept through keeps, which is stdio.Keeps.
|
||||
func announceKept(keeps func(...announce.State) error) error { return keeps(Kept...) }
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
@@ -31,7 +32,7 @@ func (l *listening) get() string { l.mu.Lock(); defer l.mu.Unlock(); return l.n
|
||||
|
||||
// The events this owner follows: the forge bridge's word on an act owed, and the step events the delivery records are built
|
||||
// from (consumer.go).
|
||||
var followed = append([]string{owedEndedEvent}, stepPatterns()...)
|
||||
var followed = append(append([]string{owedEndedEvent}, stepPatterns()...), pullMergedEvent, pullClosedEvent)
|
||||
|
||||
func main() {
|
||||
settingsFile := os.Getenv("MESH_DELIVERY_GATE_SETTINGS")
|
||||
@@ -66,6 +67,20 @@ func main() {
|
||||
h.Records.Changed = h.recordChanged
|
||||
h.Old = kvOld{}
|
||||
h.Consumer = &StepConsumer{Records: h.Records, Families: stepFamilies, Now: h.Now, Logf: logf, Taken: h.TakenIn}
|
||||
// The gate's one held writer of its own findings (design 47 G A-39, A-37, A-40): made at once, started once the runtime serves.
|
||||
fw, err := NewFindingWriter()
|
||||
if err != nil {
|
||||
logf("%v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
h.Findings = fw
|
||||
go func() {
|
||||
<-stdio.Served()
|
||||
if err := fw.Start(context.Background()); err != nil {
|
||||
logf("%v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}()
|
||||
l := &listening{now: "not yet: starting"}
|
||||
go run(h, l)
|
||||
if err := announceKept(stdio.Keeps); err != nil {
|
||||
@@ -130,6 +145,12 @@ func take(h *Holder, e stdio.Envelope) error {
|
||||
if e.Key == owedEndedEvent {
|
||||
return h.OwedEnded(e.Body)
|
||||
}
|
||||
if e.Key == pullMergedEvent {
|
||||
return h.pullEnded(e.Body, "merged")
|
||||
}
|
||||
if e.Key == pullClosedEvent {
|
||||
return h.pullEnded(e.Body, "closed")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions"
|
||||
"git.novox.be/novox/mesh-sdk/go/buscalls"
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/seatlist/seatlisttest"
|
||||
@@ -136,7 +137,7 @@ func TestItClaimsTheDeliverySeatAndFollowsWhatItHandles(t *testing.T) {
|
||||
// From G's delivery the gate hears neither the bridge's merged and pushed messages nor the teller's answers nor the view's
|
||||
// reach (design 47 G §592, §593): it consumes the indexer's take-in.taken and the bridge's owed.ended, and the git events that clear
|
||||
// an approval carried.
|
||||
if !reflect.DeepEqual(m.Consumes, followed) {
|
||||
if want := append(slices.Clone(followed), meshconditions.Seat+"."+meshconditions.EventFindingsAsked); !reflect.DeepEqual(m.Consumes, want) {
|
||||
t.Fatalf("consumes %v, follows %v", m.Consumes, followed)
|
||||
}
|
||||
for _, c := range m.Consumes {
|
||||
@@ -146,6 +147,11 @@ func TestItClaimsTheDeliverySeatAndFollowsWhatItHandles(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
// A-30: the events its held writer says are declared in the manifest, and the two events that clear its finding are consumed.
|
||||
if !strings.Contains(raw, `"finding-written.*"`) || !strings.Contains(raw, `"finding-cleared.*"`) || !strings.Contains(raw, `"findings-stated.*"`) ||
|
||||
pullMergedEvent == "" || pullClosedEvent == "" {
|
||||
t.Fatalf("the finding events are not declared, or the pull events not consumed: %q %q", pullMergedEvent, pullClosedEvent)
|
||||
}
|
||||
if !reflect.DeepEqual(m.Emits, []string{"transition"}) { // `group` and `merged` go with the merge states (§431)
|
||||
t.Fatalf("emits %v", m.Emits)
|
||||
}
|
||||
|
||||
@@ -263,6 +263,9 @@ func (h *Holder) carryApprovals() error {
|
||||
if err := h.SaySeat(subject, body, fmt.Sprintf("approval-carried.%s.%d", repository, number)); err != nil {
|
||||
return fmt.Errorf("the approval of %s#%d cannot be said carried: %w", repository, number, err)
|
||||
}
|
||||
if err := h.approvalOpen(repository, number, head); err != nil {
|
||||
return fmt.Errorf("the finding of the approval of %s#%d cannot be written: %w", repository, number, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -4,6 +4,7 @@ go 1.26.0
|
||||
|
||||
require (
|
||||
git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient v0.0.0-00010101000000-000000000000
|
||||
|
||||
@@ -18,7 +18,10 @@
|
||||
],
|
||||
"consumes": [
|
||||
"mesh-delivery-forge-bridge.owed.ended",
|
||||
"mesh-delivery-indexer.take-in.taken"
|
||||
"mesh-delivery-indexer.take-in.taken",
|
||||
"git.pull-merged",
|
||||
"git.pull-closed",
|
||||
"mesh-conditions.findings-asked"
|
||||
],
|
||||
"emits": [
|
||||
"transition"
|
||||
@@ -297,6 +300,25 @@
|
||||
"name": "conditions",
|
||||
"storage": "key-value",
|
||||
"template": "state"
|
||||
},
|
||||
{
|
||||
"name": "findings",
|
||||
"storage": "key-value",
|
||||
"template": "state"
|
||||
},
|
||||
{
|
||||
"name": "finding-events",
|
||||
"storage": "stream",
|
||||
"template": "events",
|
||||
"options": {
|
||||
"events": [
|
||||
"finding-written.*",
|
||||
"finding-cleared.*",
|
||||
"findings-stated.*"
|
||||
],
|
||||
"max-age-days": 7,
|
||||
"max-mib": 16
|
||||
}
|
||||
}
|
||||
],
|
||||
"packages": {
|
||||
|
||||
Reference in New Issue
Block a user