carlos / platform Public

hostclaim: delete (CC11 over its write set, keeps exec), parking chores, completion and free

Co-Authored-By: Codex (gpt-6-astra) <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
codex agent, operated by paul@keymail.dev pushed by claude agent, operated by paul@keymail.dev 5f8f1fccfd9f3a10d2d97dd9aac6168bc299cd45 parent 2aadfd9
2 files changed, +192 −0
  • internal/hostclaim/delete.go +97 −0
  • internal/hostclaim/delete_test.go +95 −0
diff --git a/internal/hostclaim/delete.go b/internal/hostclaim/delete.go
new file mode 100644
index 000000000..ff11f39e4
--- /dev/null
+++ b/internal/hostclaim/delete.go
@@ -0,0 +1,97 @@
+package hostclaim
+
+import (
+ "fmt"
+ "time"
+)
+
+// WriteSet is where a delete parks: the touched set, or the whole
+// deployment while discovery is incomplete (§5.2 step 3, §9).
+func WriteSet(c *Claim, allRegions, allBoxes []string) (regions, boxes []string) {
+ if c.Touched.Complete {
+ return c.Touched.Regions, c.Touched.Boxes
+ }
+ return allRegions, allBoxes
+}
+
+func BeginDelete(opID, by, recordETag string, now time.Time, allRegions, allBoxes []string) func(*Claim) error {
+ return func(c *Claim) error {
+ regions, boxes := WriteSet(c, allRegions, allBoxes)
+ if err := StartGuard(c, regions, boxes); err != nil {
+ return err
+ }
+ if opID == "" || recordETag == "" {
+ return fmt.Errorf("%w: delete needs an op id and the record's etag", ErrMalformed)
+ }
+ exec, parts := c.Op.Exec, c.Op.Participants // execution authority is kept (R6-4)
+ c.Op = Op{Kind: KindDeleting, ID: opID, By: by, StartedAt: now.UTC(), Exec: exec, Participants: parts,
+ Intent: map[string]string{"key": c.Holder.Key, "etag": recordETag}}
+ return nil
+ }
+}
+
+func ToParking(opID string, allRegions, allBoxes []string, newID func() string) func(*Claim) error {
+ return func(c *Claim) error {
+ if err := RequireOp(opID)(c); err != nil {
+ return err
+ }
+ // purging: §5.3 step 4 retires records under the purge's own claim
+ // (R4-4); Plan 5 adds the purge journal's preconditions around it.
+ if c.Op.Kind != KindDeleting && c.Op.Kind != KindPurging {
+ return fmt.Errorf("%w: kind %q", ErrWrongState, c.Op.Kind)
+ }
+ if c.Holder == nil { // a holderless purge fence (CC21) has nothing to park
+ return fmt.Errorf("%w: no holder to park", ErrWrongState)
+ }
+ regions, boxes := WriteSet(c, allRegions, allBoxes)
+ for _, r := range regions {
+ c.Obligations = append(c.Obligations, Obligation{ID: newID(), Op: opID, Kind: "park-replica", Region: r, Retirement: newID(), Owner: "region:" + r})
+ }
+ for _, b := range boxes {
+ c.Obligations = append(c.Obligations, Obligation{ID: newID(), Op: opID, Kind: "park-local", Box: b, Retirement: newID(), Owner: "box:" + b})
+ }
+ retire(c, c.Holder.Incarnation)
+ c.Op.Kind = KindParking
+ return nil
+ }
+}
+
+// CompleteObligation removes one chore, matched by its OWN op id and id.
+// Discovery chores outlive the adoption that made them (§9).
+func CompleteObligation(obligationOp, obligationID string) func(*Claim) error {
+ return func(c *Claim) error {
+ idx := -1
+ for i, o := range c.Obligations {
+ if o.ID == obligationID && o.Op == obligationOp {
+ idx = i
+ break
+ }
+ }
+ if idx < 0 {
+ return fmt.Errorf("%w: no obligation %q of op %q", ErrWrongState, obligationID, obligationOp)
+ }
+ c.Obligations = append(c.Obligations[:idx], c.Obligations[idx+1:]...)
+ if c.Touched.DiscoveryOp != "" && c.Touched.DiscoveryOp == obligationOp {
+ for _, o := range c.Obligations {
+ if o.Op == obligationOp {
+ return nil
+ }
+ }
+ c.Touched.Complete, c.Touched.DiscoveryOp = true, ""
+ }
+ return nil
+ }
+}
+
+func FreeAfterPark(opID string) func(*Claim) error {
+ return func(c *Claim) error {
+ if err := RequireOp(opID)(c); err != nil {
+ return err
+ }
+ if c.Op.Kind != KindParking || len(c.Obligations) > 0 {
+ return fmt.Errorf("%w: kind %q, %d obligations left", ErrWrongState, c.Op.Kind, len(c.Obligations))
+ }
+ c.Holder, c.Op, c.Touched, c.Inventory = nil, Op{}, Touched{}, nil
+ return nil
+ }
+}
diff --git a/internal/hostclaim/delete_test.go b/internal/hostclaim/delete_test.go
new file mode 100644
index 000000000..62ea15ecc
--- /dev/null
+++ b/internal/hostclaim/delete_test.go
@@ -0,0 +1,95 @@
+package hostclaim
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "testing"
+ "time"
+
+ "github.com/carlosframework/platform/internal/release"
+)
+
+func counter() func() string {
+ n := 0
+ return func() string { n++; return fmt.Sprintf("id_%d", n) }
+}
+
+func servingClaim(t *testing.T, ctx context.Context, s release.Store) Observed {
+ t.Helper()
+ now := time.Date(2026, 10, 4, 1, 0, 1, 0, time.UTC)
+ reg, _ := Read(ctx, s, testHost)
+ o, err := Update(ctx, s, reg, BeginCreate(newHolder(), record, "op_c1", "console", now))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if o, err = Update(ctx, s, o, CompleteCreate("op_c1", "adi-knitting")); err != nil {
+ t.Fatal(err)
+ }
+ return o // no fixture repair: CompleteCreate itself records adi-knitting in touched.boxes
+}
+
+func TestDeleteParkFree(t *testing.T) {
+ ctx := context.Background()
+ s := release.NewDirStore(t.TempDir())
+ now := time.Date(2026, 10, 4, 2, 0, 0, 0, time.UTC)
+ o := servingClaim(t, ctx, s)
+ inc, stamp := o.Claim.Holder.Incarnation, o.Claim.Holder.Stamp
+ o, err := Update(ctx, s, o, BeginDelete("op_d1", "console", `"etag1"`, now, nil, nil))
+ if err != nil || o.Claim.Op.Kind != KindDeleting || o.Claim.Op.Intent["etag"] != `"etag1"` {
+ t.Fatalf("begin delete = %+v, %v", o.Claim, err)
+ }
+ if o.Claim.Op.Exec != "adi-knitting" || o.Claim.Holder.Stamp != stamp {
+ t.Fatalf("delete dropped execution authority: exec %q stamp %d", o.Claim.Op.Exec, o.Claim.Holder.Stamp)
+ }
+ o, err = Update(ctx, s, o, ToParking("op_d1", nil, nil, counter()))
+ if err != nil || o.Claim.Op.Kind != KindParking || len(o.Claim.Obligations) != 2 {
+ t.Fatalf("to parking = %+v, %v", o.Claim, err)
+ }
+ if _, err := Update(ctx, s, o, FreeAfterPark("op_d1")); !errors.Is(err, ErrWrongState) {
+ t.Fatalf("free with chores left = %v", err)
+ }
+ for _, ob := range o.Claim.Obligations {
+ if o, err = Update(ctx, s, o, CompleteObligation(ob.Op, ob.ID)); err != nil {
+ t.Fatal(err)
+ }
+ }
+ if _, err := Update(ctx, s, o, CompleteObligation("op_d1", "id_1")); !errors.Is(err, ErrWrongState) {
+ t.Fatalf("completing a chore twice = %v", err)
+ }
+ o, err = Update(ctx, s, o, FreeAfterPark("op_d1"))
+ if err != nil || !o.Claim.Free() || o.Claim.RetiredIncarnations[0] != inc {
+ t.Fatalf("free = %+v, %v", o.Claim, err)
+ }
+}
+
+func TestIncompleteTouchedParksEverywhere(t *testing.T) {
+ ctx := context.Background()
+ s := release.NewDirStore(t.TempDir())
+ now := time.Date(2026, 10, 4, 2, 0, 0, 0, time.UTC)
+ o := servingClaim(t, ctx, s)
+ o, _ = Update(ctx, s, o, func(c *Claim) error { c.Touched.Complete = false; return nil })
+ regions, boxes := []string{"ie", "uk", "us"}, []string{"b1", "b2"}
+ o, _ = Update(ctx, s, o, BeginDelete("op_d1", "console", `"e"`, now, regions, boxes))
+ o, err := Update(ctx, s, o, ToParking("op_d1", regions, boxes, counter()))
+ if err != nil || len(o.Claim.Obligations) != 5 {
+ t.Fatalf("incomplete touched = %d obligations, %v; want 3 regions + 2 boxes", len(o.Claim.Obligations), err)
+ }
+}
+
+func TestToParkingRefusesAPurgeFence(t *testing.T) {
+ fence := &Claim{V: 2, Host: testHost, Gen: 3, Op: Op{Kind: KindPurging, ID: "op_p"}}
+ if err := ToParking("op_p", nil, nil, counter())(fence); !errors.Is(err, ErrWrongState) {
+ t.Fatalf("purge fence = %v, want ErrWrongState", err)
+ }
+}
+
+func TestRetiredCapKeepsNewest32(t *testing.T) {
+ c := &Claim{}
+ for i := 0; i < 33; i++ {
+ retire(c, fmt.Sprintf("inc%02d", i))
+ }
+ if len(c.RetiredIncarnations) != 32 || c.RetiredIncarnations[0] != "inc32" || c.RetiredIncarnations[31] != "inc01" {
+ t.Fatalf("retired = %v", c.RetiredIncarnations)
+ }
+}