carlos / platform Public

docs: stage B plan rev 5.4.1 answers Astra's Task 10b round 1: the source's hold-only reconcile checks its own installation at its row's stamp, so a first pass after installing still holds the row

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev e236acd5e384d60514b9c7ae923325e2ef5b2282 parent 1d6e75a
1 file changed, +527 −25
  • docs/superpowers/plans/2026-10-10-region-ha-1b-next-stage-b.md +527 −25
diff --git a/docs/superpowers/plans/2026-10-10-region-ha-1b-next-stage-b.md b/docs/superpowers/plans/2026-10-10-region-ha-1b-next-stage-b.md
index e8d0a8b5a..e77f846d0 100644
--- a/docs/superpowers/plans/2026-10-10-region-ha-1b-next-stage-b.md
+++ b/docs/superpowers/plans/2026-10-10-region-ha-1b-next-stage-b.md
@@ -1,4 +1,4 @@
-# Region HA 1b-next Stage B: Relocation Wired Into the Claim Assembly Implementation Plan (rev 5.4)
+# Region HA 1b-next Stage B: Relocation Wired Into the Claim Assembly Implementation Plan (rev 5.4.1)
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
@@ -20,6 +20,12 @@
---
+## Rev 5.4.1: Astra round 1 on Task 10b
+
+| # | Finding | Fix in rev 5.4.1 | Where |
+|---|---|---|---|
+| 1 (P2) | A source whose fence closed but whose next reconcile ran after `ToInstalling` had advanced `holder.stamp` was refused by `EmptyInstallationCensus.CheckLocal` (it looks for the target's stamp on the source, `ErrTypedInventory`) before `holdOnly`, so its row stayed `ready` for good. The tests stubbed `CheckLocal`. | Ruling (Astra, binding): the hold-only path checks the source's own existing installation, never the target's stamp, and grants nothing. `Reconciler.CheckHeld` replaces `CheckLocal` on the hold-only path; `EmptyInstallationCensus.CheckHeld` reads the row and its installation's inventory at the row's own stamp (this box, this enrollment, the route entry naming the row's owner and identity); `ReconcileAdapter.Install` sets it from its census; nil, a missing or a contradicting inventory refuses. Tests through the real adapter, census and provision pass, with the first post-fence reconcile delayed to `installing`: `TestReconcileHoldsTheSourceWhenItsFirstPassAfterTheFenceIsAtInstalling`, `TestCensusCheckHeldRefusesAContradictingOrMissingSourceInventory`; mutation-checked (the task's record). | 10b (decision 6) |
+
## Rev 5.4: Task 10b written
Task 10b (R5-3, the gate of ruling 79 and checklist item 18) is no longer a stub: its Files, Interfaces, decisions, complete code and test bodies, `-run` filters and commit are written against the branch after c9b51c47, and its code was compiled and its tests run in a throwaway copy (the record in the task). With `CARLOS_REGION_HA` on, the box's reconciler holds the source's row for the op in `relocating` once the host guard's admission fence for the host closes in `releasing`, through the existing `registry.SetHoldIf` under the `hold_op` guard; the completion queues the source's park-local, whose retirement ends the hold; a failover's fenced source is left to its park-local; with the flag off reconcile is main's. No other task changed. The task is written, not landed: the activation gate stands until it lands and its tests pass.
@@ -135,7 +141,7 @@ Each task was committed in a throwaway copy of this branch (code as at b9a753f0;
| 8d | clean | clean: host | empty | the owner, wake, attempt and root tests pass, with the moves probe-owner, Plan 9 D5, Plan 11 and claim-launch regressions |
| 9 | clean | clean: host, hostbox | empty | hostbox's reconcile tests and host's probe-stop (restart, failed marker write), hold-release, successor, discharge, handover, sweep, Plan 9, Plan 11 and handover regressions pass |
| 10 | clean | clean: host, hostrelocate, edgesfile, console | empty | `hostrelocate` and `edgesfile` pass in full; the console's edges, eligibility, placement, region, executor and claim-runtime tests pass; 82 region HA host tests pass with the Plan 9, Plan 11, claim-launch, moves and handover regressions |
-| 10b (rev 5.4, at 42093182) | clean | clean: host, hostbox, registry | empty | `hostbox`, `registry` and `host` pass in full; the six Task 10b tests pass; darwin, windows and linux/arm64 builds clean; each guard mutation-checked (the task's record) |
+| 10b (rev 5.4.1, at 1d6e75a5) | clean | clean: host, hostbox, registry | empty | `hostbox`, `registry` and `host` pass in full; the eight Task 10b tests pass, two through the production adapter and census; darwin, windows and linux/arm64 builds clean; each guard mutation-checked (the task's record) |
At Task 10 the whole `internal/hostclaim`, `internal/hostops`, `internal/hostrelocate`, `internal/hostbox` and `internal/host` (47s) packages also pass, once, outside the sandbox (rev 5.3). The whole `internal/activator` package passed at rev 5.1 and rev 5.2 does not touch it; the console's passed at rev 5 and neither revision since touches it. At Task 10, `GOOS=darwin`, `GOOS=windows` and `GOARCH=arm64` builds of `./...` are clean, and so is `go vet` of host, hostrelocate, hostclaim, hostops and edgesfile for all three (and, at rev 5.1, of activator for darwin). Two windows vets fail at b9a753f0 already and this plan does not touch either cause: `./internal/hostbox/` (`plan9_fix2_minors_test.go:100`, `undefined: plan9D1Probe`) and `./internal/activator/` (`activator_test.go:4528`, `undefined: unix.Mkfifo`).
@@ -14828,11 +14834,12 @@ Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
### Task 10b: The source's row held at its fence's close (R5-3), mandatory before activation
-Astra round 6 accepted R5-3 deferred while relocation is dormant, on one condition: this task lands, with its tests passing, before relocation is switched on anywhere (ruling 79; checklist item 18; the activation line in Global Constraints; "After this plan"). Nothing in Tasks 8a to 11 depends on it. Rev 5.4 writes it against the branch as it is after c9b51c47; its code was compiled and its tests run in a throwaway copy (the record below).
+Astra round 6 accepted R5-3 deferred while relocation is dormant, on one condition: this task lands, with its tests passing, before relocation is switched on anywhere (ruling 79; checklist item 18; the activation line in Global Constraints; "After this plan"). Nothing in Tasks 8a to 11 depends on it. Rev 5.4 writes it against the branch as it is after c9b51c47; rev 5.4.1 answers Astra's round 1 on it (`CODEX-PLAN-REGION-HA-TASK-10B-R1.md`). Its code was compiled and its tests run in a throwaway copy (the record below).
**What it does.** Stage A's `hostbox.Reconciler` has two hooks no production assembly set: `FenceClosed(ctx, obs) (bool, error)` and `HoldRow(ctx, host, holdOp) error` (hostbox/reconcile.go). With both nil, reconcile under a relocating claim is main's `Serving` path, which refuses and leaves every row unchanged, so the source's row stayed ready through `releasing` and the edge kept routing to a source whose admission was closed. This task sets both, with `CARLOS_REGION_HA` on only, where the box's reconciler is built (`OpenProductionClaimAssembly`, whose adapter `ClaimAssembly.Build` copies). Stage A's row decision (`RelocationRow`, `rowDecision`, `holdOnly`) is unchanged: the source in `releasing` stays ready until this box's admission fence for the host closes, and then it is held for the op; every other relocating participant keeps an existing row held and mints none.
- `FenceClosed` is `hostbox.AdmissionFenceClosed(guard)`: the host guard's admission fence for the installation the guard record has active, read exactly as `With` reads it (`hostguard.Guard.AdmissionFenced` over `hostguard.Capture`). An unreadable or missing record, or a claim for another host or incarnation, is an error, never "open"; the reconciler then keeps every row and mints none.
+- The hold-only path checks this box's own existing installation, not the claim holder's (rev 5.4.1, Astra round 1, binding): `Reconciler.CheckHeld` takes `CheckLocal`'s place there, and `ReconcileAdapter.Install` sets it to `EmptyInstallationCensus.CheckHeld`. Without it, a source whose first reconcile after its fence closed ran once `ToInstalling` had moved `holder.stamp` to the target's install stamp was refused by `CheckLocal` (it looks for that stamp's inventory on the source, finds the source's row, and answers `ErrTypedInventory`, empty_census.go:58) before `holdOnly`, and no later pass repaired it.
- `HoldRow` is `hostbox.RegistryHoldRow(reg)`: `registry.SetHoldIf(host, "relocating", op, "")`, status and `hold_op` in one statement under the `hold_op` guard. A row already held for the op is a replay and nothing is written; a row another op holds is `ErrChanged` with nothing written; no row is `ErrChanged` and nothing is minted; an unreadable row is its error.
**Decisions** (each verified against the code at HEAD; none reopens a ruling):
@@ -14842,26 +14849,28 @@ Astra round 6 accepted R5-3 deferred while relocation is dormant, on one conditi
3. **Nothing releases the source's hold.** The fenced installation never serves again. The completion (`hostclaim.CompleteRelocation`) queues the live source's `park-local`, and the park-local's typed retirement (`hostbox.RegistryRows` to `registry.RetireClaimRows`) deletes the row by ownership whether it is held or not; that ends the hold. `relocationReleaseHold` is unchanged: it releases the completion box's own row (the target's), the source is never the completion box (`StartRelocation` and `replaceTargetSlot` refuse a target that is the source), and a source branch there would be a second writer releasing a relocating hold, which ruling 80's argument relies on not existing. After a failover from a fenced source the source is on `lost`, so `RelocationRow` never gives it a held row: reconcile neither asks its fence nor writes its row, before or after the completion, and the row is left to the park-local. *Cost if wrong:* until the source's park-local runs (it waits on M2 after a converted landing, checklist item 8), the source keeps a held, unserved row while the host serves on the target, as a move's source does.
4. **The stage-requested marker is not touched.** `SetHold`'s comment asks every caller to touch it so root re-judges a held release stage (moves' G10). Relocation stages nothing under the source's hold, and Task 8c's held mint does not touch it either. *Cost if wrong:* root re-judges the source's staged release at its next timer pass rather than at once; the source serves nothing meanwhile.
5. **The hooks follow `CARLOS_REGION_HA` alone**, not whether the relocation owners are bound (ruling 53). With the flag on and C8 unbound this box starts nothing, but a worker elsewhere may still drive a relocation it is the source of, and holding its row then is right. Every assembly mode gets them, but only the agent runs provision's pass with the adapter (agent.go, `instancesLoop` and `fastConvergeLoop`), so root's assemblies never call them. *Cost if wrong:* if the flag is switched off in the one provision pass between the source's fence closing and the hold landing, the row stays ready until the op completes and the park-local retires it; with the flag off the source never runs its release, so its fence does not close then.
+6. **The hold-only local check is the census's `CheckHeld`, at the row's own stamp** (rev 5.4.1; Astra round 1's ruling: holding is not installing). It reads the host's row and the inventory of the installation the row's ownership names: complete, this host, incarnation and stamp, this box and enrollment, and exactly one route entry naming the row's owner and row identity, materialized. It never reads `holder.stamp`, writes nothing and grants nothing. No row is `ErrChanged`; a row without valid ownership, a missing inventory or a contradicting one is `ErrTypedInventory`; a read failure is its error; a nil `CheckHeld` refuses the hold. It lives on the census beside `CheckLocal` (one owner of the inventory checks) and the adapter sets it, so every production reconcile through the adapter gets it. The directory revision is not compared: it records which roster admitted the installation, and the hold needs only that the installation is this box's. *Cost if wrong:* if the directory revision should bind, a source whose inventory was recorded under an older roster revision is still held; a hold only stops serving.
**Files:**
-- Create: `internal/hostbox/relocationsource.go`, `internal/host/relocationsourcehold.go`, `internal/hostbox/reconcile_relocation_source_test.go`, `internal/host/relocationsourcehold_test.go`, `internal/host/relocationsourcehold_linux_test.go`
-- Modify: `internal/host/claimconstruction.go` (the call), `internal/hostbox/reconcile.go` (comments only: who sets the hooks and which write `holdOnly` makes), `internal/registry/relocating_test.go` (the `SetHoldIf` contract this task relies on)
+- Create: `internal/hostbox/relocationsource.go`, `internal/host/relocationsourcehold.go`, `internal/hostbox/reconcile_relocation_source_test.go`, `internal/hostbox/reconcile_relocation_source_census_test.go`, `internal/host/relocationsourcehold_test.go`, `internal/host/relocationsourcehold_linux_test.go`
+- Modify: `internal/hostbox/reconcile.go` (`CheckHeld` and its use on the hold-only path; comments on who sets the hooks), `internal/hostbox/empty_census.go` (`CheckHeld`), `internal/hostbox/reconcile_adapter.go` (sets `CheckHeld` from its census), `internal/host/claimconstruction.go` (the call), `internal/hostbox/reconcile_relocation_test.go` (stage A's fixture gains a `CheckHeld`, as it has a `CheckLocal`), `internal/registry/relocating_test.go` (the `SetHoldIf` contract this task relies on)
- Not modified, and why: `internal/registry/registry.go` (decision 2), `internal/host/relocationowners.go` (decision 3).
**Interfaces:**
-- Consumes: `hostbox.Reconciler` (`FenceClosed`, `HoldRow`, `Install`, `holdOnly`, `rowDecision`, `RelocationRow`), `hostbox.ReconcileAdapter`, `hostbox.RegistryRows`, `hostguard.Guard.AdmissionFenced`, `hostguard.Capture`, `hostguard.Guard.BeginFence`, `registry.SetHoldIf`, `registry.Get`, `registry.IsHold`, `registry.RetireClaimRows`, `hostclaim.CompleteRelocation`, `hostclaim.OwesLocal`, `regionHAEnabled`, `claimEntryEnv`, `OpenProductionClaimAssembly`; the test fixture `fix4JournalFixture` (plan9fix4_claimjournal_migration_linux_test.go).
-- Produces: `hostbox.AdmissionFenceClosed`, `hostbox.RegistryHoldRow`, `relocationSourceRows`.
+- Consumes: `hostbox.Reconciler` (`FenceClosed`, `HoldRow`, `CheckLocal`, `Install`, `checkOn`, `holdOnly`, `rowDecision`, `RelocationRow`), `hostbox.ReconcileAdapter` (`Install`, `Census`), `hostbox.EmptyInstallationCensus`, `hostbox.StopInventoryDB.ReadInstallation`, `hostbox.RegistryRows`, `hostguard.Guard.AdmissionFenced`, `hostguard.Capture`, `hostguard.Guard.BeginFence`, `registry.SetHoldIf`, `registry.Get`, `registry.IsHold`, `registry.RetireClaimRows`, `provision.Reconcile`, `hostclaim.CompleteRelocation`, `hostclaim.OwesLocal`, `hostclaim.AdmitWriter`, `regionHAEnabled`, `claimEntryEnv`, `OpenProductionClaimAssembly`; the test fixtures `newAdmissionFixture` (admission_refusal_test.go), `targetGuardFiles` (reconcile_relocation_target_test.go) and `fix4JournalFixture` (plan9fix4_claimjournal_migration_linux_test.go).
+- Produces: `hostbox.AdmissionFenceClosed`, `hostbox.RegistryHoldRow`, `hostbox.EmptyInstallationCensus.CheckHeld`, `hostbox.Reconciler.CheckHeld`, `relocationSourceRows`.
- [ ] **Step 1: Write the failing tests**
-Four required tests, one per stub item, plus the registry contract and the call site:
+Four required tests, one per stub item, plus the delayed source reconcile, the registry contract and the call site:
-1. `TestReconcileHoldsTheSourcesRowAtItsFencesClose` (hostbox): before the fence closes the row is unchanged (dry run and real); an unreadable fence is the read's error with the row unchanged; a dry run after the close writes nothing; then the row is `relocating`, held for `rel-1`, every other column as it was.
+1. `TestReconcileHoldsTheSourcesRowAtItsFencesClose` (hostbox): before the fence closes the row is unchanged (dry run and real); an unreadable fence is the read's error with the row unchanged; a dry run after the close writes nothing; with no `CheckHeld` nothing is held; then the row is `relocating`, held for `rel-1`, every other column as it was.
2. `TestReconcileSourceHoldNeverTakesAnotherOpsRow` (hostbox) and `TestSetHoldIfRelocatingNeverTakesAnotherOpsRow` (registry): another op's row is never taken, a replay writes nothing, no row is minted, an unreadable row is an error; the held row keeps its ownership and the park-local's `RetireClaimRows` deletes it.
3. `TestRelocationSourceHoldEndsWithTheCompletionOrTheParkLocal` (host): the held row through the real `CompleteRelocation`, reconcile leaving it, then the park-local's retirement; a failover from a fenced source, with the row ready or already held when fenced, left as it was during the failover and after its completion, then retired.
4. `TestRelocationSourceHoldFollowsTheFlag` (host): with `CARLOS_REGION_HA` unset, `0`, `false`, `off` or unparseable, no hook is set and a fenced source in `releasing` is refused with the row, the claim and the guard record unchanged, while the serving executor still writes; with `1` or `true` the same box holds the row. `TestProductionClaimAssemblySetsTheSourceHoldOnlyWithRegionHA` (host, linux) opens the production assembly and checks the hooks are set with the flag on and not with it off.
+5. Astra round 1 (rev 5.4.1), through the production `ReconcileAdapter`, the real census, `park.Local` and provision's pass, no stubbed local check: `TestReconcileHoldsTheSourceWhenItsFirstPassAfterTheFenceIsAtInstalling` (hostbox) mints the source's row with a serving pass, closes its fence in `releasing`, moves the claim to `installing` with `holder.stamp` at the target's install stamp, and only then runs the first pass: the row is held, its ownership, the guard record and the claim unchanged, and a replay changes nothing. `TestCensusCheckHeldRefusesAContradictingOrMissingSourceInventory` (hostbox): `CheckHeld` accepts the source's own installation where `CheckLocal` refuses, and refuses another box, another enrollment, no inventory, a row owned by another row identity, another incarnation, no row and an unreadable registry; through the pass, a contradicting inventory leaves the row exactly as it was.
-The registry test passes before this task's code (it pins the existing `SetHoldIf`); the others fail to compile.
+The registry test passes before this task's code (it pins the existing `SetHoldIf`); the others fail to compile. Before `CheckHeld` existed, the first test of item 5 failed with the row `ready` and provision logging `claim install shop.example: hostbox: invalid typed retirement inventory`: Astra's finding, reproduced.
`internal/hostbox/reconcile_relocation_source_test.go` (new):
@@ -14939,8 +14948,9 @@ func rowOf(t *testing.T, reg *registry.Registry, host string) *registry.Route {
// until this box's admission fence for the host closes; reconcile then
// holds it for the op in "relocating", a status the edge never serves,
// with every other column as it was, its claim ownership included. A dry
-// run writes nothing, and an unreadable fence is an error with the row
-// left as it was, never "open".
+// run writes nothing, an unreadable fence is an error with the row left
+// as it was, never "open", and with no hold-only local check (CheckHeld)
+// nothing is held.
func TestReconcileHoldsTheSourcesRowAtItsFencesClose(t *testing.T) {
r, selected, reg, guard := sourceRows(t)
ctx := context.Background()
@@ -14983,6 +14993,17 @@ func TestReconcileHoldsTheSourcesRowAtItsFencesClose(t *testing.T) {
if got := rowOf(t, reg, selected.Host); *got != before {
t.Fatalf("a dry run held the row: %+v", got)
}
+ // The hold-only path's local check is required: without it nothing
+ // is held.
+ checkHeld := r.CheckHeld
+ r.CheckHeld = nil
+ if err := r.Install(ctx, selected, func() error { writes++; return nil }); !errors.Is(err, ErrUnsupported) {
+ t.Fatalf("no hold-only check: %v", err)
+ }
+ if got := rowOf(t, reg, selected.Host); *got != before {
+ t.Fatalf("a hold without its local check: %+v", got)
+ }
+ r.CheckHeld = checkHeld
if err := r.Install(ctx, selected, func() error { writes++; return nil }); err != nil {
t.Fatalf("the fenced source's hold: %v", err)
}
@@ -15056,6 +15077,277 @@ func TestReconcileSourceHoldNeverTakesAnotherOpsRow(t *testing.T) {
}
```
+`internal/hostbox/reconcile_relocation_source_census_test.go` (new):
+
+```go
+//go:build unix
+
+package hostbox
+
+import (
+ "context"
+ "errors"
+ "path/filepath"
+ "strconv"
+ "testing"
+ "time"
+
+ "github.com/carlosframework/platform/internal/hostclaim"
+ "github.com/carlosframework/platform/internal/hostguard"
+ "github.com/carlosframework/platform/internal/hostwrite"
+ "github.com/carlosframework/platform/internal/park"
+ "github.com/carlosframework/platform/internal/provision"
+ "github.com/carlosframework/platform/internal/registry"
+ "github.com/carlosframework/platform/internal/release"
+)
+
+// sourceReconcile is the box "box" as a relocation's source, built as the
+// claim assembly builds it: the production ReconcileAdapter with the real
+// census and park.Local, provision's pass, and Task 10b's production
+// hooks. Its row is minted by a serving pass, then one cold writer session
+// is admitted at the same stamp.
+type sourceReconcile struct {
+ f admissionFixture
+ reg *registry.Registry
+ inv *StopInventoryDB
+ census *EmptyInstallationCensus
+ guard hostguard.Guard
+ adapter ReconcileAdapter
+ cfg provision.ReconcileConfig
+}
+
+func newSourceReconcile(t *testing.T) *sourceReconcile {
+ t.Helper()
+ s := &sourceReconcile{f: newAdmissionFixture(t, false)}
+ var err error
+ if s.reg, err = registry.Open(filepath.Join(t.TempDir(), "registry.db")); err != nil {
+ t.Fatal(err)
+ }
+ t.Cleanup(func() { s.reg.Close() })
+ if s.inv, err = OpenStopInventoryDB(filepath.Join(t.TempDir(), "inventory.db")); err != nil {
+ t.Fatal(err)
+ }
+ t.Cleanup(func() { s.inv.Close() })
+ launches, err := OpenLaunchJournal(filepath.Join(t.TempDir(), "launches.db"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ t.Cleanup(func() { launches.Close() })
+ s.census = &EmptyInstallationCensus{Inventory: s.inv, Registry: s.reg, Launches: launches, Box: "box", Enrollment: "en", DirectoryRevision: "1",
+ Roots: map[string]string{"data": s.f.data, "opt": t.TempDir(), "units": t.TempDir()}, SharedPaths: map[string][]string{"data": {"hostguard"}}}
+ s.guard = hostguard.Guard{DataDir: s.f.data, LockDir: t.TempDir(), Claims: hostguard.StoreReader{Store: s.f.store}}
+ source := WorkSource{Guard: s.guard, Box: "box", Region: "ie", Now: time.Now,
+ Policy: func(context.Context, *hostclaim.Claim, Selection, hostwrite.Identity) error { return nil },
+ Local: func(context.Context, *hostclaim.Claim, Selection) error { return nil }}
+ s.adapter = ReconcileAdapter{Registry: s.reg, Inventory: s.inv, Census: s.census, Box: "box", Enrollment: "en", DirectoryRevision: "1",
+ Reconciler: Reconciler{Claims: s.f.store, Box: "box", Region: "ie", Guard: s.guard, Writes: hostwrite.Config{Enabled: true, Selector: source},
+ Local: park.Local{Guard: s.guard, Claims: s.f.store, DataDir: s.f.data, Box: "box"},
+ Now: func() time.Time { return time.Unix(100, 0) }, CheckRecord: func(context.Context, RecordSelection) error { return nil }, CheckLocal: s.census.CheckLocal,
+ FenceClosed: AdmissionFenceClosed(s.guard), HoldRow: RegistryHoldRow(s.reg)}}
+ s.cfg = provision.ReconcileConfig{DataDir: s.f.data, Region: "ie"}
+ s.cfg.Layout = func(context.Context, release.Store) (string, error) { return release.LayoutModeNested, nil }
+ s.adapter.Configure(&s.cfg)
+ s.pass(t)
+ if row := s.row(t); row == nil || row.Status != "ready" || row.HoldOp != "" || !row.ClaimOwnership.Valid() {
+ t.Fatalf("the serving pass minted %+v", row)
+ }
+ s.update(t, func(c *hostclaim.Claim) error {
+ return hostclaim.AdmitWriter(hostclaim.Admission{Box: "box", Mode: "cold", NewGeneration: "g1", Region: "ie", KeepStamp: true})(c)
+ })
+ return s
+}
+
+func (s *sourceReconcile) pass(t *testing.T) {
+ t.Helper()
+ if err := s.f.pass(t.Context(), s.f.store, s.reg, s.cfg); err != nil {
+ t.Fatal(err)
+ }
+}
+
+func (s *sourceReconcile) row(t *testing.T) *registry.Route {
+ t.Helper()
+ row, err := s.reg.Get(s.f.host)
+ if err != nil {
+ t.Fatal(err)
+ }
+ return row
+}
+
+func (s *sourceReconcile) update(t *testing.T, change func(*hostclaim.Claim) error) hostclaim.Observed {
+ t.Helper()
+ o, err := hostclaim.Read(t.Context(), s.f.store, s.f.host)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if o, err = hostclaim.Update(t.Context(), s.f.store, o, change); err != nil {
+ t.Fatal(err)
+ }
+ return o
+}
+
+// releasingThenInstalling moves the claim through a drain from "box" to
+// "dst": releasing, root's release stop fencing the source's admission
+// (BeginFence), then the installing CAS, which advances holder.stamp to
+// the target's install stamp. No reconcile runs on the source in between.
+func (s *sourceReconcile) releasingThenInstalling(t *testing.T) hostclaim.Observed {
+ t.Helper()
+ ctx := t.Context()
+ o := s.update(t, func(c *hostclaim.Claim) error {
+ c.Op = hostclaim.Op{Kind: hostclaim.KindRelocating, ID: "rel-1", Phase: hostclaim.PhaseReleasing, Exec: "box", Participants: []string{"box", "dst"},
+ Intent: map[string]string{"region": "ie", "reason": "drain", "source_box": "box", "target_box": "dst"}, Journal: map[string]string{"exec_ack.box": "{}", "exec_ack.dst": "{}"}}
+ return nil
+ })
+ captured, err := hostguard.Capture(s.f.data, s.f.host)
+ if err != nil {
+ t.Fatal(err)
+ }
+ target := hostguard.FenceTarget{FenceID: "fence-1", Host: s.f.host, Incarnation: captured.Incarnation, Stamp: captured.Stamp, Session: o.Claim.Holder.Session,
+ Op: "rel-1", Phase: hostclaim.PhaseReleasing, Box: "box", Enrollment: "en", BootID: "boot", ControllerID: "ctl", ClaimETag: o.ETag, DirectoryRevision: "1"}
+ if _, err := s.guard.BeginFence(ctx, target); err != nil {
+ t.Fatal(err)
+ }
+ return s.update(t, func(c *hostclaim.Claim) error {
+ c.Holder.Stamp = c.Gen + 1
+ c.Op.Phase, c.Op.Exec = hostclaim.PhaseInstalling, "dst"
+ c.Op.Journal["install_stamp"] = strconv.FormatUint(c.Holder.Stamp, 10)
+ return nil
+ })
+}
+
+// Astra Task 10b round 1 (P2; rev 5.4.1): the source's fence closes in
+// releasing, but its first reconcile after that runs only once the claim
+// is at installing, with holder.stamp at the target's install stamp, which
+// this box never had. Through the production adapter, census and
+// provision pass, the source's row is held all the same, its ownership
+// unchanged: the hold-only path checks the source's own installation (the
+// row's stamp and its inventory), never the holder's stamp, and grants
+// nothing, so the guard record and the claim are unchanged. A replay
+// changes nothing.
+func TestReconcileHoldsTheSourceWhenItsFirstPassAfterTheFenceIsAtInstalling(t *testing.T) {
+ s := newSourceReconcile(t)
+ minted := *s.row(t)
+ o := s.releasingThenInstalling(t)
+ if o.Claim.Holder.Stamp == minted.ClaimStamp {
+ t.Fatal("the installing CAS kept the source's stamp")
+ }
+ guard := targetGuardFiles(t, s.f.data)
+ for i := 0; i < 2; i++ {
+ s.pass(t)
+ row := s.row(t)
+ if row == nil || row.Status != "relocating" || row.HoldOp != "rel-1" || row.ClaimOwnership != minted.ClaimOwnership {
+ t.Fatalf("pass %d at installing: the source's row is %+v (minted %+v)", i, row, minted)
+ }
+ held := *row
+ held.Status, held.HoldOp = minted.Status, minted.HoldOp
+ if held != minted {
+ t.Fatalf("pass %d: the hold changed more than status and hold_op: %+v", i, row)
+ }
+ if got := targetGuardFiles(t, s.f.data); got[s.f.host+".json"] != guard[s.f.host+".json"] {
+ t.Fatalf("pass %d: the guard record changed", i)
+ }
+ after, err := hostclaim.Read(t.Context(), s.f.store, s.f.host)
+ if err != nil || after.ETag != o.ETag {
+ t.Fatalf("pass %d: the claim was written: %v", i, err)
+ }
+ }
+}
+
+// The hold-only check (EmptyInstallationCensus.CheckHeld) accepts the
+// source's own existing installation at installing, where main's
+// CheckLocal refuses (it asks for the target's stamp). Absence is never
+// evidence: an inventory of another box or enrollment, no inventory, a
+// row owned by another row identity, another incarnation, no row and an
+// unreadable registry all refuse. Through the pass, a contradicting
+// inventory leaves the row exactly as it was.
+func TestCensusCheckHeldRefusesAContradictingOrMissingSourceInventory(t *testing.T) {
+ ctx := t.Context()
+ s := newSourceReconcile(t)
+ minted := *s.row(t)
+ o := s.releasingThenInstalling(t)
+ selected := RecordSelection{Host: s.f.host, Incarnation: o.Claim.Holder.Incarnation}
+ if err := s.census.CheckHeld(ctx, o, selected); err != nil {
+ t.Fatalf("the source's own installation: %v", err)
+ }
+ if err := s.census.CheckLocal(ctx, o, selected); !errors.Is(err, ErrTypedInventory) {
+ t.Fatalf("main's CheckLocal at installing: %v (it asks for the target's stamp)", err)
+ }
+ empty, err := OpenStopInventoryDB(filepath.Join(t.TempDir(), "inventory.db"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ t.Cleanup(func() { empty.Close() })
+ otherRows, err := registry.Open(filepath.Join(t.TempDir(), "registry.db"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ t.Cleanup(func() { otherRows.Close() })
+ stranger := minted
+ stranger.ClaimRowID = "another-row"
+ if err := otherRows.Upsert(stranger); err != nil {
+ t.Fatal(err)
+ }
+ for name, tc := range map[string]struct {
+ change func(*EmptyInstallationCensus, *RecordSelection)
+ want error
+ }{
+ "another box": {func(c *EmptyInstallationCensus, _ *RecordSelection) { c.Box = "dst" }, ErrTypedInventory},
+ "another enrollment": {func(c *EmptyInstallationCensus, _ *RecordSelection) { c.Enrollment = "en-2" }, ErrTypedInventory},
+ "no inventory": {func(c *EmptyInstallationCensus, _ *RecordSelection) { c.Inventory = empty }, ErrTypedInventory},
+ "another row id": {func(c *EmptyInstallationCensus, _ *RecordSelection) { c.Registry = otherRows }, ErrTypedInventory},
+ "another incarnation": {func(_ *EmptyInstallationCensus, sel *RecordSelection) { sel.Incarnation = "inc-other" }, ErrChanged},
+ } {
+ c, sel := *s.census, selected
+ tc.change(&c, &sel)
+ if err := c.CheckHeld(ctx, o, sel); !errors.Is(err, tc.want) {
+ t.Fatalf("%s: %v, want %v", name, err, tc.want)
+ }
+ }
+ noRow, err := registry.Open(filepath.Join(t.TempDir(), "registry.db"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ t.Cleanup(func() { noRow.Close() })
+ c := *s.census
+ c.Registry = noRow
+ if err := c.CheckHeld(ctx, o, selected); !errors.Is(err, ErrChanged) {
+ t.Fatalf("no row: %v", err)
+ }
+ closed, err := registry.Open(filepath.Join(t.TempDir(), "registry.db"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ closed.Close()
+ c.Registry = closed
+ if err := c.CheckHeld(ctx, o, selected); err == nil || errors.Is(err, ErrChanged) || errors.Is(err, ErrTypedInventory) {
+ t.Fatalf("an unreadable registry: %v (want the read's error)", err)
+ }
+ // Through the pass: a contradicting inventory refuses the hold.
+ contradicting := *s.census
+ contradicting.Enrollment = "en-2"
+ s.adapter.Census = &contradicting
+ s.adapter.Configure(&s.cfg)
+ s.pass(t)
+ if row := s.row(t); *row != minted {
+ t.Fatalf("a contradicting inventory: the row is %+v (was %+v)", row, minted)
+ }
+}
+```
+
+Stage A's fixture (`relocationReconciler`) gains the hold-only check beside its `CheckLocal` stub, so its hold-only tests keep running.
+
+`internal/hostbox/reconcile_relocation_test.go`, replace:
+
+```go
+ CheckLocal: func(context.Context, hostclaim.Observed, RecordSelection) error { return nil },
+```
+
+with:
+
+```go
+ CheckLocal: func(context.Context, hostclaim.Observed, RecordSelection) error { return nil },
+ CheckHeld: func(context.Context, hostclaim.Observed, RecordSelection) error { return nil },
+```
+
`internal/registry/relocating_test.go`: add `"context"` to the imports, and append:
```go
@@ -15211,7 +15503,8 @@ func newSourceHoldBox(t *testing.T, getenv func(string) string) *sourceHoldBox {
b.selected = hostbox.RecordSelection{Host: sourceHoldHost, Key: h.Key, ETag: "record-etag", Incarnation: h.Incarnation, Family: h.Family, Layout: "flat", Paths: []string{"shop.example.db"}}
b.r = hostbox.Reconciler{Claims: b.store, Guard: b.guard, Local: park.Local{Guard: b.guard, Claims: b.store, DataDir: dir, Box: "box"}, Writes: hostwrite.Config{Enabled: true, Selector: source}, Box: "box", Region: "ie", Now: now,
CheckRecord: func(context.Context, hostbox.RecordSelection) error { return nil },
- CheckLocal: func(context.Context, hostclaim.Observed, hostbox.RecordSelection) error { return nil }}
+ CheckLocal: func(context.Context, hostclaim.Observed, hostbox.RecordSelection) error { return nil },
+ CheckHeld: func(context.Context, hostclaim.Observed, hostbox.RecordSelection) error { return nil }}
relocationSourceRows(getenv, &b.r, b.guard, b.reg)
if fence := b.r.FenceClosed; fence != nil {
b.r.FenceClosed = func(ctx context.Context, o hostclaim.Observed) (bool, error) { b.asked++; return fence(ctx, o) }
@@ -15521,12 +15814,12 @@ func TestProductionClaimAssemblySetsTheSourceHoldOnlyWithRegionHA(t *testing.T)
Run:
```bash
-nice -n 19 go test ./internal/hostbox/ -count=1 -run '^(TestReconcileHoldsTheSourcesRowAtItsFencesClose|TestReconcileSourceHoldNeverTakesAnotherOpsRow)$'
+nice -n 19 go test ./internal/hostbox/ -count=1 -run '^(TestReconcileHoldsTheSourcesRowAtItsFencesClose|TestReconcileSourceHoldNeverTakesAnotherOpsRow|TestReconcileHoldsTheSourceWhenItsFirstPassAfterTheFenceIsAtInstalling|TestCensusCheckHeldRefusesAContradictingOrMissingSourceInventory)$'
```
-Expected: FAIL to compile: `undefined: AdmissionFenceClosed`, `undefined: RegistryHoldRow`. (`./internal/host/` fails to compile the same way, with `undefined: relocationSourceRows`.)
+Expected: FAIL to compile: `undefined: AdmissionFenceClosed`, `undefined: RegistryHoldRow`, `CheckHeld undefined`. (`./internal/host/` fails to compile the same way, with `undefined: relocationSourceRows`.)
-- [ ] **Step 3: The guard's fence and the hold write**
+- [ ] **Step 3: The guard's fence, the hold write and the hold-only check**
`internal/hostbox/relocationsource.go` (new):
@@ -15614,9 +15907,120 @@ func RegistryHoldRow(reg *registry.Registry) func(context.Context, string, strin
}
```
-`internal/hostbox/reconcile.go`, comments only. In `Reconciler`, replace the `FenceClosed` and `HoldRow` field comments' last lines:
+`CheckHeld` goes directly before `CheckLocal`.
+
+`internal/hostbox/empty_census.go`, replace:
+
+```go
+func (c EmptyInstallationCensus) CheckLocal(ctx context.Context, obs hostclaim.Observed, selected RecordSelection) error {
+```
+
+with:
+
+```go
+// CheckHeld is the hold-only path's local check (region HA R5-3; Astra
+// Task 10b round 1): the row this box would hold for a relocation must be
+// its own existing installation's, for the record's incarnation. It reads
+// the host's row and that installation's inventory at the row's own stamp,
+// never at the claim holder's: a relocation's source is held after
+// ToInstalling has moved holder.stamp to the target's install stamp, which
+// this box never had, and CheckLocal would ask for that. It writes nothing
+// and grants nothing: holding is not installing. The directory revision is
+// not compared: it says which roster admitted the installation, and a hold
+// needs only that the installation is this box's. Absence is never
+// evidence: no row is ErrChanged (there is nothing to hold); a row with no
+// valid ownership, no complete inventory of this box and enrollment at its
+// stamp, or a route entry that does not name exactly its owner and row
+// identity, is ErrTypedInventory; a read failure is its error.
+func (c EmptyInstallationCensus) CheckHeld(ctx context.Context, obs hostclaim.Observed, selected RecordSelection) error {
+ if c.Inventory == nil || c.Registry == nil || obs.Claim == nil || obs.Claim.Holder == nil || c.Box == "" || c.Enrollment == "" {
+ return ErrUnsupported
+ }
+ if err := ctx.Err(); err != nil {
+ return err
+ }
+ if obs.Host != selected.Host || selected.Incarnation == "" || obs.Claim.Holder.Incarnation != selected.Incarnation {
+ return ErrChanged
+ }
+ row, err := c.Registry.Get(selected.Host)
+ if err != nil {
+ return err
+ }
+ if row == nil {
+ return ErrChanged
+ }
+ own := row.ClaimOwnership
+ if !own.Valid() || own.ClaimIncarnation != selected.Incarnation {
+ return ErrTypedInventory
+ }
+ in, err := c.Inventory.ReadInstallation(ctx, selected.Host, own.ClaimIncarnation, own.ClaimStamp)
+ if errors.Is(err, sql.ErrNoRows) {
+ return ErrTypedInventory
+ }
+ if err != nil {
+ return err
+ }
+ if !in.Complete || in.Host != selected.Host || in.Incarnation != own.ClaimIncarnation || in.InstallationStamp != own.ClaimStamp || in.Box != c.Box || in.Enrollment != c.Enrollment {
+ return ErrTypedInventory
+ }
+ routes := 0
+ for _, e := range in.Entries {
+ if e.Kind != "route" || e.RelativePath != selected.Host {
+ continue
+ }
+ routes++
+ if e.Ownership != own.ClaimOwner || e.RowIdentity != own.ClaimRowID || e.Materialization != "materialized" {
+ return ErrTypedInventory
+ }
+ }
+ if routes != 1 {
+ return ErrTypedInventory
+ }
+ return nil
+}
+
+func (c EmptyInstallationCensus) CheckLocal(ctx context.Context, obs hostclaim.Observed, selected RecordSelection) error {
+```
+
+`internal/hostbox/reconcile.go`: the `Reconciler` fields from `CheckLocal` to the end of the struct (`CheckHeld` added; the hooks' comments say who sets them).
+
+`internal/hostbox/reconcile.go`, replace:
+
+```go
+ // CheckLocal includes complete census and journal recovery before initialization.
+ CheckLocal func(context.Context, hostclaim.Observed, RecordSelection) error
+ // Adopt is Plan 5's #232 backfill coordinator, never a second adoption rule.
+ Adopt func(context.Context, RecordSelection) (hostclaim.Observed, error)
+ // FenceClosed reports whether this box's admission fence for the host
+ // is closed on the claim given (region HA §5.3: the source in
+ // releasing is ready until then). It is asked only where the row
+ // decision depends on it (rowDecision). It is called under the host lock
+ // and must not take it (hostguard.Guard.AdmissionFenced). nil: never
+ // closed, today's behaviour. An error keeps rows and mints none (C6).
+ FenceClosed func(context.Context, hostclaim.Observed) (bool, error)
+ // HoldRow keeps an existing row held for an operation without
+ // installation authority (ruling 17); 6b's adapter supplies it.
+ HoldRow func(ctx context.Context, host, holdOp string) error
+```
+
+with:
```go
+ // CheckLocal includes complete census and journal recovery before initialization.
+ CheckLocal func(context.Context, hostclaim.Observed, RecordSelection) error
+ // CheckHeld is the hold-only path's local check, in CheckLocal's place
+ // there: this box's own existing installation for the row it would
+ // hold, never the claim holder's stamp (Task 10b, Astra round 1).
+ // Production: EmptyInstallationCensus.CheckHeld, which
+ // ReconcileAdapter.Install sets from its census. nil refuses the hold.
+ CheckHeld func(context.Context, hostclaim.Observed, RecordSelection) error
+ // Adopt is Plan 5's #232 backfill coordinator, never a second adoption rule.
+ Adopt func(context.Context, RecordSelection) (hostclaim.Observed, error)
+ // FenceClosed reports whether this box's admission fence for the host
+ // is closed on the claim given (region HA §5.3: the source in
+ // releasing is ready until then). It is asked only where the row
+ // decision depends on it (rowDecision). It is called under the host lock
+ // and must not take it (hostguard.Guard.AdmissionFenced). nil: never
// closed, today's behaviour. An error keeps rows and mints none (C6).
// Production: AdmissionFenceClosed, region HA on only (Task 10b).
FenceClosed func(context.Context, hostclaim.Observed) (bool, error)
@@ -15626,7 +16030,17 @@ func RegistryHoldRow(reg *registry.Registry) func(context.Context, string, strin
HoldRow func(ctx context.Context, host, holdOp string) error
```
-Replace `relocationAware`'s comment with:
+`relocationAware`'s comment:
+
+`internal/hostbox/reconcile.go`, replace:
+
+```go
+// relocationAware is region HA on: Task 10 sets FenceClosed and HoldRow
+// only with CARLOS_REGION_HA. With neither, reconcile is today's: the
+// serving executor alone (Serving), and no hold-only path.
+```
+
+with:
```go
// relocationAware is region HA on: Task 10b sets FenceClosed and HoldRow
@@ -15635,7 +16049,52 @@ Replace `relocationAware`'s comment with:
// hold-only path.
```
-and `holdOnly`'s with:
+The end of `checkOn`: the hold-only path takes `CheckHeld` in `CheckLocal`'s place.
+
+`internal/hostbox/reconcile.go`, replace:
+
+```go
+ if err := r.CheckRecord(ctx, selected); err != nil {
+ return err
+ }
+ return r.CheckLocal(ctx, obs, selected)
+}
+```
+
+with:
+
+```go
+ if err := r.CheckRecord(ctx, selected); err != nil {
+ return err
+ }
+ if holdOnly {
+ // Holding is not installing (Task 10b, Astra round 1): the source
+ // at installing or later is held against its own installation,
+ // which CheckHeld checks; CheckLocal would ask for the holder's
+ // stamp, the target's, and refuse for good.
+ if r.CheckHeld == nil {
+ return ErrUnsupported
+ }
+ return r.CheckHeld(ctx, obs, selected)
+ }
+ return r.CheckLocal(ctx, obs, selected)
+}
+```
+
+`holdOnly`'s comment:
+
+`internal/hostbox/reconcile.go`, replace:
+
+```go
+// holdOnly keeps an existing row held for the operation, with no
+// installation authority: the write is conditional on the row's recorded
+// ownership and its observed hold (6b's registry.SetClaimHold, through
+// HoldRow), under the host's flock. The decision is remade on a fresh
+// read under that lock. ErrChanged when there is no row: nothing is
+// minted.
+```
+
+with:
```go
// holdOnly keeps an existing row held for the operation, with no
@@ -15645,6 +16104,32 @@ and `holdOnly`'s with:
// ErrChanged when there is no row: nothing is minted.
```
+`ReconcileAdapter.Install` sets the hold-only check from its census, after its record check:
+
+`internal/hostbox/reconcile_adapter.go`, replace:
+
+```go
+ if priorCheck != nil {
+ return priorCheck(ctx, selected)
+ }
+ return nil
+ }
+```
+
+with:
+
+```go
+ if priorCheck != nil {
+ return priorCheck(ctx, selected)
+ }
+ return nil
+ }
+ if reconciler.CheckHeld == nil && a.Census != nil {
+ // The hold-only path's local check is the census's own (Task 10b).
+ reconciler.CheckHeld = a.Census.CheckHeld
+ }
+```
+
- [ ] **Step 4: Wire the hooks where the box's reconciler is built**
`internal/host/relocationsourcehold.go` (new):
@@ -15683,7 +16168,15 @@ func relocationSourceRows(getenv func(string) string, r *hostbox.Reconciler, gua
}
```
-`internal/host/claimconstruction.go`, in `OpenProductionClaimAssembly`, directly after the `ReconcileAdapter` is constructed:
+In `OpenProductionClaimAssembly`, directly after the `ReconcileAdapter` is constructed (`ClaimAssembly.Build` already sets `Reconcile.Census`, which the adapter's `Install` turns into `CheckHeld`):
+
+`internal/host/claimconstruction.go`, replace:
+
+```go
+ reconcile := &hostbox.ReconcileAdapter{Registry: r.Registry, Inventory: inventory, Retirement: hostbox.TypedRetirement{JournalDir: claimRetireJournalDir(r.Mode, r.DataDir)}}
+```
+
+with:
```go
reconcile := &hostbox.ReconcileAdapter{Registry: r.Registry, Inventory: inventory, Retirement: hostbox.TypedRetirement{JournalDir: claimRetireJournalDir(r.Mode, r.DataDir)}}
@@ -15698,14 +16191,14 @@ func relocationSourceRows(getenv func(string) string, r *hostbox.Reconciler, gua
Run:
```bash
-nice -n 19 go test ./internal/hostbox/ -count=1 -run '^(TestReconcileHoldsTheSourcesRowAtItsFencesClose|TestReconcileSourceHoldNeverTakesAnotherOpsRow|TestReconcile)'
+nice -n 19 go test ./internal/hostbox/ -count=1 -run '^(TestReconcileHoldsTheSourcesRowAtItsFencesClose|TestReconcileSourceHoldNeverTakesAnotherOpsRow|TestReconcileHoldsTheSourceWhenItsFirstPassAfterTheFenceIsAtInstalling|TestCensusCheckHeldRefusesAContradictingOrMissingSourceInventory|TestReconcile|TestEmptyInstallationCensus)'
nice -n 19 go test ./internal/registry/ -count=1 -run '^(TestSetHoldIfRelocatingNeverTakesAnotherOpsRow|TestRelocatingIsAHoldStatus)$'
nice -n 19 go test ./internal/host/ -count=1 -run '^(TestRelocationSourceHold|TestProductionClaimAssemblySetsTheSourceHold)'
```
Expected: PASS. The task's tests open no socket and pass inside the sandbox. Then the gate in Global Constraints, with `GOOS=windows go vet` of `./internal/host/` and `./internal/registry/` (hostbox's windows vet fails at the base already, at `plan9_fix2_minors_test.go:100`, which this task does not touch).
-**Compile and test record (rev 5.4).** In a throwaway copy of the branch at 42093182 (code as after c9b51c47; the tip adds only a review brief), with `GOFLAGS=-buildvcs=false`, niced: `go build ./...` clean; `go vet` of host, hostbox and registry clean; `gofmt -l` empty; the six tests above pass; the whole `internal/hostbox`, `internal/registry` and `internal/host` packages pass (host outside the sandbox with `TMPDIR=/tmp/k`); `GOOS=darwin`, `GOOS=windows` and `GOARCH=arm64` builds of `./...` clean; `GOOS=windows go vet` of host and registry clean, and `GOOS=darwin go vet` of all three clean. Each guard was mutated in the copy and the task's tests run again:
+**Compile and test record (rev 5.4.1).** In a throwaway copy of the branch at 1d6e75a5 (code as after c9b51c47; the commits since add only plans and reviews), with `GOFLAGS=-buildvcs=false`, niced: `go build ./...` clean; `go vet` of host, hostbox and registry clean; `gofmt -l` empty; the eight tests above pass; the whole `internal/hostbox`, `internal/registry` and `internal/host` packages pass (outside the sandbox with `TMPDIR=/tmp/k`); `GOOS=darwin`, `GOOS=windows` and `GOARCH=arm64` builds of `./...` clean; `GOOS=windows go vet` of host and registry clean, and `GOOS=darwin go vet` of all three clean. The code in this section was then pulled back out of the text into a second copy of the branch and found identical, file for file, to the copy that was checked. Each guard was mutated in the copy and the task's tests run again:
| Mutation | Caught by |
|---|---|
@@ -15715,19 +16208,28 @@ Expected: PASS. The task's tests open no socket and pass inside the sandbox. The
| An unreadable row answered as held | `TestReconcileSourceHoldNeverTakesAnotherOpsRow` |
| An unreadable fence answered as open | `TestReconcileHoldsTheSourcesRowAtItsFencesClose` |
| The fence always closed | `TestReconcileHoldsTheSourcesRowAtItsFencesClose` |
-| The fence never closed | both hostbox tests, `TestRelocationSourceHoldEndsWithTheCompletionOrTheParkLocal`, `TestRelocationSourceHoldFollowsTheFlag` |
+| The fence never closed | both hostbox fence tests, `TestRelocationSourceHoldEndsWithTheCompletionOrTheParkLocal`, `TestRelocationSourceHoldFollowsTheFlag` |
| The hold status changed (`moving` for `relocating`) | `TestReconcileHoldsTheSourcesRowAtItsFencesClose`, `TestRelocationSourceHoldEndsWithTheCompletionOrTheParkLocal`, `TestRelocationSourceHoldFollowsTheFlag` |
| The `CARLOS_REGION_HA` gate removed from `relocationSourceRows` | `TestRelocationSourceHoldFollowsTheFlag`, `TestProductionClaimAssemblySetsTheSourceHoldOnlyWithRegionHA` |
| The call in `OpenProductionClaimAssembly` removed | `TestProductionClaimAssemblySetsTheSourceHoldOnlyWithRegionHA` |
| `SetHoldIf`'s `hold_op` guard dropped from its statement | `TestSetHoldIfRelocatingNeverTakesAnotherOpsRow` |
+| The hold-only path back on `CheckLocal` (Astra round 1's finding) | `TestReconcileHoldsTheSourceWhenItsFirstPassAfterTheFenceIsAtInstalling` |
+| The adapter not setting `CheckHeld` | `TestReconcileHoldsTheSourceWhenItsFirstPassAfterTheFenceIsAtInstalling` |
+| A nil `CheckHeld` allowing the hold | `TestReconcileHoldsTheSourcesRowAtItsFencesClose` |
+| `CheckHeld` reading the holder's stamp instead of the row's | both round 1 tests |
+| `CheckHeld` accepting a missing inventory | `TestCensusCheckHeldRefusesAContradictingOrMissingSourceInventory` |
+| `CheckHeld` not comparing box and enrollment | `TestCensusCheckHeldRefusesAContradictingOrMissingSourceInventory` |
+| `CheckHeld` not comparing the route entry's owner and row identity | `TestCensusCheckHeldRefusesAContradictingOrMissingSourceInventory` |
+| `CheckHeld` not comparing the incarnation | `TestCensusCheckHeldRefusesAContradictingOrMissingSourceInventory` |
+| `CheckHeld` answering no row, or an unreadable registry, as held | `TestCensusCheckHeldRefusesAContradictingOrMissingSourceInventory` |
One mutation survives, by design: removing only the explicit other-op refusal (`row.HoldOp != ""`) leaves `SetHoldIf`'s own guard, which still refuses, so the behaviour is unchanged; the explicit case gives the clearer error. The second row of the table shows the test catches the takeover once that guard is gone too.
- [ ] **Step 6: Commit**
```bash
-git add internal/hostbox/relocationsource.go internal/host/relocationsourcehold.go internal/hostbox/reconcile_relocation_source_test.go internal/host/relocationsourcehold_test.go internal/host/relocationsourcehold_linux_test.go internal/host/claimconstruction.go internal/hostbox/reconcile.go internal/registry/relocating_test.go
-git commit -m "host: with region HA on, the source's row is held for its relocation once this box's admission fence for the host closes, and only the source's park-local ends that hold (R5-3)
+git add internal/hostbox/relocationsource.go internal/host/relocationsourcehold.go internal/hostbox/reconcile_relocation_source_test.go internal/hostbox/reconcile_relocation_source_census_test.go internal/host/relocationsourcehold_test.go internal/host/relocationsourcehold_linux_test.go internal/hostbox/reconcile.go internal/hostbox/empty_census.go internal/hostbox/reconcile_adapter.go internal/host/claimconstruction.go internal/hostbox/reconcile_relocation_test.go internal/registry/relocating_test.go
+git commit -m "host: with region HA on, the source's row is held for its relocation once this box's admission fence for the host closes, checked against the source's own installation, and only the source's park-local ends that hold (R5-3)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```