From 349a3519effcb2638cfb571d6ad8b7fe1efba96d Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 11 Nov 2026 11:05:56 +0100 Subject: [PATCH] CT10: meshdeliveryclient reads the source per watched branch; SourceMainKey and the removed-verb rows retire MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Builds: design 47 G §15, §305 (a reader names a branch's source revision: SourceKey, the value SourceByBranchValue), §431 (the five verbs of release 6 leave the definition, so the list RemovedVerbs and its test go; the settle ask goes with the verb settle). Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ --- .../go/clients/meshdeliveryclient/client.go | 25 ++------- .../clients/meshdeliveryclient/client_test.go | 43 +--------------- .../go/clients/meshdeliveryclient/gate.go | 17 ++++--- .../clients/meshdeliveryclient/gate_test.go | 41 +++++++++++++-- .../go/clients/meshdeliveryclient/switched.go | 51 +++++++++++++++++++ 5 files changed, 100 insertions(+), 77 deletions(-) create mode 100644 contracts/go/clients/meshdeliveryclient/switched.go diff --git a/contracts/go/clients/meshdeliveryclient/client.go b/contracts/go/clients/meshdeliveryclient/client.go index 5754e183..590b5345 100644 --- a/contracts/go/clients/meshdeliveryclient/client.go +++ b/contracts/go/clients/meshdeliveryclient/client.go @@ -90,28 +90,9 @@ func ParseAskKey(key string) (kind, group, hash string, err error) { return parts[0], group, parts[2], nil } -// SettleGroupPrefix begins the group of a settle ask (novox/hq ADR 0556 clause 5): a person's ask to settle deliveries held -// because their merge reached the trunk without a passing check. It is kept in the asks under MergeAskKey like a merge ask, -// so the teller carries it unchanged. Git forbids a colon in a branch name, so no pull request's group ever begins with it. -const SettleGroupPrefix = "settle:" - -// SettleGroup is the group of the settle ask of these delivery ids: the prefix and twelve hex digits of the sha256 over the -// ids, sorted and joined by a newline, so the same deliveries asked again keep one key. -func SettleGroup(ids []string) string { - sorted := append([]string(nil), ids...) - sort.Strings(sorted) - sum := sha256.Sum256([]byte(strings.Join(sorted, "\n"))) - return SettleGroupPrefix + hex.EncodeToString(sum[:])[:12] -} - -// AskVerb is the act the options of an ask bind, by its group: a settle ask's `mesh-delivery.settle`, any other -// `mesh-delivery.merge`. The gate composes the act with it and the teller checks a warrant against it. -func AskVerb(group string) string { - if strings.HasPrefix(group, SettleGroupPrefix) { - return seat.Seat + "." + seat.VerbSettle - } - return seat.Seat + "." + VerbMerge -} +// AskVerb is the act the options of an ask bind, `mesh-delivery.merge`; the settle ask went with the verb settle (design 47 +// G §431). The gate composes the act with it and the teller checks a warrant against it. +func AskVerb(group string) string { return seat.Seat + "." + VerbMerge } // AskID is the id the teller gives the router for the ask at askKey. The router's ids refuse dots, so in the readable part a // dot becomes `-` and any other byte outside letters, digits, `_` and `-` becomes `_`; that part is cut to fit, and every id diff --git a/contracts/go/clients/meshdeliveryclient/client_test.go b/contracts/go/clients/meshdeliveryclient/client_test.go index 08718e85..9fcfc582 100644 --- a/contracts/go/clients/meshdeliveryclient/client_test.go +++ b/contracts/go/clients/meshdeliveryclient/client_test.go @@ -1,8 +1,6 @@ package meshdeliveryclient import ( - "encoding/json" - "os" "strings" "testing" @@ -40,10 +38,7 @@ func TestTheAsksAndOwedActsKeepTheirFormsAndRules(t *testing.T) { if id := AskingID(key, 3); id != "merge-fix_1_2e2-d106c7950d28-3-ea06879e48d8" || len(id) > 64 { t.Errorf("AskingID(3) = %q", id) } - if g := SettleGroup([]string{"b", "a"}); g != "settle:7e18f737311b" || g != SettleGroup([]string{"a", "b"}) { - t.Errorf("SettleGroup = %q", g) - } - if AskVerb(SettleGroup([]string{"a"})) != "mesh-delivery.settle" || AskVerb("fix/1") != "mesh-delivery.merge" { + if AskVerb("fix/1") != "mesh-delivery.merge" { t.Error("AskVerb binds the wrong act") } @@ -91,39 +86,3 @@ func TestTheAsksAndOwedActsKeepTheirFormsAndRules(t *testing.T) { } } } - -// The removed verbs are the optional rows of the seat's definition that say they were removed, each with the replacement -// the definition gives: the list and the definition cannot drift apart. -func TestTheRemovedVerbsAreTheDefinitionsRemovedRows(t *testing.T) { - raw, err := os.ReadFile("../../../seats/mesh-delivery/seat.json") - if err != nil { - t.Fatal(err) - } - var def struct { - Verbs []struct { - Name string `json:"name"` - Description string `json:"description"` - Optional bool `json:"optional"` - } `json:"verbs"` - } - if err := json.Unmarshal(raw, &def); err != nil { - t.Fatal(err) - } - removed := RemovedVerbs() - rows := 0 - for _, v := range def.Verbs { - if !strings.HasPrefix(v.Description, RemovedPrefix) { - if _, in := removed[v.Name]; in { - t.Errorf("%s is in the removed list, the definition keeps it", v.Name) - } - continue - } - rows++ - if want, in := removed[v.Name]; !in || !v.Optional || v.Description != RemovedPrefix+want { - t.Errorf("%s: in the list %v, optional %v, described %q, want %q", v.Name, in, v.Optional, v.Description, RemovedPrefix+want) - } - } - if rows != len(removed) || rows == 0 { - t.Errorf("the definition removes %d verbs, the list names %d", rows, len(removed)) - } -} diff --git a/contracts/go/clients/meshdeliveryclient/gate.go b/contracts/go/clients/meshdeliveryclient/gate.go index de79bd28..fa56a1c1 100644 --- a/contracts/go/clients/meshdeliveryclient/gate.go +++ b/contracts/go/clients/meshdeliveryclient/gate.go @@ -31,8 +31,9 @@ const ( MergeRefused MergeState = "refused" ) -// SourceMainKey is the one key of the state source. -const SourceMainKey = "main" +// SourceKey is the key of the state source for a watched branch (design 47 *Watched branches and the catalog graph per +// branch*: source. in place of source.main). +func SourceKey(branch string) string { return seat.SourceByBranchKey(branch) } func encodeValue(v any, check error, stamp func()) ([]byte, error) { if check != nil { @@ -76,7 +77,7 @@ func ParseTurnsKey(key string) (string, error) { // ----- source // CheckSource refuses a source naming no repository, a repository with no name or an empty head. -func CheckSource(s seat.SourceByMainValue) error { +func CheckSource(s seat.SourceByBranchValue) error { if len(s.Heads) == 0 { return fmt.Errorf("meshdeliveryclient: a source names no repository") } @@ -92,16 +93,16 @@ func CheckSource(s seat.SourceByMainValue) error { } // EncodeSource is s as the value of the key main. -func EncodeSource(s seat.SourceByMainValue) ([]byte, error) { +func EncodeSource(s seat.SourceByBranchValue) ([]byte, error) { return encodeValue(&s, CheckSource(s), func() { s.V = Version }) } // DecodeSource reads the value of the key main. -func DecodeSource(data []byte) (seat.SourceByMainValue, error) { +func DecodeSource(data []byte) (seat.SourceByBranchValue, error) { if err := keys.CheckVersion(data); err != nil { - return seat.SourceByMainValue{}, err + return seat.SourceByBranchValue{}, err } - s, err := seat.DecodeSourceByMainValue(data) + s, err := seat.DecodeSourceByBranchValue(data) if err != nil { return s, fmt.Errorf("meshdeliveryclient: a source does not fit its shape: %w", err) } @@ -109,7 +110,7 @@ func DecodeSource(data []byte) (seat.SourceByMainValue, error) { } // SameSource says whether two sources are the same. -func SameSource(a, b seat.SourceByMainValue) bool { return reflect.DeepEqual(a, b) } +func SameSource(a, b seat.SourceByBranchValue) bool { return reflect.DeepEqual(a, b) } // ----- merge diff --git a/contracts/go/clients/meshdeliveryclient/gate_test.go b/contracts/go/clients/meshdeliveryclient/gate_test.go index 4ec7b46e..92be0bf2 100644 --- a/contracts/go/clients/meshdeliveryclient/gate_test.go +++ b/contracts/go/clients/meshdeliveryclient/gate_test.go @@ -41,9 +41,9 @@ func TestGateValuesEncodeAsGoDeliveryDid(t *testing.T) { golden string }{ {"source", func() ([]byte, error) { - return EncodeSource(seat.SourceByMainValue{Group: "fix/1.2", Heads: map[string]string{"novox/a": "aaaa", "novox/b": "bbbb"}}) + return EncodeSource(seat.SourceByBranchValue{Group: "fix/1.2", Heads: map[string]string{"novox/a": "aaaa", "novox/b": "bbbb"}}) }, func(b []byte) (any, error) { return DecodeSource(b) }, - seat.SourceByMainValue{V: 1, Group: "fix/1.2", Heads: map[string]string{"novox/a": "aaaa", "novox/b": "bbbb"}}, + seat.SourceByBranchValue{V: 1, Group: "fix/1.2", Heads: map[string]string{"novox/a": "aaaa", "novox/b": "bbbb"}}, `{"v":1,"group":"fix/1.2","heads":{"novox/a":"aaaa","novox/b":"bbbb"}}`}, {"merge", func() ([]byte, error) { return EncodeMerge(seat.MergesByGroupValue{Group: "g", Ask: "merge.g.0123456789ab", Approved: map[string]string{"novox/a@7": "h1"}, @@ -108,8 +108,39 @@ func TestGateKeysAsGoDeliveryHadThem(t *testing.T) { if k := ApprovedKey("novox/a", 7); k != "novox/a@7" { t.Error(k) } - if SourceMainKey != seat.SourceByMainKey() { - t.Error("the source's key is the seat's") + if SourceKey("beta") != seat.SourceByBranchKey("beta") || SourceKey("beta") != "beta" { + t.Error("the source's key is the seat's, one per watched branch") + } + // A switched act's id carries its state, key and revision, and reads back; any other id is not one. + for _, c := range []struct { + state, key string + rev uint64 + id string + }{ + {SwitchedOwed, "novox.app.7.0123abcd.status=mesh/delivery", 4, "switched.owed.4.novox.app.7.0123abcd.status=mesh/delivery"}, + {SwitchedMerges, "release-g", 12, "switched.merges.12.release-g"}, + } { + id, err := SwitchedActID(c.state, c.key, c.rev) + if err != nil || id != c.id { + t.Errorf("SwitchedActID(%q, %q, %d) = %q, %v; want %q", c.state, c.key, c.rev, id, err, c.id) + } + if s, k, r, ok := ParseSwitchedActID(id); !ok || s != c.state || k != c.key || r != c.rev { + t.Errorf("ParseSwitchedActID(%q) = %q %q %d %v", id, s, k, r, ok) + } + } + for _, bad := range []struct { + state, key string + rev uint64 + }{{"turns", "k", 1}, {SwitchedOwed, "", 1}, {SwitchedOwed, "a@b", 1}, {SwitchedMerges, "k", 0}} { + if id, err := SwitchedActID(bad.state, bad.key, bad.rev); err == nil { + t.Errorf("SwitchedActID(%+v) = %q, no error", bad, id) + } + } + for _, other := range []string{"novox.app.7.0123abcd.view@1700000000", "switched.turns.1.k", "switched.owed.x.k", + "switched.owed.0.k", "switched.owed.3", "switched.owed.3.k@1"} { + if _, _, _, ok := ParseSwitchedActID(other); ok { + t.Errorf("%q reads as a switched act's id", other) + } } } @@ -150,7 +181,7 @@ func TestGateValuesRefuseWhatTheirRulesRefuse(t *testing.T) { t.Errorf("%s: %v, want %q", name, c.err, c.want) } } - if _, err := EncodeSource(seat.SourceByMainValue{Group: "g"}); err == nil { + if _, err := EncodeSource(seat.SourceByBranchValue{Group: "g"}); err == nil { t.Error("an empty source encodes") } if _, err := EncodeAsk(seat.AsksByMergeGroupHeadsValue{Group: "g", Ask: "text", Heads: []seat.AsksByMergeGroupHeadsValueHeadsItem{{Repository: "novox/a", Number: 1, Head: "h"}}}); err == nil { diff --git a/contracts/go/clients/meshdeliveryclient/switched.go b/contracts/go/clients/meshdeliveryclient/switched.go new file mode 100644 index 00000000..1addf31f --- /dev/null +++ b/contracts/go/clients/meshdeliveryclient/switched.go @@ -0,0 +1,51 @@ +package meshdeliveryclient + +import ( + "fmt" + "strconv" + "strings" +) + +// A switched act is an act the gate owed before its switch to the delivery record (design 47 *The switch carries what is +// owed*): a key of the gate's state owed or merges that the old forge bridge had not performed. The gate says it as the +// event owed under an id made of the state, the key and its revision, so the new forge bridge counts it as performed when +// the old bridge's own bookkeeping names that key at that revision, and nothing is done twice. The form is +// `switched...`, the key last because it holds dots; it holds no `@`, which the gate's own ids hold, +// so the two never meet. + +// The states a switched act comes from. +const ( + SwitchedOwed = "owed" + SwitchedMerges = "merges" +) + +const switchedPrefix = "switched." + +// SwitchedActID is the id of the act the key of the state held at the revision. +func SwitchedActID(state, key string, revision uint64) (string, error) { + if state != SwitchedOwed && state != SwitchedMerges { + return "", fmt.Errorf("meshdeliveryclient: a switched act comes from the state owed or merges, not %q", state) + } + if key == "" || strings.Contains(key, "@") { + return "", fmt.Errorf("meshdeliveryclient: a switched act's key %q is empty or holds an @", key) + } + if revision == 0 { + return "", fmt.Errorf("meshdeliveryclient: a switched act's revision is 1 or more") + } + return switchedPrefix + state + "." + strconv.FormatUint(revision, 10) + "." + key, nil +} + +// ParseSwitchedActID reads the state, the key and the revision out of a switched act's id; ok is false for any other id. +func ParseSwitchedActID(id string) (state, key string, revision uint64, ok bool) { + rest, found := strings.CutPrefix(id, switchedPrefix) + if !found || strings.Contains(id, "@") { + return "", "", 0, false + } + state, rest, _ = strings.Cut(rest, ".") + rev, key, _ := strings.Cut(rest, ".") + revision, err := strconv.ParseUint(rev, 10, 64) + if err != nil || revision == 0 || key == "" || (state != SwitchedOwed && state != SwitchedMerges) { + return "", "", 0, false + } + return state, key, revision, true +}