GATE7 (source part, §690): the gate's source answers a branch's source key, or the branches it keeps

The gate's source value is the contract's SourceByBranchValue and its key SourceKey(branch); the verb takes an optional
branch and answers branches alone when given none. The whole-module package does not compile on the join for GATE11
(settle) and GATE17 (ledger fill and remove), so the unit suite was run with those five sites stubbed in a scratch edit,
never committed: green apart from the stubbed removal case.

Builds: design 47 G §690, §614, §15

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
This commit is contained in:
2026-11-11 21:35:26 +01:00
parent b90d3a4556
commit d786477a0d
6 changed files with 80 additions and 27 deletions
@@ -178,17 +178,27 @@ func (s *fakeSource) Move(update func(prev *Source) (Source, bool)) error {
return nil
}
// Read is source.main as held, and its revision: the number of writes.
func (s *fakeSource) Read() (*Source, uint64, error) {
// Read is a branch's source key as held, and its revision: the number of writes. The fake holds the key of MergeBranch only.
func (s *fakeSource) Read(branch string) (*Source, uint64, error) {
s.mu.Lock()
defer s.mu.Unlock()
if s.main == nil {
if s.main == nil || branch != MergeBranch {
return nil, 0, nil
}
held := *s.main
return &held, uint64(len(s.moves)), nil
}
// Branches are the branches whose source key the fake holds.
func (s *fakeSource) Branches() ([]string, error) {
s.mu.Lock()
defer s.mu.Unlock()
if s.main == nil {
return nil, nil
}
return []string{MergeBranch}, nil
}
func sortStrings(s []string) { sort.Strings(s) }
func itoa(n int) string { return strconv.Itoa(n) }
@@ -639,7 +639,7 @@ func TestAPassingVerdictAsksTheApproveAndItsApproveMergesWithinSecondsOverARealB
// The forge's own word that the pull request merged follows the bridge's (its webhook): the delivery ends merged.
b.publish(t, "mesh.seat.git.event.pull.merged", seatMerged(merged("novox/app", 7, head, strings.Repeat("b", 40))))
rt.until("source.main to hold the merge", eventWithin, func() bool {
e, err := b.kvs["source"].Get(ctx, mdc.SourceMainKey)
e, err := b.kvs["source"].Get(ctx, mdc.SourceKey(MergeBranch))
if err != nil {
return false
}
@@ -864,7 +864,7 @@ func TestAGroupOfTwoWithGreenChecksGetsOneAskAndOnItsApproveBothMergeOneAfterThe
b.forgeSays(t, forgemsg.Forge{Kind: forgemsg.ForgeMerged, Repository: two.Repository, Number: two.Number, Head: two.Head.Head,
Merge: strings.Repeat("b", 40), Root: "r", Trees: map[string]string{"x": "t"}})
rt.until("source.main to hold both merges", eventWithin, func() bool {
e, err := b.kvs["source"].Get(ctx, mdc.SourceMainKey)
e, err := b.kvs["source"].Get(ctx, mdc.SourceKey(MergeBranch))
if err != nil {
return false
}
@@ -1,6 +1,7 @@
package main
import (
"reflect"
"testing"
"time"
@@ -41,11 +42,18 @@ func TestAMergeHeardOnlyFromTheForgeMovesSourceMain(t *testing.T) {
t.Fatalf("source.main is %+v; wanted %s at %s and %s kept", w.source.main, i888App, i888Merge1, i888Other)
}
// The seat's verb source answers each repository's trunk branch and head, and the revision, from the same key.
if a := w.h.SourceRevision(); a.Error != "" || a.Revision != 1 || len(a.Repositories) != 2 ||
if a := w.h.SourceRevision(MergeBranch); a.Error != "" || a.Revision != 1 || len(a.Repositories) != 2 ||
a.Repositories[0] != (seat.SourceHead{Repository: i888App, Trunk: MergeBranch, Head: i888Merge1}) ||
a.Repositories[1].Repository != i888Other || a.Repositories[1].Head != i888OtherH {
t.Fatalf("source answers %+v", a)
}
// §690: given no branch it answers the branches of the keys it keeps, and no repository; a branch it keeps no key of has none.
if a := w.h.SourceRevision(""); a.Error != "" || !reflect.DeepEqual(a.Branches, []string{MergeBranch}) || len(a.Repositories) != 0 || a.Revision != 0 {
t.Fatalf("source with no branch answers %+v, want the branches %v alone", a, []string{MergeBranch})
}
if a := w.h.SourceRevision("beta"); a.Error != "" || a.Branch != "beta" || a.Revision != 0 || len(a.Repositories) != 0 {
t.Fatalf("source of a branch with no key answers %+v", a)
}
n := len(w.source.moves)
w.settleAll()
w.h.PullMerged(i888MergedAt(i888App, 3, i888Head1, i888Merge1, w.now.Add(-time.Minute))) // heard again
@@ -345,8 +345,9 @@ func tools(h *Holder, l *listening) []stdio.Tool {
Description: "The source revision the gate holds: every repository it names, its trunk branch and that trunk's head, the " +
"group that moved it last and its revision. Read, never written: a node-engine reads the repositories here to call a " +
"reverted build's catch-up tool once per repository, never the gate's state.",
Input: map[string]any{"type": "object", "properties": map[string]any{}},
Run: func(map[string]any) (any, error) { return h.SourceRevision(), nil }},
Input: map[string]any{"type": "object", "properties": map[string]any{
"branch": str("the watched branch, escaped as a key name is; absent: the list of the branches the gate keeps a source revision of")}},
Run: func(a map[string]any) (any, error) { return h.SourceRevision(strArg(a, "branch")), nil }},
{Name: seat + "checks",
Description: "What the mesh's checks said of a pull request's head or of one commit: each of the commit's " +
"statuses (mesh/check, mesh/delivery, …) with its state, description and when it " +
@@ -14,6 +14,7 @@ import (
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/keys"
)
// The gate merges through the forge bridge, never itself (novox/hq ADR 0437 §8, ADR 0441 §5, ADR 0450 §3). It writes into
@@ -48,13 +49,15 @@ func (kvOld) Get(state, key string) ([]byte, uint64, error) {
return e.Value, e.Revision, nil
}
// SourceStore is the gate's bucket source, one key, moved by compare-and-set.
// SourceStore is the gate's bucket source, one key per watched branch (design 47 G §15), moved by compare-and-set.
type SourceStore interface {
// Move reads source.main and writes what update makes of it, unless update says nothing is to be written; a write
// another writer won is read again and tried again.
// Move reads the source key of the gate's merge branch (MergeBranch) and writes what update makes of it, unless update
// says nothing is to be written; a write another writer won is read again and tried again.
Move(update func(prev *Source) (Source, bool)) error
// Read is source.main as held and its revision on the bus; nil and 0 before it is first written.
Read() (*Source, uint64, error)
// Read is a branch's source key as held and its revision on the bus; nil and 0 before it is first written.
Read(branch string) (*Source, uint64, error)
// Branches are the branch names of the source keys the gate keeps (§614, §690).
Branches() ([]string, error)
}
// kvMerges is merges over the runtime.
@@ -95,7 +98,7 @@ type kvSource struct{}
func (kvSource) Move(update func(prev *Source) (Source, bool)) error {
state := stdio.State("source")
for range changeAttempts {
e, err := state.Get(mdc.SourceMainKey)
e, err := state.Get(mdc.SourceKey(MergeBranch))
if err != nil {
return err
}
@@ -116,9 +119,9 @@ func (kvSource) Move(update func(prev *Source) (Source, bool)) error {
return err
}
if e == nil {
_, err = state.Create(mdc.SourceMainKey, json.RawMessage(raw))
_, err = state.Create(mdc.SourceKey(MergeBranch), json.RawMessage(raw))
} else {
_, err = state.Update(mdc.SourceMainKey, json.RawMessage(raw), e.Revision)
_, err = state.Update(mdc.SourceKey(MergeBranch), json.RawMessage(raw), e.Revision)
}
if err == nil {
return nil
@@ -130,33 +133,64 @@ func (kvSource) Move(update func(prev *Source) (Source, bool)) error {
return fmt.Errorf("source.main was written by another writer first %d times: %w", changeAttempts, stdio.ErrChanged)
}
func (kvSource) Read() (*Source, uint64, error) {
e, err := stdio.State("source").Get(mdc.SourceMainKey)
func (kvSource) Read(branch string) (*Source, uint64, error) {
e, err := stdio.State("source").Get(mdc.SourceKey(branch))
if err != nil || e == nil {
return nil, 0, err
}
held, err := mdc.DecodeSource(e.Value)
if err != nil {
return nil, 0, fmt.Errorf("source.main is not readable: %w", err)
return nil, 0, fmt.Errorf("source.%s is not readable: %w", branch, err)
}
return &held, e.Revision, nil
}
// SourceRevision is the seat's verb source: every repository source.main names, its trunk branch and head, the group that
// moved it last and its revision, or why it cannot be read. source.main names each repository's main only (MergeBranch).
// Outside the lock.
func (h *Holder) SourceRevision() seat.SourceAnswer {
func (kvSource) Branches() ([]string, error) {
names, err := stdio.State("source").Keys()
if err != nil {
return nil, err
}
var out []string
for _, key := range names {
escaped, ok := seat.ParseSourceByBranchKey(key)
if !ok {
continue
}
branch, err := keys.UnescapeName(escaped)
if err != nil {
return nil, fmt.Errorf("the source key %q is not a branch's: %w", key, err)
}
out = append(out, branch)
}
sort.Strings(out)
return out, nil
}
// SourceRevision is the seat's verb source (§690): given a branch, every repository that branch's source key names, its
// trunk branch and head, the group that moved it last and its revision, or why it cannot be read; given none, `branches`,
// the branch of each source key the gate keeps. A key names each repository's merge branch only (MergeBranch). Outside the lock.
func (h *Holder) SourceRevision(branch string) seat.SourceAnswer {
if h.Source == nil {
return seat.SourceAnswer{Error: "no source bucket"}
}
held, revision, err := h.Source.Read()
if branch == "" {
branches, err := h.Source.Branches()
if err != nil {
return seat.SourceAnswer{Error: err.Error()}
}
if branches == nil {
branches = []string{}
}
return seat.SourceAnswer{Branches: branches}
}
held, revision, err := h.Source.Read(branch)
if err != nil {
return seat.SourceAnswer{Error: err.Error()}
}
if held == nil {
return seat.SourceAnswer{}
return seat.SourceAnswer{Branch: branch}
}
out := seat.SourceAnswer{Group: held.Group, Revision: int64(revision)}
out := seat.SourceAnswer{Branch: branch, Group: held.Group, Revision: int64(revision)}
for repository, head := range held.Heads {
out.Repositories = append(out.Repositories, seat.SourceHead{Repository: repository, Trunk: MergeBranch, Head: head})
}
@@ -22,7 +22,7 @@ const (
// are converted at the edge, so a stored value decodes unchanged (novox/hq ADR 0634 §2).
type (
// Source is the state source's value.
Source = seat.SourceByMainValue
Source = seat.SourceByBranchValue
// Owed is the state owed's value: what the forge bridge performs.
Owed = seat.OwedByRepositoryNumberHeadActValue
// Answer is the operator's answer as the gate acts on it.