B7-3: the pinned event's catalog entries name their branch, the record keeps it

A catalog revision is pinned by its watched branch and its number (revisions are numbered per branch): PinnedRevision gains
branch, Pin and Free take it, the key names it so two branches' revision 9 are two pins, and pinsOf says {branch, revision}.
Also the C2 merge's sourcetools imports follow the contracts' new path.

Builds: design 47 G §579, A-171
Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
2026-11-12 05:45:53 +01:00
parent 11f49905a1
commit b52dd7ca44
6 changed files with 62 additions and 26 deletions
@@ -194,10 +194,12 @@ type NodeAway struct {
Since time.Time `json:"since"`
}
// PinnedRevision is a graph revision the record points at: its half (catalog or assignment) and its revision.
// PinnedRevision is a graph revision the record points at: its half (catalog or assignment), its revision and, for a catalog
// revision, the watched branch it is numbered on.
type PinnedRevision struct {
Revision string `json:"revision"`
Half string `json:"half"`
Branch string `json:"branch,omitempty"`
}
// RecordEnded is how a record ended: when, why, by whom, and the nodes away it ended without.
@@ -258,7 +258,7 @@ func TestTheDeliveryRecordIsALedgerInstanceKeyedByMilestoneAndHeads(t *testing.T
return nil
}}
old, _, _ := rs.Open("46/F", heads, false)
if err := rs.Pin(old, HalfCatalog, 1); err != nil {
if err := rs.Pin(old, HalfCatalog, "main", 1); err != nil {
t.Fatal(err)
}
now = at.Add(89 * 24 * time.Hour)
@@ -273,7 +273,7 @@ func TestTheDeliveryRecordIsALedgerInstanceKeyedByMilestoneAndHeads(t *testing.T
if r, _ := rs.Read(old.Instance); r != nil {
t.Fatal("a record past its retention is kept")
}
if !slices.Equal(freed, []string{"46/F [1] the record is past its retention"}) {
if !slices.Equal(freed, []string{"46/F [{main 1}] the record is past its retention"}) {
t.Fatalf("freed %v", freed)
}
list, _ := rs.List()
@@ -299,11 +299,15 @@ func TestTheDeliveryRecordIsALedgerInstanceKeyedByMilestoneAndHeads(t *testing.T
}}
r, _, _ := rs.Open("47/G", heads, false)
for range 2 {
if err := rs.Pin(r, HalfAssignment, 9); err != nil {
if err := rs.Pin(r, HalfAssignment, "", 9); err != nil {
t.Fatal(err)
}
}
if err := rs.Pin(r, HalfCatalog, 9); err != nil {
if err := rs.Pin(r, HalfCatalog, "main", 9); err != nil {
t.Fatal(err)
}
// Revisions are numbered per watched branch: the same number on another branch is another pin.
if err := rs.Pin(r, HalfCatalog, "feat/x", 9); err != nil {
t.Fatal(err)
}
pins := 0
@@ -313,28 +317,36 @@ func TestTheDeliveryRecordIsALedgerInstanceKeyedByMilestoneAndHeads(t *testing.T
}
}
back, _ := rs.Read(r.Instance)
if pins != 2 || len(back.Pinned) != 2 {
if pins != 3 || len(back.Pinned) != 3 {
t.Fatalf("%d pins written, the record holds %+v", pins, back.Pinned)
}
if len(got) != 3 || !reflect.DeepEqual(got[0], got[1]) || got[0].subject != meshdelivery.SubjectPinned ||
!slices.Equal(got[0].pins.Assignment, []int64{9}) || got[2].pins.Catalog[0] != 9 || got[0].pins.Record != r.Instance {
if len(got) != 4 || !reflect.DeepEqual(got[0], got[1]) || got[0].subject != meshdelivery.SubjectPinned ||
!slices.Equal(got[0].pins.Assignment, []int64{9}) || !slices.Equal(got[2].pins.Catalog, []meshdelivery.PinsCatalogItem{{Branch: "main", Revision: 9}}) ||
!slices.Equal(got[3].pins.Catalog, []meshdelivery.PinsCatalogItem{{Branch: "feat/x", Revision: 9}}) || got[0].pins.Record != r.Instance {
t.Fatalf("said %+v", got)
}
got = nil
if err := rs.Free(back, HalfAssignment, 9, "removed by hand"); err != nil {
if err := rs.Free(back, HalfAssignment, "", 9, "removed by hand"); err != nil {
t.Fatal(err)
}
if err := rs.Free(back, HalfAssignment, 9, "again"); err != nil {
if err := rs.Free(back, HalfAssignment, "", 9, "again"); err != nil {
t.Fatal(err)
}
if len(got) != 1 || got[0].subject != meshdelivery.SubjectFreed || got[0].pins.Why != "removed by hand" ||
!slices.Equal(got[0].pins.Assignment, []int64{9}) {
t.Fatalf("said %+v", got)
}
got = nil
if err := rs.Free(back, HalfCatalog, "feat/x", 9, "removed"); err != nil {
t.Fatal(err)
}
if len(got) != 1 || !slices.Equal(got[0].pins.Catalog, []meshdelivery.PinsCatalogItem{{Branch: "feat/x", Revision: 9}}) {
t.Fatalf("said %+v", got)
}
if again, _ := rs.Read(r.Instance); len(again.Pinned) != 1 {
t.Fatalf("pinned after the free %+v", again.Pinned)
}
if err := rs.Pin(r, "trunk", 1); err == nil {
if err := rs.Pin(r, "trunk", "", 1); err == nil {
t.Fatal("a pin of no half is taken")
}
})
@@ -14,6 +14,7 @@ import (
"time"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshdelivery"
"git.novox.be/novox/mesh-sdk/go/keys"
)
// The halves of the revision view a record pins (design 47 *Two graphs, one source of truth*).
@@ -39,13 +40,24 @@ func (rs *Records) say(subject string, body any, id string) error {
return rs.Say(subject, body, id)
}
// pinKey is a pinned revision's key in the record: its half and its number, so the two halves' numbers never meet.
func pinKey(half string, revision int64) string { return half + "-" + strconv.FormatInt(revision, 10) }
// pinKey is a pinned revision's key in the record: its half, for a catalog revision its watched branch (revisions are numbered
// per branch, A-171) escaped as a name with its slashes escaped too, and its number, so no two revisions' keys meet.
func pinKey(half, branch string, revision int64) string {
key := half + "-"
if half == HalfCatalog {
key += strings.ReplaceAll(keys.EscapeName(branch), "/", "=2f") + "-"
}
return key + strconv.FormatInt(revision, 10)
}
// number is the revision's number on its half's seat.
// number is the revision's number on its half's seat: what follows the key's last dash.
func (p PinnedRevision) number() (int64, bool) {
n, err := strconv.ParseInt(strings.TrimPrefix(p.Revision, p.Half+"-"), 10, 64)
return n, err == nil && strings.HasPrefix(p.Revision, p.Half+"-")
i := strings.LastIndex(p.Revision, "-")
if i < 0 || !strings.HasPrefix(p.Revision, p.Half+"-") {
return 0, false
}
n, err := strconv.ParseInt(p.Revision[i+1:], 10, 64)
return n, err == nil
}
// pinsOf is the seat's Pins of a record for these revisions.
@@ -58,7 +70,7 @@ func pinsOf(r *Record, pins []PinnedRevision, why string) meshdelivery.Pins {
}
switch p.Half {
case HalfCatalog:
out.Catalog = append(out.Catalog, n)
out.Catalog = append(out.Catalog, meshdelivery.PinsCatalogItem{Branch: p.Branch, Revision: n})
case HalfAssignment:
out.Assignment = append(out.Assignment, n)
}
@@ -74,12 +86,18 @@ func checkHalf(half string) error {
}
// Pin writes that the record points at a graph revision, unless it does already, and says `pinned` (again, when written
// before: the id keeps it once).
func (rs *Records) Pin(r *Record, half string, revision int64) error {
// before: the id keeps it once). A catalog revision names its watched branch, which the record keeps and the event says.
func (rs *Records) Pin(r *Record, half, branch string, revision int64) error {
if err := checkHalf(half); err != nil {
return err
}
p := PinnedRevision{Revision: pinKey(half, revision), Half: half}
if half == HalfCatalog && branch == "" {
return fmt.Errorf("a catalog revision is pinned by its watched branch and its number")
}
if half != HalfCatalog {
branch = ""
}
p := PinnedRevision{Revision: pinKey(half, branch, revision), Half: half, Branch: branch}
if _, ok := r.Pinned[p.Revision]; !ok {
seq, err := rs.Ledger.Apply(r.Instance, OpPin, p)
if err != nil {
@@ -95,11 +113,11 @@ func (rs *Records) Pin(r *Record, half string, revision int64) error {
// Free writes that the record no longer points at a graph revision and says `freed` with why; a revision it does not
// point at frees nothing and says nothing.
func (rs *Records) Free(r *Record, half string, revision int64, why string) error {
func (rs *Records) Free(r *Record, half, branch string, revision int64, why string) error {
if err := checkHalf(half); err != nil {
return err
}
p, ok := r.Pinned[pinKey(half, revision)]
p, ok := r.Pinned[pinKey(half, branch, revision)]
if !ok {
return nil
}
@@ -11,7 +11,7 @@ import (
"strings"
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient"
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
seat "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshdelivery"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/keys"
)
@@ -2,7 +2,7 @@ package main
import (
"errors"
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
seat "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshdelivery"
"strings"
"testing"
"time"
@@ -74,7 +74,7 @@ func TestTheGateStatesItsPinsThenSaysPinsStated(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if err := rs.Pin(r, HalfCatalog, 3); err != nil {
if err := rs.Pin(r, HalfCatalog, "main", 3); err != nil {
t.Fatal(err)
}
events = nil
+5 -1
View File
@@ -98,6 +98,9 @@
"half": {
"type": "string"
},
"branch": {
"type": "string"
},
"ended": {
"type": "object"
},
@@ -208,7 +211,8 @@
"effect": "set-entry",
"fields": [
"revision",
"half"
"half",
"branch"
],
"into": "pinned",
"key": "revision"