plan: Plan 1 rev 5, Task 6a NewGeneration (cold recovery, spec rev 6.6)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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 |