carlos / platform Public

moves: the Task 5 retirement refusals, re-scoped (Q4)

Drop the instances/<host> residue refusal and movesLegacyCensus: the claim's
census labels qualified paths by their own identity and its view refuses
unmapped bare history per location, so the refusal only repeated theirs and
skipped the lane sweep too. retiredGenerations stays: it is what makes the
returned time true at the live location. A holderless claim (a joining
parking sweep) now gets a quiet MovesSweepRefusal, not an anomaly.

B8 census: the movesLegacyCensus row is removed and the
movesRetirementEvidence.retired digest moves to 944be37e.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 229c351357055b505c729d8c64631c664a3573d3 parent 797ed61
5 files changed, +193 −70
  • internal/host/movesevidence.go +18 −20
  • internal/host/movesevidence_test.go +169 −42
  • internal/host/movesrefusals.go +3 −3
  • internal/host/movesrefusals_test.go +2 −2
  • internal/provision/plan9_b8_census_test.go +1 −3
diff --git a/internal/host/movesevidence.go b/internal/host/movesevidence.go
index 20d7ee717..f52a4d1e3 100644
--- a/internal/host/movesevidence.go
+++ b/internal/host/movesevidence.go
@@ -35,19 +35,23 @@ package host
//
// VerifyRetirement answers for less than the proof covers (M7; fix round 1
// ruling 1). The worker binds its one time to every instances/<host>
-// candidate, so it refuses unless the close is genuine evidence for all of
-// them:
+// candidate, so it refuses unless the close is genuine evidence for the
+// residue at the live location:
//
-// - instances/<host> residue outside the holder's live location has no
-// moves history (the archived-incarnation census is claim Plan 12's);
// - at the live location the close covers only generations the move
// retired: those superseded at or before the move's own admission. A
// generation admitted from that admission on was written past the
// close, and ordinary retirement owns it.
//
-// Nothing registers this provider yet. Task 9a passes it to
-// RegisterProductionClaimMoveParks, which reaches VerifyMovesSweep by type
-// assertion (claimassembly.go).
+// Residue outside the holder's live location is not judged here (Q4). The
+// claim's census places it: ClaimRetirementCensus labels a qualified path by
+// its own identity, and the claim's view refuses unmapped bare history per
+// location (productionClaimRegional.View), so a refusal of it here would
+// only repeat theirs and would skip the lane sweep as well.
+//
+// The provider is registered by RegisterProductionMovesParks, whose view
+// stays closed until movesWakeSourceRegistered opens; the claim reaches
+// VerifyMovesSweep by type assertion (claimassembly.go).
import (
"context"
@@ -104,10 +108,6 @@ func (e *NotMovesEvidence) Error() string {
func (e *NotMovesEvidence) Unwrap() error { return ErrNotMovesEvidence }
-// movesLegacyCensus is the contract VerifyRetirement waits on for residue it
-// has no history for (M7: the legacy census is claim Plan 12's).
-const movesLegacyCensus = "claim Plan 12 archived-incarnation census for instances/<host> residue outside the live location"
-
// movesRetirementEvidence implements hostbox.RegionalEvidence and
// park.MoveSweepVerifier over the host's provisioning record.
type movesRetirementEvidence struct {
@@ -135,12 +135,6 @@ func (e movesRetirementEvidence) VerifyRetirement(ctx context.Context, o hostcla
if err != nil {
return "", time.Time{}, err
}
- for _, v := range versions {
- loc, ok := movesInstancesLocation(r.host, v.Key)
- if ok && loc != r.location {
- return "", time.Time{}, &ClaimDependencyError{Contracts: []string{movesLegacyCensus}}
- }
- }
if err := r.retiredGenerations(versions); err != nil {
return "", time.Time{}, err
}
@@ -230,9 +224,13 @@ func (e movesRetirementEvidence) retired(ctx context.Context, o hostclaim.Observ
return movesRetired{}, &MovesSweepRefusal{Host: host, Reason: fmt.Sprintf(format, args...), Anomaly: anomaly}
}
if c.Holder == nil {
- // A free or joining claim names no record, so no move history. The
- // free-host lane proof is claim Plan 12's (fix round 1, ruling 3).
- return movesRetired{}, &ClaimDependencyError{Contracts: []string{movesLegacyCensus}}
+ // A free or joining claim names no record, so no move history. A free
+ // claim's lanes are the claim's own proof (claimFreeMoveProof), which
+ // the claim asks first; a joining parking claim reaches here. It is an
+ // ordinary state, so it is a refusal, quiet in the agent's log, and
+ // not an anomaly (Q4). Once the claim has a holder again the next
+ // pass proves the move.
+ return refuse(false, "the claim has no holder, so no record to prove a closed move from")
}
if c.Op.Kind != hostclaim.KindServing {
return refuse(false, "the claim is under a %s op", c.Op.Kind)
diff --git a/internal/host/movesevidence_test.go b/internal/host/movesevidence_test.go
index b6c3153b0..498b6002a 100644
--- a/internal/host/movesevidence_test.go
+++ b/internal/host/movesevidence_test.go
@@ -223,46 +223,164 @@ func TestVerifyRetirementLabelsMovesResidueWithTheProofsIncarnation(t *testing.T
}
}
-// TestVerifyRetirementRefusesLegacyResidue (M7): residue under
-// instances/<host> outside the holder's live location has no moves-owned
-// archived-incarnation history. VerifyRetirement refuses it with a typed
-// dependency error (the legacy census is claim Plan 12's), which skips the
-// whole at-rest sweep. The moves proof itself is unaffected.
-func TestVerifyRetirementRefusesLegacyResidue(t *testing.T) {
+// retirement runs the claim's retirement answer, the one the regional worker
+// binds to every instances/<host> candidate, over the real census with this
+// fixture's provider as the claim's moves evidence.
+func (f *evFixture) retirement(t *testing.T, evidence hostbox.RegionalEvidence) (string, time.Time, error) {
+ t.Helper()
+ p := productionClaimRegional{Now: func() time.Time { return evNow }, Parks: &claimMoveParkSources{Evidence: evidence}}
+ return p.retirement(t.Context(), f.observe(t), f.census(t))
+}
+
+// TestRetirementLeavesQualifiedResidueToTheCensus (Q4): residue under
+// instances/<host> outside the holder's live location is the claim's census
+// to place, not the moves provider's to refuse. The census labels a
+// qualified path by its own identity (ClaimRetirementCensus) and the claim's
+// view refuses unmapped bare history per location, so the provider answers
+// with the move's close and the moves/<host> lane sweep goes ahead.
+func TestRetirementLeavesQualifiedResidueToTheCensus(t *testing.T) {
+ older := strings.Repeat("ab", 32)
for name, key := range map[string]string{
"a legacy level object": "instances/" + evHost + "/0000/db",
"a legacy generation object": "instances/" + evHost + "/g-old/0000/db",
- "an older incarnation": "instances/" + evHost + "/" + strings.Repeat("ab", 32) + "/g-old/0000/db",
+ "an older incarnation": "instances/" + evHost + "/" + older + "/g-old/0000/db",
} {
t.Run(name, func(t *testing.T) {
f := newEvFixture(t)
f.lane(evMoveID, "move.json")
f.bucket.Put("data", f.genKey(evMoveID), []byte("live"))
- f.bucket.Put("data", key, []byte("legacy"))
- obs, census := f.observe(t), f.census(t)
- inc, at, err := f.provider().VerifyRetirement(t.Context(), obs, census)
- var dep *ClaimDependencyError
- if !errors.As(err, &dep) || inc != "" || !at.IsZero() {
- t.Fatalf("VerifyRetirement = %q, %v, %v; want a *ClaimDependencyError and no evidence", inc, at, err)
+ f.bucket.Put("data", key, []byte("residue"))
+ inc, at, err := f.retirement(t, f.provider())
+ if err != nil || inc != f.inc || !at.Equal(evAt) {
+ t.Fatalf("retirement = %q, %v, %v; want %q, %v and no refusal", inc, at, err, f.inc, evAt)
}
- if _, err := f.provider().VerifyMovesSweep(t.Context(), obs, census); err != nil {
+ if _, err := f.provider().VerifyMovesSweep(t.Context(), f.observe(t), f.census(t)); err != nil {
t.Fatalf("the moves proof does not depend on instances residue: %v", err)
}
})
}
+ t.Run("the census labels a qualified path by its own identity", func(t *testing.T) {
+ f := newEvFixture(t)
+ f.bucket.Put("data", f.genKey(evMoveID), []byte("live"))
+ f.bucket.Put("data", "instances/"+evHost+"/"+older+"/g-old/0000/db", []byte("older"))
+ labels, err := ClaimRetirementCensus(f.observe(t), f.census(t))
+ if err != nil || labels["instances/"+evHost+"/"+older] != older {
+ t.Fatalf("labels = %v, %v", labels, err)
+ }
+ if _, _, err := f.retirement(t, f.provider()); err != nil {
+ t.Fatal(err)
+ }
+ })
t.Run("an incarnation directory beside a bare holder", func(t *testing.T) {
f := newEvFixture(t)
f.bare(t)
f.bucket.Put("data", f.genKey(evMoveID), []byte("live"))
f.bucket.Put("data", "instances/"+evHost+"/"+f.inc+"/g-old/0000/db", []byte("scoped"))
- _, _, err := f.provider().VerifyRetirement(t.Context(), f.observe(t), f.census(t))
- var dep *ClaimDependencyError
- if !errors.As(err, &dep) {
- t.Fatalf("err = %v, want a *ClaimDependencyError", err)
+ inc, at, err := f.retirement(t, f.provider())
+ if err != nil || inc != f.inc || !at.Equal(evAt) {
+ t.Fatalf("retirement = %q, %v, %v", inc, at, err)
}
})
}
+// TestRetirementStillRefusesAGenerationTheMoveDidNotRetire (Q4): the close
+// is the evidence time for every instances/<host> candidate the worker binds
+// it to, so at the live location it must be true. retiredGenerations stays:
+// a generation admitted from the move's own admission on, or written after
+// the close, is refused with *NotMovesEvidence. A generation the move did
+// retire passes beside it.
+func TestRetirementStillRefusesAGenerationTheMoveDidNotRetire(t *testing.T) {
+ refused := func(t *testing.T, f *evFixture, gen string) {
+ t.Helper()
+ inc, at, err := f.retirement(t, f.provider())
+ var n *NotMovesEvidence
+ if !errors.As(err, &n) || n.Generation != gen || inc != "" || !at.IsZero() {
+ t.Fatalf("retirement = %q, %v, %v; want *NotMovesEvidence for %q and no evidence", inc, at, err, gen)
+ }
+ if !errors.Is(err, ErrNotMovesEvidence) {
+ t.Fatalf("err does not unwrap to ErrNotMovesEvidence: %v", err)
+ }
+ }
+ t.Run("a generation the move retired passes", func(t *testing.T) {
+ f := newEvFixture(t)
+ f.bucket.Put("data", f.genKey(evMoveID), []byte("live"))
+ f.bucket.Put("data", f.genKey("old"), []byte("superseded by the move"))
+ inc, at, err := f.retirement(t, f.provider())
+ if err != nil || inc != f.inc || !at.Equal(evAt) {
+ t.Fatalf("retirement = %q, %v, %v", inc, at, err)
+ }
+ })
+ t.Run("a generation admitted after the move", func(t *testing.T) {
+ f := newEvFixture(t)
+ f.setClaim(t, func(c *hostclaim.Claim) {
+ c.Holder.Admissions = evAdmissions(evMoveID, "next")
+ c.Holder.Generation = "next"
+ })
+ f.bucket.Put("data", f.genKey("next"), []byte("live"))
+ f.bucket.Put("data", f.genKey(evMoveID), []byte("superseded after the move"))
+ refused(t, f, evMoveID)
+ })
+ t.Run("an object written after the close", func(t *testing.T) {
+ f := newEvFixture(t)
+ f.bucket.Put("data", f.genKey(evMoveID), []byte("live"))
+ f.bucket.Put("data", f.genKey("old"), []byte("late"))
+ f.rec.LastMove.At = time.Date(2026, 10, 5, 0, 0, 0, 0, time.UTC)
+ refused(t, f, "old")
+ })
+ t.Run("a claim that holds no admission of the move", func(t *testing.T) {
+ f := newEvFixture(t)
+ f.setClaim(t, func(c *hostclaim.Claim) { c.Holder.Admissions = evAdmissions()[:1]; c.Holder.Generation = "old" })
+ f.bucket.Put("data", f.genKey("old"), []byte("live"))
+ f.bucket.Put("data", f.genKey("other"), []byte("no admission to date it by"))
+ refused(t, f, "other")
+ })
+}
+
+// TestJoiningClaimRefusesQuietly (Q4): a joining claim (a parking sweep with
+// no holder) and a free one name no record, so the moves provider has no
+// close to prove from. It refuses with a *MovesSweepRefusal that is not an
+// anomaly, never a dependency, and reads no record: the agent logs it once an
+// hour and pages nobody. The claim's own free-host proof answers a free claim
+// before it gets here (claimFreeMoveProof).
+func TestJoiningClaimRefusesQuietly(t *testing.T) {
+ f := newEvFixture(t)
+ f.setClaim(t, func(c *hostclaim.Claim) {
+ c.Holder, c.Touched = nil, hostclaim.Touched{}
+ c.Op = hostclaim.Op{Kind: hostclaim.KindParking, ID: "op", Intent: map[string]string{"park_sweep": "v1"}}
+ c.Obligations = []hostclaim.Obligation{{ID: "chore", Op: "op", Kind: "park-replica", Region: "ie", Owner: "region:ie", Retirement: "ret",
+ Park: &hostclaim.ParkScope{Location: f.location, Incarnation: f.inc, EvidenceAt: evAt, RequestLimit: hostbox.RequestLimit}}}
+ })
+ f.lane(evMoveID, "move.json")
+ var pages pagesSeen
+ l := &refusalLog{now: evNow}
+ r := newMovesRefusals("ie-box", pages.hook(), l.logf, l.clock)
+ for name, evidence := range map[string]hostbox.RegionalEvidence{"the provider": f.provider(), "the quiet wrapper": r.evidence(f.provider())} {
+ t.Run(name, func(t *testing.T) {
+ inc, at, err := f.retirement(t, evidence)
+ var dep *ClaimDependencyError
+ if errors.As(err, &dep) {
+ t.Fatalf("a dependency, not a refusal: %v", err)
+ }
+ wantRefusal(t, err, false)
+ if inc != "" || !at.IsZero() {
+ t.Fatalf("evidence %q, %v with a refusal", inc, at)
+ }
+ })
+ }
+ if f.reads != 0 {
+ t.Fatalf("read %d records with no holder to name one", f.reads)
+ }
+ _, _, err := f.retirement(t, f.provider())
+ r.report(err)
+ r.report(err)
+ if got := l.take(); len(got) != 1 {
+ t.Fatalf("logged %q; want one line within the hour", got)
+ }
+ if len(pages) != 0 {
+ t.Fatalf("a joining claim paged: %+v", pages)
+ }
+}
+
// bare puts the holder at the activator's own lease location,
// instances/<host>: the only holder a move cuts (ruling T5-lease,
// movescut.go writerLease), so every moved host's holder.
@@ -547,12 +665,11 @@ func TestMovesEvidenceRefusesAnotherIdentity(t *testing.T) {
f.setClaim(t, func(c *hostclaim.Claim) { c.Holder, c.Op = nil, hostclaim.Op{} })
obs, census := f.observe(t), f.census(t)
_, err := f.provider().VerifyMovesSweep(t.Context(), obs, census)
- var dep *ClaimDependencyError
- if !errors.As(err, &dep) {
- t.Fatalf("err = %v, want a *ClaimDependencyError", err)
- }
- if _, _, err := f.provider().VerifyRetirement(t.Context(), obs, census); !errors.As(err, &dep) {
- t.Fatalf("err = %v, want a *ClaimDependencyError", err)
+ wantRefusal(t, err, false)
+ if _, _, err := f.provider().VerifyRetirement(t.Context(), obs, census); err == nil {
+ t.Fatal("VerifyRetirement answered with no holder")
+ } else {
+ wantRefusal(t, err, false)
}
if f.reads != 0 {
t.Fatalf("read %d records with no holder to name one", f.reads)
@@ -727,35 +844,45 @@ func TestSweepRefusedUnlessTheCloseIsBeforeTheListing(t *testing.T) {
}
// TestSweepScopeMembership (witness wSweepScopeMembership, M7): residue of
-// an older incarnation is never the moves/<host> chore's. Without its own
-// evidence the provider refuses the whole sweep, so nothing is queued.
+// an older incarnation is never the moves/<host> chore's. The provider no
+// longer refuses the sweep for it (Q4): the engine queues it as its own
+// chore under its own qualified identity and parks it, and the moves/<host>
+// chore keeps the holder's.
func TestSweepScopeMembership(t *testing.T) {
f := newEvFixture(t)
+ older := strings.Repeat("cd", 32)
f.lane(evMoveID, "move.json")
- f.bucket.Put("data", "instances/"+evHost+"/"+strings.Repeat("cd", 32)+"/g-old/0000/db", []byte("late"))
- err := f.worker(t, evNow).Run(t.Context())
- var dep *ClaimDependencyError
- if !errors.As(err, &dep) {
- t.Fatalf("err = %v, want the legacy census dependency", err)
+ residue := "instances/" + evHost + "/" + older + "/g-old/0000/db"
+ f.bucket.Put("data", residue, []byte("late"))
+ if err := f.worker(t, evNow).Run(t.Context()); !errors.Is(err, park.ErrPending) {
+ t.Fatalf("the chores wait for their horizons: %v", err)
}
- if len(f.chores(t)) != 0 {
- t.Fatalf("chores %+v", f.chores(t))
+ chores := f.chores(t)
+ moves, ok := chores["moves/"+evHost]
+ if !ok || moves.Incarnation != f.inc {
+ t.Fatalf("chores = %+v; want the moves/<host> chore under the holder's incarnation", chores)
+ }
+ if _, ok := chores["instances/"+evHost+"/"+older]; ok || len(chores) != 1 {
+ t.Fatalf("chores = %+v; want only the moves/<host> chore pending", chores)
+ }
+ // The older incarnation's chore ran first, under its own identity, and
+ // parked its residue in the same pass.
+ if _, ok := f.bucket.Current("data", residue); ok {
+ t.Fatal("the older incarnation's residue was not parked")
}
}
-// TestFreeHostLaneProofIsClaimPlan12s (fix round 1, ruling 3): a free claim
-// names no record, so moves can prove no closed move and the claim stays
-// free. The free-host lane proof is transferred to claim Plan 12 (confirmed
-// by platform-1e); this test pins the refusal until then.
-func TestFreeHostLaneProofIsClaimPlan12s(t *testing.T) {
+// TestFreeHostLanesAreTheClaimsProof (fix round 1, ruling 3; Q4): a free
+// claim names no record, so moves can prove no closed move and the claim
+// stays free. The free-host lane proof is the claim's own
+// (claimFreeMoveProof, asked before the provider); a free claim that
+// reaches the provider gets the quiet sweep refusal, not a dependency.
+func TestFreeHostLanesAreTheClaimsProof(t *testing.T) {
f := newEvFixture(t)
f.setClaim(t, func(c *hostclaim.Claim) { c.Holder, c.Op, c.Touched = nil, hostclaim.Op{}, hostclaim.Touched{} })
f.lane(evMoveID, "move.json")
err := f.worker(t, evNow).Run(t.Context())
- var dep *ClaimDependencyError
- if !errors.As(err, &dep) {
- t.Fatalf("err = %v, want a *ClaimDependencyError", err)
- }
+ wantRefusal(t, err, false)
if c := f.observe(t).Claim; !c.Free() || len(c.Obligations) != 0 {
t.Fatalf("claim = %+v", c)
}
diff --git a/internal/host/movesrefusals.go b/internal/host/movesrefusals.go
index fcad00f68..cb97d75b7 100644
--- a/internal/host/movesrefusals.go
+++ b/internal/host/movesrefusals.go
@@ -3,9 +3,9 @@ package host
// Quiet refusals (moves Plan 5 Task 9a; the Task 5 rulings). The claim's
// regional worker asks Task 5's provider about every host at rest, every
// minute, and an ordinary state refuses: a live move, residue the move's
-// close does not vouch for, a free host whose lanes the claim's own
-// free-host proof answers (claimFreeMoveProof). The
-// worker reports each refusal, so the agent's log would repeat them every
+// close does not vouch for, a joining claim with no holder to name a record
+// (a free host's lanes are the claim's own free-host proof,
+// claimFreeMoveProof). The worker reports each refusal, so the agent's log would repeat them every
// minute. movesRefusals keeps them quiet:
//
// - report is the worker's report: a provider refusal (MovesSweepRefusal,
diff --git a/internal/host/movesrefusals_test.go b/internal/host/movesrefusals_test.go
index 14ff1b521..d699f19bf 100644
--- a/internal/host/movesrefusals_test.go
+++ b/internal/host/movesrefusals_test.go
@@ -44,7 +44,7 @@ func (l *refusalLog) take() []string {
}
// TestExpectedMovesRefusalsAreRateLimited: a live move, residue the close
-// does not vouch for and a free host (the provider's dependency refusal) are
+// does not vouch for and a named dependency (the wake-source gate's) are
// each logged once, then not again within the hour; an unexpected error is
// logged every time.
func TestExpectedMovesRefusalsAreRateLimited(t *testing.T) {
@@ -55,7 +55,7 @@ func TestExpectedMovesRefusalsAreRateLimited(t *testing.T) {
pass := errors.Join(
&MovesSweepRefusal{Host: "a.example", Reason: "the record carries the live move mv_x"},
&NotMovesEvidence{Host: "b.example", Generation: "g", Reason: "written after the close"},
- &ClaimDependencyError{Contracts: []string{movesLegacyCensus}},
+ &ClaimDependencyError{Contracts: []string{movesWakeSourceContract}},
boom,
)
r.report(pass)
diff --git a/internal/provision/plan9_b8_census_test.go b/internal/provision/plan9_b8_census_test.go
index 241785dff..b5335c517 100644
--- a/internal/provision/plan9_b8_census_test.go
+++ b/internal/provision/plan9_b8_census_test.go
@@ -526,12 +526,10 @@ var plan9B8CensusInventory = []censusEntry{
{"cli.go", "runRouteHibernate", "key-site", "key:SidecarManifestKey", "c151cae2cbc57943e9a5d0a5dac611823d769a6b651d5dbc2fe86182f6e098b1"},
{"cli.go", "RunStatus", "key-site", "lit:instances", "3b9936cf0f29f57f06c378a334e3118096679dfdadab7aad45b1f6c7b5841562"},
{"domains_cli.go", "runDomainsList", "key-site", "lit:instances", "f7aefeff56b0be7b50ee82bd250ada08ae9a095b7a62f577ead70846e00b61ad"},
- // Names a dependency contract in an error message; the instances/<host> words are the data-bucket namespace, and it touches no store.
- {"internal/host/movesevidence.go", "const movesLegacyCensus", "key-site", "lit:instances", "ccc817d41b2148d07dfdd1e780b46fbf67bcf78272c297b95f4336d83487a813"},
// Maps a data-bucket replica key to its park location instances/<host>[/<incarnation>]; a pure string function.
{"internal/host/movesevidence.go", "movesInstancesLocation", "key-site", "lit:instances", "4f79083f445c5892fb4933a696bc322eb081d0dd0c0b53c2134e8544c5a9df3e"},
// Reads the record through MovesRecordReader (GetRecord); RecordKey only checks the claim holder's key, and the literals only compare data-bucket locations. No write.
- {"internal/host/movesevidence.go", "movesRetirementEvidence.retired", "key-site", "key:RecordKey,lit:instances", "d6a42cfd64728e581bb123d5743d3c74b1b6eb5abddb8972571f15cb42d84cb6"},
+ {"internal/host/movesevidence.go", "movesRetirementEvidence.retired", "key-site", "key:RecordKey,lit:instances", "944be37e61727fdc6ed76f0394a8ae3100ff5b15a92b8619ac9e7a8cd91362df"},
// The moves park view's record read (Plan 5 Task 9a): reads through MovesRecordReader (GetRecord); RecordKey only checks the claim holder's key. No write.
{"internal/host/movessources.go", "holderRecord", "key-site", "key:RecordKey", "84e840552703cd9863c9b27786baa864109e94d15b9e6f3e035c0da18622eb2e"},
// The park view's read-only store over one manifest version (Plan 5 Task 9a, M10): Put refuses and writes nothing.