carlos / platform Public

plan: Plan 1 rev 5, Task 6a NewGeneration (cold recovery, spec rev 6.6)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev d2e83b3be1bde255c4896d569af214db5e732396 parent b10547e
1 file changed, +110 −2
  • docs/superpowers/plans/2026-10-04-host-lifecycle-claim-1-library.md +110 −2

CI passed — run details

diff --git a/docs/superpowers/plans/2026-10-04-host-lifecycle-claim-1-library.md b/docs/superpowers/plans/2026-10-04-host-lifecycle-claim-1-library.md
index 663e36152..9a4bb4318 100644
--- a/docs/superpowers/plans/2026-10-04-host-lifecycle-claim-1-library.md
+++ b/docs/superpowers/plans/2026-10-04-host-lifecycle-claim-1-library.md
@@ -31,8 +31,8 @@ ErrCASConflict, DirStore for tests), `internal/maintenanceguard`
dependencies.
**Spec:** `docs/superpowers/specs/2026-10-02-host-lifecycle-claim-design.md`,
-rev 6.5 (d36adcfa), plus the `touched.discovery_op` note added with this
-plan. Read §2 (the object), §3 (C1-C6, C5a), §4, §5.1, §5.2, §6.4 (stamps
+rev 6.6 (b10547e1), which adds writer epochs (§6.4a, R7-1), plus the
+`touched.discovery_op` note added with this plan. Read §2 (the object), §3 (C1-C6, C5a), §4, §5.1, §5.2, §6.4 (stamps
on artifacts) and §9 before starting. Joint round 6 judged the spec ready
for implementation planning. Rev 2 of this plan answers Astra's review of
rev 1 (five P1s, four P2s and a P3).
@@ -118,6 +118,7 @@ code the earlier ones land. The moves spec builds its operations on Plan
| `internal/hostclaim/create.go` | `BeginCreate`, `CompleteCreate`, `CancelCreate` |
| `internal/hostclaim/delete.go` | `WriteSet`, `BeginDelete`, `ToParking`, `CompleteObligation`, `FreeAfterPark` |
| `internal/hostclaim/adopt.go` | `Adopt` |
+| `internal/hostclaim/generation.go` | `NewGeneration`, the cold-recovery installing CAS (§6.4a) |
| `internal/hostclaim/orphan.go` | `IsOrphan` |
| `internal/hostclaim/*_test.go` | One test file per source file, using `release.NewDirStore(t.TempDir())` |
@@ -1498,6 +1499,112 @@ git commit -m "hostclaim: one adoption routine, discovery chores and their compl
---
+### Task 6a: Cold recovery, the installing CAS (§6.4a, R7-1)
+
+**Files:**
+- Create: `internal/hostclaim/generation.go`
+- Test: `internal/hostclaim/generation_test.go`
+
+**Interfaces:**
+- Consumes: Tasks 1 to 6.
+- Produces: `NewGeneration(observedGeneration, newGeneration, execBox string) func(*Claim) error`.
+
+A box that finds replication state lost (§6.4a) calls this through
+`Update` **before admitting any write**. The transition:
+- requires a holder, `op.kind == serving`, and `op.exec == execBox`;
+- requires `holder.generation` to still equal the generation the box
+ observed, so two recoveries cannot both install;
+- requires `newGeneration` to be non-empty and different from the
+ current one;
+- writes `holder.generation = newGeneration` and
+ `holder.stamp = the resulting gen`.
+
+Generation ids are fresh op ids and never reused. The local check that
+the generation isn't one of the box's retired ones is Plan 3's.
+
+- [ ] **Step 1: Write the failing test**
+
+```go
+package hostclaim
+
+import (
+ "context"
+ "errors"
+ "testing"
+
+ "github.com/carlosframework/platform/internal/release"
+)
+
+func TestNewGenerationInstallsOnce(t *testing.T) {
+ ctx := context.Background()
+ s := release.NewDirStore(t.TempDir())
+ o := servingClaim(t, ctx, s)
+ before := o.Claim.Holder.Stamp
+ o2, err := Update(ctx, s, o, NewGeneration("", "gen_a", "adi-knitting"))
+ if err != nil || o2.Claim.Holder.Generation != "gen_a" || o2.Claim.Holder.Stamp != o2.Claim.Gen || o2.Claim.Holder.Stamp <= before {
+ t.Fatalf("new generation = %+v, %v", o2.Claim.Holder, err)
+ }
+ // A second recovery that observed the old generation must not install.
+ if _, err := Update(ctx, s, o2, NewGeneration("", "gen_b", "adi-knitting")); !errors.Is(err, ErrWrongState) {
+ t.Fatalf("stale recovery = %v, want ErrWrongState", err)
+ }
+ if _, err := Update(ctx, s, o2, NewGeneration("gen_a", "gen_c", "some-other-box")); !errors.Is(err, ErrWrongState) {
+ t.Fatalf("recovery by a non-executor = %v, want ErrWrongState", err)
+ }
+ if _, err := Update(ctx, s, o2, NewGeneration("gen_a", "gen_a", "adi-knitting")); !errors.Is(err, ErrWrongState) {
+ t.Fatalf("same-generation restart = %v, want ErrWrongState", err)
+ }
+}
+```
+
+- [ ] **Step 2: Run the test to verify it fails**
+
+Run: `go test ./internal/hostclaim/ -run TestNewGeneration -count=1`
+Expected: FAIL, with "undefined: NewGeneration".
+
+- [ ] **Step 3: Write the minimal implementation**
+
+```go
+package hostclaim
+
+import "fmt"
+
+// NewGeneration is §6.4a's cold recovery: an installing CAS that gives a
+// writer whose predecessor may still have uploads in flight a fresh
+// generation, and with it a fresh stamp (§2, R6-4). It is never a
+// same-generation restart.
+func NewGeneration(observedGeneration, newGeneration, execBox string) func(*Claim) error {
+ return func(c *Claim) error {
+ if c.Holder == nil || c.Op.Kind != KindServing || c.Op.Exec == "" || c.Op.Exec != execBox {
+ return fmt.Errorf("%w: only the serving executor may start a generation", ErrWrongState)
+ }
+ if c.Holder.Generation != observedGeneration {
+ return fmt.Errorf("%w: generation moved since it was observed", ErrWrongState)
+ }
+ if newGeneration == "" || newGeneration == c.Holder.Generation {
+ return fmt.Errorf("%w: a cold recovery needs a new generation", ErrWrongState)
+ }
+ c.Holder.Generation = newGeneration
+ c.Holder.Stamp = c.Gen + 1 // the resulting gen
+ return nil
+ }
+}
+```
+
+- [ ] **Step 4: Run the test to verify it passes**
+
+Run: `go test ./internal/hostclaim/ -run TestNewGeneration -count=1`
+Expected: PASS.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add internal/hostclaim/generation.go internal/hostclaim/generation_test.go
+git commit -m "hostclaim: cold recovery installs a new generation and stamp (§6.4a, R7-1)"
+```
+
+---
+
### Task 7: The orphan rule (§5.1 step 5, R3-2, CC17, Q2)
**Files:**
@@ -1631,6 +1738,7 @@ git commit -m "hostclaim: repository gate" || true
| Round 3: executor recorded as touched (P1) | Task 4 `CompleteCreate`; Task 5's fixture no longer repairs it |
| Round 3: sidecar name and region in the record body (P2) | Task 4 `recordMatches` and `TestBeginCreateSidecar` |
| Round 3: obligation validation (P2) | Task 1 `Validate`, with malformed reads in Task 2 |
+| Spec rev 6.6 writer epochs (R7-1) | Task 6a `NewGeneration`. Clean-close markers and generation-bound clients come in Plans 2 and 3 |
**Types used across tasks:** `Observed{Host,…}`, `Update`, `RequireOp`,
`StartGuard`, `WriteSet`, `NewHolder`, `keyMatches`, `retire` and