carlos / platform Public

moves-phase1-5d

Squashed from moves-phase1-5d into main.
claude agent, operated by paul@keymail.dev d562d7f24ddfeb211242c925ac4c897382492e6d parent 0901e83
3 files changed, +1218 −0
  • internal/host/movesevidence.go +315 −0
  • internal/host/movesevidence_test.go +897 −0
  • internal/provision/plan9_b8_census_test.go +6 −0

CI passed — run details

CI log
=== gate ===
cd launchctx && go vet ./... && go test ./... -count=1
ok  	amadan.net/carlos/platform.git/launchctx	0.011s
?   	amadan.net/carlos/platform.git/launchctx/controlname	[no test files]
?   	amadan.net/carlos/platform.git/launchctx/wireid	[no test files]
go vet ./...
gofmt -l .: clean
go test ./... -count=1 -timeout 30m
ok  	github.com/carlosframework/platform	313.744s
?   	github.com/carlosframework/platform/cmd/changelog	[no test files]
ok  	github.com/carlosframework/platform/cmd/console	2.979s
ok  	github.com/carlosframework/platform/internal/activator	134.917s
ok  	github.com/carlosframework/platform/internal/agentsock	0.022s
ok  	github.com/carlosframework/platform/internal/alerting	0.004s
ok  	github.com/carlosframework/platform/internal/analytics	0.021s
ok  	github.com/carlosframework/platform/internal/appconfig	2.145s
ok  	github.com/carlosframework/platform/internal/apperr	0.094s
ok  	github.com/carlosframework/platform/internal/applog	0.192s
ok  	github.com/carlosframework/platform/internal/awsinv	0.006s
ok  	github.com/carlosframework/platform/internal/boxchoice	0.008s
ok  	github.com/carlosframework/platform/internal/boxid	0.688s
?   	github.com/carlosframework/platform/internal/boxid/boxidtest	[no test files]
?   	github.com/carlosframework/platform/internal/boxid/internal/signtest	[no test files]
ok  	github.com/carlosframework/platform/internal/boxvolumes	0.020s
ok  	github.com/carlosframework/platform/internal/caps	0.018s
ok  	github.com/carlosframework/platform/internal/certwarm	0.004s
ok  	github.com/carlosframework/platform/internal/cgroup	0.193s
ok  	github.com/carlosframework/platform/internal/changelog	0.272s
ok  	github.com/carlosframework/platform/internal/clamd	0.061s
?   	github.com/carlosframework/platform/internal/cliflag	[no test files]
ok  	github.com/carlosframework/platform/internal/console	317.751s
?   	github.com/carlosframework/platform/internal/controlname	[no test files]
?   	github.com/carlosframework/platform/internal/controlsock	[no test files]
ok  	github.com/carlosframework/platform/internal/dbdoor	6.373s
ok  	github.com/carlosframework/platform/internal/dbdoor/dbdoortest	0.009s
ok  	github.com/carlosframework/platform/internal/dbverify	0.004s
ok  	github.com/carlosframework/platform/internal/dnsrec	0.005s
ok  	github.com/carlosframework/platform/internal/docsite	0.005s
ok  	github.com/carlosframework/platform/internal/domains	1.974s
ok  	github.com/carlosframework/platform/internal/doorkey	0.006s
ok  	github.com/carlosframework/platform/internal/economics	0.004s
ok  	github.com/carlosframework/platform/internal/edge	25.881s
?   	github.com/carlosframework/platform/internal/edgeproof	[no test files]
ok  	github.com/carlosframework/platform/internal/edgesfile	0.008s
ok  	github.com/carlosframework/platform/internal/fleetapi	0.128s
ok  	github.com/carlosframework/platform/internal/gate	1.655s
ok  	github.com/carlosframework/platform/internal/genselect	0.019s
ok  	github.com/carlosframework/platform/internal/geo	0.013s
ok  	github.com/carlosframework/platform/internal/geoip	0.033s
ok  	github.com/carlosframework/platform/internal/host	44.076s
ok  	github.com/carlosframework/platform/internal/hostactors	0.503s
ok  	github.com/carlosframework/platform/internal/hostbox	1.341s
ok  	github.com/carlosframework/platform/internal/hostclaim	0.217s
ok  	github.com/carlosframework/platform/internal/hostevidence	0.014s
ok  	github.com/carlosframework/platform/internal/hostexec	0.314s
ok  	github.com/carlosframework/platform/internal/hostfence	0.038s
ok  	github.com/carlosframework/platform/internal/hostfiles	4.061s
ok  	github.com/carlosframework/platform/internal/hostfloor	0.015s
ok  	github.com/carlosframework/platform/internal/hostguard	0.141s
ok  	github.com/carlosframework/platform/internal/hostops	1.236s
ok  	github.com/carlosframework/platform/internal/hostquarantine	0.004s
ok  	github.com/carlosframework/platform/internal/hostresolve	0.141s
ok  	github.com/carlosframework/platform/internal/hostwrite	0.005s
ok  	github.com/carlosframework/platform/internal/infraview	0.003s
ok  	github.com/carlosframework/platform/internal/instancectl	1.700s
ok  	github.com/carlosframework/platform/internal/instanceexec	2.678s
ok  	github.com/carlosframework/platform/internal/instancefs	0.005s
ok  	github.com/carlosframework/platform/internal/layout	1.303s
ok  	github.com/carlosframework/platform/internal/lease	9.426s
ok  	github.com/carlosframework/platform/internal/lease/leasetest	0.005s
ok  	github.com/carlosframework/platform/internal/ledger	0.014s
ok  	github.com/carlosframework/platform/internal/mail	0.165s
ok  	github.com/carlosframework/platform/internal/maintenance	0.075s
ok  	github.com/carlosframework/platform/internal/maintenanceexec	5.503s
ok  	github.com/carlosframework/platform/internal/maintenanceguard	0.065s
ok  	github.com/carlosframework/platform/internal/metrics	0.005s
ok  	github.com/carlosframework/platform/internal/moves	20.902s
ok  	github.com/carlosframework/platform/internal/moves/archive	0.732s
ok  	github.com/carlosframework/platform/internal/objstore	0.166s
ok  	github.com/carlosframework/platform/internal/ops	0.165s
ok  	github.com/carlosframework/platform/internal/park	0.190s
ok  	github.com/carlosframework/platform/internal/privatefile	0.003s
ok  	github.com/carlosframework/platform/internal/provision	3.019s
ok  	github.com/carlosframework/platform/internal/publicrelease	0.007s
ok  	github.com/carlosframework/platform/internal/registry	3.482s
ok  	github.com/carlosframework/platform/internal/relay	7.222s
ok  	github.com/carlosframework/platform/internal/release	18.336s
ok  	github.com/carlosframework/platform/internal/rollout	1.477s
ok  	github.com/carlosframework/platform/internal/rootaws	0.006s
ok  	github.com/carlosframework/platform/internal/s3compat	0.005s
ok  	github.com/carlosframework/platform/internal/safefile	0.003s
ok  	github.com/carlosframework/platform/internal/schedule	2.186s
ok  	github.com/carlosframework/platform/internal/sim/hostlife	2.329s
ok  	github.com/carlosframework/platform/internal/sim/litestreamfacts	3.906s
ok  	github.com/carlosframework/platform/internal/status	0.040s
ok  	github.com/carlosframework/platform/internal/steering	0.005s
ok  	github.com/carlosframework/platform/internal/stopevidence	0.098s
ok  	github.com/carlosframework/platform/internal/svcrole	0.002s
ok  	github.com/carlosframework/platform/internal/telemetry	0.004s
ok  	github.com/carlosframework/platform/internal/termfence	0.004s
ok  	github.com/carlosframework/platform/internal/tunnelclient	0.004s
ok  	github.com/carlosframework/platform/internal/unixdial	0.002s
ok  	github.com/carlosframework/platform/internal/updatewords	0.002s
ok  	github.com/carlosframework/platform/internal/usage	0.004s
ok  	github.com/carlosframework/platform/internal/version	0.002s
ok  	github.com/carlosframework/platform/internal/vet	1.552s
=== cross ===
--- shard 0 of 5 ---
cross-compiling darwin/arm64
--- shard 1 of 5 ---
cross-compiling darwin/amd64
--- shard 2 of 5 ---
cross-compiling linux/arm64
--- shard 3 of 5 ---
cross-compiling windows/amd64
--- shard 4 of 5 ---
cross-compiling windows/arm64
=== minio-contract ===
minio contract: arm64, minio RELEASE.2025-09-07T16-13-09Z, mc RELEASE.2025-08-13T08-35-41Z
  minio: cached, digest verified
  mc: cached, digest verified
  minio healthy on 127.0.0.1:20999
Added `local` successfully.
Bucket created successfully `local/contract-bucket`.
Bucket created successfully `local/denied-bucket`.
Created policy `contract-only` successfully.
Added user `scopeduser` successfully.
Attached Policies: [contract-only]
To User: scopeduser
  buckets and scoped user ready
=== RUN   TestDeliveredContractMinio
=== RUN   TestDeliveredContractMinio/put
=== RUN   TestDeliveredContractMinio/get_round-trips_the_body
=== RUN   TestDeliveredContractMinio/list_shows_the_key
=== RUN   TestDeliveredContractMinio/head_and_list_agree_on_the_version
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_its_own_object
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/not_by_PutObjectTagging
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/and_not_by_x-amz-tagging_on_upload
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CreateMultipartUpload
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CopyObject_with_a_REPLACE_tagging_directive
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/but_multipart_itself_is_allowed
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_copy_itself_is_allowed
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/a_POST-policy_forged_tag_with_a_matching_version_is_still_not_a_verdict
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_an_ordinary_tagged-with-something-else_upload_is_allowed
=== RUN   TestDeliveredContractMinio/delete_removes_it
=== RUN   TestDeliveredContractMinio/cross-bucket_denial
--- PASS: TestDeliveredContractMinio (0.02s)
    --- PASS: TestDeliveredContractMinio/put (0.00s)
    --- PASS: TestDeliveredContractMinio/get_round-trips_the_body (0.00s)
    --- PASS: TestDeliveredContractMinio/list_shows_the_key (0.00s)
    --- PASS: TestDeliveredContractMinio/head_and_list_agree_on_the_version (0.00s)
    --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/not_by_PutObjectTagging (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/and_not_by_x-amz-tagging_on_upload (0.00s)
    --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route (0.01s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CreateMultipartUpload (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CopyObject_with_a_REPLACE_tagging_directive (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/but_multipart_itself_is_allowed (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_copy_itself_is_allowed (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/a_POST-policy_forged_tag_with_a_matching_version_is_still_not_a_verdict (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_an_ordinary_tagged-with-something-else_upload_is_allowed (0.00s)
    --- PASS: TestDeliveredContractMinio/delete_removes_it (0.00s)
    --- PASS: TestDeliveredContractMinio/cross-bucket_denial (0.00s)
PASS
ok  	github.com/carlosframework/platform/internal/objstore	0.023s
=== RUN   TestT4StorageProbeMinio
2026/10/11 03:29:45 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18DD5C00F53AA829, HostID: 7c420053e0fbcd7f41653fdd92ffa2802c903d05303b1aa15adc2e8bb65d183f, api error SignatureDoesNotMatch: The request signature we calculated does not match the signature you provided. Check your key and signing method.
--- PASS: TestT4StorageProbeMinio (0.01s)
PASS
ok  	github.com/carlosframework/platform/internal/host	0.015s
minio contract: PASS
diff --git a/internal/host/movesevidence.go b/internal/host/movesevidence.go
new file mode 100644
index 000000000..20d7ee717
--- /dev/null
+++ b/internal/host/movesevidence.go
@@ -0,0 +1,315 @@
+package host
+
+// The moves retirement evidence provider (moves Plan 5 Task 5; rulings K10,
+// M7 and M8 of the Plan 5 ledger). The claim's regional worker
+// (hostbox.RegionalWorker) sweeps instances/<host>/ and moves/<host>/ at rest.
+// Before it sweeps it asks this provider twice:
+//
+// - VerifyRetirement, once per sweep, for the incarnation label of every
+// candidate outside an incarnation directory and for the evidence time of
+// every instances/<host> candidate. Decide labels the moves/<host>
+// candidate with the same incarnation (park/sweep.go), and the engine's
+// proof Check requires the proof to name it, so both answers come from one
+// proof.
+// - VerifyMovesSweep, per moves/<host> candidate (park/move_sweep.go). The
+// engine then stamps the listing's end as that chore's evidence, which is
+// valid only with the precondition proof (K10): the claim shows no move
+// op, and the newest retired move's close is earlier than the listing.
+//
+// The proof comes from the record's LastMove (M8, fix round 1 ruling 2): the
+// durable move id, outcome and close time, bound to the holder's
+// incarnation. It holds only when:
+//
+// - the claim is serving, so no move op is live on the host;
+// - the record is the holder's own and carries no live move;
+// - LastMove names a move closed at a real time before this clock;
+// - every moves/<host>/<op>/ object is that move's own, or was written
+// before its close.
+//
+// The claim's released_move journal is not required: any later serving op
+// replaces it (CompleteMetadataPlacement, for one). With M4's record-first
+// close, LastMove is written before the claim's release, so it is the
+// serving claim, with no move op, that proves the release finished.
+// LastMove.At follows every park of the move's lanes, and so every staging
+// horizon. A zero time is never evidence.
+//
+// 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:
+//
+// - 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).
+
+import (
+ "context"
+ "encoding/hex"
+ "errors"
+ "fmt"
+ "strings"
+ "time"
+
+ "github.com/carlosframework/platform/internal/hostclaim"
+ "github.com/carlosframework/platform/internal/hostwrite"
+ "github.com/carlosframework/platform/internal/lease"
+ "github.com/carlosframework/platform/internal/park"
+ "github.com/carlosframework/platform/internal/provision"
+)
+
+// ErrMovesSweepRefused is what every MovesSweepRefusal unwraps to.
+var ErrMovesSweepRefused = errors.New("host: the moves sweep is refused")
+
+// MovesSweepRefusal is the provider's refusal of one observation. Each one
+// skips that pass's sweep, and the next pass asks again. Anomaly marks a
+// refusal that no ordinary move produces: the history contradicts itself, or
+// something wrote where no known move could. M8 asks for one page per
+// anomaly; the provider sends none itself, and its wiring (Task 9a) pages.
+// Reason is for the agent's log, never shown to a user.
+type MovesSweepRefusal struct {
+ Host, Reason string
+ Anomaly bool
+}
+
+func (e *MovesSweepRefusal) Error() string {
+ return "host: the moves sweep of " + e.Host + " is refused: " + e.Reason
+}
+
+func (e *MovesSweepRefusal) Unwrap() error { return ErrMovesSweepRefused }
+
+// ErrNotMovesEvidence is what every NotMovesEvidence unwraps to.
+var ErrNotMovesEvidence = errors.New("host: the move's close is not evidence for this residue")
+
+// NotMovesEvidence is VerifyRetirement's refusal of residue at the live
+// location that the move did not retire: a generation written past the
+// move's close, or an object no generation owns. The close is not its
+// evidence, and ordinary retirement owns it. The worker binds one evidence
+// time to every instances/<host> candidate, so the provider cannot abstain
+// for one generation; this refusal skips the pass's sweep while such
+// residue stands. Reason is for the agent's log, never shown to a user.
+type NotMovesEvidence struct {
+ Host, Generation, Reason string
+}
+
+func (e *NotMovesEvidence) Error() string {
+ return "host: the move's close is not evidence for " + e.Host + " generation " + e.Generation + ": " + e.Reason
+}
+
+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 {
+ Records MovesRecordReader
+ Now func() time.Time
+}
+
+// movesRetired is the proof both methods answer from.
+type movesRetired struct {
+ host, incarnation, location, moveID string
+ at time.Time
+ holder hostclaim.Holder
+}
+
+func (e movesRetirementEvidence) VerifyMovesSweep(ctx context.Context, o hostclaim.Observed, versions []lease.ObjectVersion) (park.MoveSweepProof, error) {
+ r, err := e.retired(ctx, o, versions)
+ if err != nil {
+ return park.MoveSweepProof{}, err
+ }
+ return park.MoveSweepProof{Host: r.host, ClaimETag: o.ETag, MoveID: r.moveID, Incarnation: r.incarnation, RetiredAt: r.at}, nil
+}
+
+func (e movesRetirementEvidence) VerifyRetirement(ctx context.Context, o hostclaim.Observed, versions []lease.ObjectVersion) (string, time.Time, error) {
+ r, err := e.retired(ctx, o, versions)
+ 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
+ }
+ return r.incarnation, r.at, nil
+}
+
+// retiredGenerations refuses unless the close is evidence for every object
+// at the live location that the engine could make a candidate (fix round 1,
+// ruling 1). The marker key and the holder's current generation are the
+// live installation's. Any other generation must be one the move retired:
+// the claim must still hold the move's own admission (a prepared landing
+// admits the generation named after the move, movesprobeowners.go), the
+// generation must not be admitted at or after it (admissions are in claim
+// order and carry no time), and every version of it must predate the close.
+// A rolled-back or aborted move, or a landing that admitted a fresh name,
+// leaves no such admission, so no generation is vouched for. Objects are
+// classified as the landing's view classifies them (landingGenerationOf),
+// after the lock objects the engine itself skips (movesParkLock, fix round
+// 2): the writer's lease stands at every live location.
+func (r movesRetired) retiredGenerations(versions []lease.ObjectVersion) error {
+ h := r.holder
+ refuse := func(gen, format string, args ...any) error {
+ return &NotMovesEvidence{Host: r.host, Generation: gen, Reason: fmt.Sprintf(format, args...)}
+ }
+ moveAdmission := -1
+ for i := len(h.Admissions) - 1; i >= 0; i-- {
+ if h.Admissions[i].Generation == r.moveID {
+ moveAdmission = i
+ break
+ }
+ }
+ afterTheMove := map[string]bool{}
+ if moveAdmission >= 0 {
+ for _, a := range h.Admissions[moveAdmission:] {
+ afterTheMove[a.Generation] = true
+ }
+ }
+ for _, v := range versions {
+ if loc, ok := movesInstancesLocation(r.host, v.Key); !ok || loc != r.location || movesParkLock(r.host, v.Key) {
+ continue
+ }
+ gen, ok := landingGenerationOf(r.location, v.Key)
+ switch {
+ case !ok:
+ return refuse("", "%s belongs to no generation", v.Key)
+ case gen == landingMarkerGeneration || gen == h.Generation:
+ continue
+ case moveAdmission < 0:
+ return refuse(gen, "the claim holds no admission of %s to date it by", r.moveID)
+ case afterTheMove[gen]:
+ return refuse(gen, "it was admitted after %s's admission, so written past the close", r.moveID)
+ case v.LastModified.IsZero() || !v.LastModified.Before(r.at):
+ return refuse(gen, "%s was written after %s closed", v.Key, r.moveID)
+ }
+ }
+ return nil
+}
+
+// movesInstancesLocation is the location park.Decide files an
+// instances/<host>/ key under (park's locationOf): a 64-hex first segment
+// with a child is its own incarnation location, and everything else is the
+// bare instances/<host>. ok is false outside instances/<host>/.
+func movesInstancesLocation(host, key string) (string, bool) {
+ base := "instances/" + host
+ tail, ok := strings.CutPrefix(key, base+"/")
+ if !ok {
+ return "", false
+ }
+ first, _, child := strings.Cut(tail, "/")
+ if _, err := hex.DecodeString(first); child && err == nil && len(first) == 64 {
+ return base + "/" + first, true
+ }
+ return base, true
+}
+
+// retired proves the newest retired move from the claim observation, the
+// holder's record and the complete census, or refuses.
+func (e movesRetirementEvidence) retired(ctx context.Context, o hostclaim.Observed, versions []lease.ObjectVersion) (movesRetired, error) {
+ if ctx == nil || e.Records == nil || e.Now == nil {
+ return movesRetired{}, hostwrite.ErrUnavailable
+ }
+ if o.Absent || o.Claim == nil || o.ETag == "" || o.Host == "" || o.Claim.Host != o.Host {
+ return movesRetired{}, hostwrite.ErrUnavailable
+ }
+ c, host := o.Claim, o.Host
+ refuse := func(anomaly bool, format string, args ...any) (movesRetired, error) {
+ 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}}
+ }
+ if c.Op.Kind != hostclaim.KindServing {
+ return refuse(false, "the claim is under a %s op", c.Op.Kind)
+ }
+ h := c.Holder
+ if h.Family != "instance" || h.Key != provision.RecordKey(h.Account, h.App, host) {
+ return refuse(false, "the holder is not an instance record")
+ }
+ // A moved holder is at the bare location, the activator's own lease
+ // (ruling T5-lease, movescut.go writerLease); a scoped holder is at its
+ // incarnation's. Anything else is a location the census cannot place.
+ location := strings.TrimSuffix(h.Location, "/")
+ if location != "instances/"+host && location != "instances/"+host+"/"+h.Incarnation {
+ return refuse(true, "the holder is at %s, neither the bare nor the incarnation location", h.Location)
+ }
+ rec, err := e.Records(ctx, h.Account, h.App, host)
+ if err != nil {
+ return movesRetired{}, err
+ }
+ if rec == nil {
+ return refuse(true, "the holder's record is missing")
+ }
+ inc, err := hostclaim.Incarnation(rec.Account, rec.App, rec.Host, rec.CreatedAt)
+ if err != nil || rec.Account != h.Account || rec.App != h.App || rec.Host != host || inc != h.Incarnation {
+ return refuse(true, "the record is not the holder's incarnation")
+ }
+ if rec.Move != nil {
+ return refuse(false, "the record carries the live move %s", rec.Move.ID)
+ }
+ last := rec.LastMove
+ if last == nil || last.ID == "" {
+ return refuse(false, "the record has closed no move")
+ }
+ if last.At.IsZero() || !last.At.Before(e.Now()) {
+ return refuse(true, "the close of %s has no time before now", last.ID)
+ }
+ movesRoot := "moves/" + host + "/"
+ for _, v := range versions {
+ if strings.HasPrefix(v.Key, "instances/"+host+"/") {
+ continue
+ }
+ tail, ok := strings.CutPrefix(v.Key, movesRoot)
+ if !ok {
+ return refuse(true, "the census holds %s, outside this host", v.Key)
+ }
+ if movesParkLock(host, v.Key) {
+ continue // the moves/<host> park's own lease
+ }
+ op, _, inLane := strings.Cut(tail, "/")
+ if !inLane {
+ return refuse(true, "%s is outside every lane", v.Key)
+ }
+ if op == last.ID {
+ continue
+ }
+ if v.LastModified.IsZero() || !v.LastModified.Before(last.At) {
+ return refuse(true, "lane %s was written after %s closed", op, last.ID)
+ }
+ }
+ return movesRetired{host: host, incarnation: h.Incarnation, location: location, moveID: last.ID, at: last.At.UTC(), holder: *h}, nil
+}
+
+// movesParkLock mirrors the park engine's lock predicate (park's isLock over
+// locationOf, both unexported): a key is a lock exactly when it is the
+// lock.json at the root of its own park location, instances/<host>,
+// instances/<host>/<incarnation> or moves/<host>. That is the writer's
+// lease at a holder location (lease.Acquire) and a park's own lease.
+// Decide never makes it a candidate, so the close need not vouch for it.
+// TestMovesParkLockMatchesTheEngine keeps the two in step through
+// park.Decide.
+func movesParkLock(host, key string) bool {
+ loc, ok := movesInstancesLocation(host, key)
+ if !ok {
+ if !strings.HasPrefix(key, "moves/"+host+"/") {
+ return false
+ }
+ loc = "moves/" + host
+ }
+ return key == loc+"/lock.json"
+}
diff --git a/internal/host/movesevidence_test.go b/internal/host/movesevidence_test.go
new file mode 100644
index 000000000..b6c3153b0
--- /dev/null
+++ b/internal/host/movesevidence_test.go
@@ -0,0 +1,897 @@
+package host
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/carlosframework/platform/internal/hostbox"
+ "github.com/carlosframework/platform/internal/hostclaim"
+ "github.com/carlosframework/platform/internal/hostops"
+ "github.com/carlosframework/platform/internal/lease"
+ "github.com/carlosframework/platform/internal/lease/leasetest"
+ "github.com/carlosframework/platform/internal/park"
+ "github.com/carlosframework/platform/internal/provision"
+ "github.com/carlosframework/platform/internal/release"
+)
+
+// The moves retirement evidence provider (Plan 5 Task 5, rulings K10, M7,
+// M8). The fake bucket's clock starts at 2026-10-05T00:00:00Z and ticks a
+// second per write, so every object a fixture writes is older than evAt.
+var (
+ evCreated = time.Date(2026, 9, 1, 0, 0, 0, 0, time.UTC)
+ evAt = time.Date(2026, 10, 5, 1, 0, 0, 0, time.UTC) // LastMove.At, the close
+ evNow = time.Date(2026, 10, 5, 2, 0, 0, 0, time.UTC) // the sweep's clock
+)
+
+const (
+ evHost = "ev.example"
+ evAcct = "acct"
+ evApp = "app"
+ evMoveID = "mv_retired"
+)
+
+type evFixture struct {
+ inc, location string
+ claims release.Store
+ bucket *leasetest.VersionedBucket
+ rec *provision.Record
+ reads int
+}
+
+// newEvFixture is a serving claim released by evMoveID, whose record closed
+// that move at evAt and carries no live move.
+func newEvFixture(t *testing.T) *evFixture {
+ t.Helper()
+ inc, err := hostclaim.Incarnation(evAcct, evApp, evHost, evCreated)
+ if err != nil {
+ t.Fatal(err)
+ }
+ f := &evFixture{inc: inc, location: "instances/" + evHost + "/" + inc, claims: release.NewDirStore(t.TempDir()), bucket: leasetest.NewVersionedBucket()}
+ f.bucket.AddBucket("data")
+ f.rec = &provision.Record{Schema: 1, Host: evHost, App: evApp, Account: evAcct, CreatedAt: evCreated,
+ LastMove: &provision.MoveOutcome{ID: evMoveID, Outcome: "done", At: evAt}}
+ f.setClaim(t, func(c *hostclaim.Claim) {
+ c.Holder = &hostclaim.Holder{Family: "instance", Key: provision.RecordKey(evAcct, evApp, evHost), Account: evAcct, App: evApp, Incarnation: inc, CreatedAt: evCreated, Location: f.location, Stamp: 2, Session: 2, WriterBox: "box", Generation: evMoveID,
+ Admissions: evAdmissions(evMoveID)}
+ c.Op = servingReleasedBy(evMoveID)
+ c.Touched = hostclaim.Touched{Complete: true, Regions: []string{"ie"}}
+ })
+ return f
+}
+
+// evAdmissions is the holder's admission history: an origin generation
+// "old", then the move's prepared landing, which admits the generation
+// named after the move (movesprobeowners.go), then any later generations,
+// each the next one's predecessor. The last is the holder's current.
+func evAdmissions(gens ...string) []hostclaim.AdmissionRecord {
+ out := []hostclaim.AdmissionRecord{{Session: 1, Box: "box", Generation: "old", Mode: "cold", Region: "ie"}}
+ for i, g := range gens {
+ out = append(out, hostclaim.AdmissionRecord{Session: uint64(i + 2), Box: "box", Generation: g, Predecessor: out[i].Generation, Mode: "cold", Region: "ie", PredecessorRegion: "ie"})
+ }
+ return out
+}
+
+// genKey is an object of generation gen at the fixture's live location.
+func (f *evFixture) genKey(gen string) string { return f.location + "/g-" + gen + "/0000/db" }
+
+// servingReleasedBy is the serving op hostclaim.ReleaseMove leaves.
+func servingReleasedBy(id string) hostclaim.Op {
+ return hostclaim.Op{Kind: hostclaim.KindServing, Exec: "box", Journal: map[string]string{"released_move": id}}
+}
+
+func (f *evFixture) setClaim(t *testing.T, mut func(*hostclaim.Claim)) {
+ t.Helper()
+ obs, err := hostclaim.Read(t.Context(), f.claims, evHost)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if _, err := hostclaim.Update(t.Context(), f.claims, obs, func(c *hostclaim.Claim) error { mut(c); return nil }); err != nil {
+ t.Fatal(err)
+ }
+}
+
+func (f *evFixture) observe(t *testing.T) hostclaim.Observed {
+ t.Helper()
+ obs, err := hostclaim.Read(t.Context(), f.claims, evHost)
+ if err != nil {
+ t.Fatal(err)
+ }
+ return obs
+}
+
+func (f *evFixture) census(t *testing.T) []lease.ObjectVersion {
+ t.Helper()
+ var out []lease.ObjectVersion
+ for _, p := range []string{"instances/" + evHost + "/", "moves/" + evHost + "/"} {
+ vs, err := f.bucket.ListVersions(t.Context(), "data", p)
+ if err != nil {
+ t.Fatal(err)
+ }
+ out = append(out, vs...)
+ }
+ return out
+}
+
+func (f *evFixture) provider() movesRetirementEvidence {
+ return movesRetirementEvidence{
+ Records: func(_ context.Context, acct, app, host string) (*provision.Record, error) {
+ f.reads++
+ if acct != evAcct || app != evApp || host != evHost {
+ return nil, fmt.Errorf("read %s/%s/%s", acct, app, host)
+ }
+ if f.rec == nil {
+ return nil, errors.New("no record")
+ }
+ cp := *f.rec
+ return &cp, nil
+ },
+ Now: func() time.Time { return evNow },
+ }
+}
+
+// lane writes one object of a move's staging lane.
+func (f *evFixture) lane(op, name string) {
+ f.bucket.Put("data", "moves/"+evHost+"/"+op+"/"+name, []byte(op))
+}
+
+func wantRefusal(t *testing.T, err error, anomaly bool) {
+ t.Helper()
+ var r *MovesSweepRefusal
+ if !errors.As(err, &r) {
+ t.Fatalf("err = %v, want a *MovesSweepRefusal", err)
+ }
+ if r.Host != evHost || r.Reason == "" || r.Anomaly != anomaly {
+ t.Fatalf("refusal = %+v, want host %s, a reason and anomaly %v", r, evHost, anomaly)
+ }
+}
+
+// bothRefuse asserts the two methods refuse alike: VerifyRetirement's
+// label is the proof's, so neither may answer where the other refuses.
+func bothRefuse(t *testing.T, f *evFixture, anomaly bool) {
+ t.Helper()
+ obs, census := f.observe(t), f.census(t)
+ if _, err := f.provider().VerifyMovesSweep(t.Context(), obs, census); err == nil {
+ t.Fatal("VerifyMovesSweep answered")
+ } else {
+ wantRefusal(t, err, anomaly)
+ }
+ inc, at, err := f.provider().VerifyRetirement(t.Context(), obs, census)
+ if err == nil || inc != "" || !at.IsZero() {
+ t.Fatalf("VerifyRetirement = %q, %v, %v; want a refusal and no evidence", inc, at, err)
+ }
+ wantRefusal(t, err, anomaly)
+}
+
+// TestMovesSweepProofRetiredAtIsTheClose (M8): the proof binds the exact
+// claim observation, names LastMove's id, the holder's incarnation and the
+// close's time, and the engine's Check accepts it only for a listing that
+// started after that close.
+func TestMovesSweepProofRetiredAtIsTheClose(t *testing.T) {
+ f := newEvFixture(t)
+ f.lane(evMoveID, "move.json")
+ f.lane(evMoveID, "replica/0000/db")
+ f.bucket.Put("data", "moves/"+evHost+"/lock.json", []byte("lease"))
+ obs := f.observe(t)
+ proof, err := f.provider().VerifyMovesSweep(t.Context(), obs, f.census(t))
+ if err != nil {
+ t.Fatal(err)
+ }
+ want := park.MoveSweepProof{Host: evHost, ClaimETag: obs.ETag, MoveID: evMoveID, Incarnation: f.inc, RetiredAt: evAt}
+ if proof != want {
+ t.Fatalf("proof = %+v\nwant %+v", proof, want)
+ }
+ if err := proof.Check(obs, f.inc, evNow); err != nil {
+ t.Fatalf("a listing after the close: %v", err)
+ }
+ for _, listing := range []time.Time{evAt, evAt.Add(-time.Second)} {
+ if err := proof.Check(obs, f.inc, listing); !errors.Is(err, park.ErrUnsafe) {
+ t.Fatalf("a listing at %v, not after the close %v: %v", listing, evAt, err)
+ }
+ }
+}
+
+// TestVerifyRetirementLabelsMovesResidueWithTheProofsIncarnation (M7):
+// Decide labels every moves/<host> candidate with VerifyRetirement's
+// answer, and Check requires the proof's incarnation to equal it, so the
+// two answers come from one source. The evidence time is the close's, never
+// zero and never the clock's.
+func TestVerifyRetirementLabelsMovesResidueWithTheProofsIncarnation(t *testing.T) {
+ f := newEvFixture(t)
+ f.lane(evMoveID, "move.json")
+ f.bucket.Put("data", f.genKey(evMoveID), []byte("live"))
+ obs, census := f.observe(t), f.census(t)
+ inc, at, err := f.provider().VerifyRetirement(t.Context(), obs, census)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if inc != f.inc || !at.Equal(evAt) {
+ t.Fatalf("VerifyRetirement = %q, %v; want %q, %v", inc, at, f.inc, evAt)
+ }
+ proof, err := f.provider().VerifyMovesSweep(t.Context(), obs, census)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if proof.Incarnation != inc {
+ t.Fatalf("label %q, proof %q", inc, proof.Incarnation)
+ }
+ if err := proof.Check(obs, inc, evNow); err != nil {
+ t.Fatal(err)
+ }
+}
+
+// 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) {
+ 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",
+ } {
+ 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)
+ }
+ if _, err := f.provider().VerifyMovesSweep(t.Context(), obs, census); err != nil {
+ t.Fatalf("the moves proof does not depend on instances residue: %v", 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)
+ }
+ })
+}
+
+// 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.
+func (f *evFixture) bare(t *testing.T) {
+ t.Helper()
+ f.location = "instances/" + evHost
+ f.setClaim(t, func(c *hostclaim.Claim) { c.Holder.Location = f.location })
+}
+
+// TestMovedHostAtABareLocationIsSwept (controller finding, Plan 12 review):
+// a moved host's holder is at the bare location, and its objects there are
+// its live location's, not residue outside it. The provider answers, and
+// the worker queues the moves/<host> lane chore.
+func TestMovedHostAtABareLocationIsSwept(t *testing.T) {
+ f := newEvFixture(t)
+ f.bare(t)
+ f.lane(evMoveID, "move.json")
+ f.lane(evMoveID, "replica/0000/db")
+ f.bucket.Put("data", f.genKey(evMoveID), []byte("live"))
+ obs, census := f.observe(t), f.census(t)
+ inc, at, err := f.provider().VerifyRetirement(t.Context(), obs, census)
+ if err != nil || inc != f.inc || !at.Equal(evAt) {
+ t.Fatalf("VerifyRetirement = %q, %v, %v; want %q, %v", inc, at, err, f.inc, evAt)
+ }
+ proof, err := f.provider().VerifyMovesSweep(t.Context(), obs, census)
+ if err != nil || proof.Incarnation != f.inc || !proof.RetiredAt.Equal(evAt) {
+ t.Fatalf("proof = %+v, %v", proof, err)
+ }
+ if err := f.worker(t, evNow).Run(t.Context()); !errors.Is(err, park.ErrPending) {
+ t.Fatalf("the lane chore waits for its horizon: %v", err)
+ }
+ moves, ok := f.chores(t)["moves/"+evHost]
+ if !ok || moves.Incarnation != f.inc || !moves.EvidenceAt.Equal(evNow) {
+ t.Fatalf("chores = %+v; want the moves/<host> chore at the listing's end", f.chores(t))
+ }
+}
+
+// TestVerifyRetirementVouchesOnlyForWhatTheMoveRetired (fix round 1,
+// ruling 1): the close is evidence only for what the move retired. At the
+// live location that is a generation superseded at or before the move's
+// own admission, every version of it older than the close. A generation
+// admitted from the move's admission on was written past the close, so the
+// provider has no evidence for it and says so with a typed error; the
+// moves lane proof is unaffected.
+func TestVerifyRetirementVouchesOnlyForWhatTheMoveRetired(t *testing.T) {
+ answers := func(t *testing.T, f *evFixture) {
+ t.Helper()
+ inc, at, err := f.provider().VerifyRetirement(t.Context(), f.observe(t), f.census(t))
+ if err != nil || inc != f.inc || !at.Equal(evAt) {
+ t.Fatalf("VerifyRetirement = %q, %v, %v; want %q, %v", inc, at, err, f.inc, evAt)
+ }
+ }
+ notEvidence := func(t *testing.T, f *evFixture, gen string) {
+ t.Helper()
+ obs, census := f.observe(t), f.census(t)
+ inc, at, err := f.provider().VerifyRetirement(t.Context(), obs, census)
+ var ne *NotMovesEvidence
+ if !errors.As(err, &ne) || inc != "" || !at.IsZero() {
+ t.Fatalf("VerifyRetirement = %q, %v, %v; want a *NotMovesEvidence and no evidence", inc, at, err)
+ }
+ if ne.Host != evHost || ne.Generation != gen || ne.Reason == "" {
+ t.Fatalf("refusal = %+v, want generation %q", ne, gen)
+ }
+ if _, err := f.provider().VerifyMovesSweep(t.Context(), obs, census); err != nil {
+ t.Fatalf("the lane proof does not depend on the generations: %v", err)
+ }
+ }
+ t.Run("the current generation and the marker", func(t *testing.T) {
+ f := newEvFixture(t)
+ f.bucket.Put("data", f.genKey(evMoveID), []byte("live"))
+ f.bucket.Put("data", f.location+"/generation.json", []byte("marker"))
+ answers(t, f)
+ })
+ t.Run("a generation the move's admission superseded", func(t *testing.T) {
+ f := newEvFixture(t)
+ f.bucket.Put("data", f.genKey("old"), []byte("old"))
+ f.bucket.Put("data", f.location+"/files/g-old/a.txt", []byte("old"))
+ answers(t, f)
+ })
+ t.Run("the move's generation superseded after the close", func(t *testing.T) {
+ f := newEvFixture(t)
+ f.setClaim(t, func(c *hostclaim.Claim) {
+ c.Holder.Admissions, c.Holder.Generation, c.Holder.Session, c.Holder.Stamp = evAdmissions(evMoveID, "later"), "later", 3, 3
+ })
+ f.bucket.Put("data", f.genKey(evMoveID), []byte("superseded after the close"))
+ f.bucket.Put("data", f.genKey("later"), []byte("live"))
+ notEvidence(t, f, evMoveID)
+ })
+ t.Run("a later generation superseded after the close", func(t *testing.T) {
+ f := newEvFixture(t)
+ f.setClaim(t, func(c *hostclaim.Claim) {
+ c.Holder.Admissions, c.Holder.Generation, c.Holder.Session, c.Holder.Stamp = evAdmissions(evMoveID, "later", "latest"), "latest", 4, 4
+ })
+ f.bucket.Put("data", f.genKey("later"), []byte("superseded after the close"))
+ notEvidence(t, f, "later")
+ })
+ t.Run("no admission of the move in the claim", func(t *testing.T) {
+ f := newEvFixture(t)
+ f.setClaim(t, func(c *hostclaim.Claim) {
+ c.Holder.Admissions, c.Holder.Generation = evAdmissions("fresh"), "fresh"
+ })
+ f.bucket.Put("data", f.genKey("old"), []byte("old"))
+ notEvidence(t, f, "old")
+ })
+ t.Run("a retired generation written after the close", func(t *testing.T) {
+ f := newEvFixture(t)
+ f.rec.LastMove.At = time.Date(2026, 10, 4, 0, 0, 0, 0, time.UTC)
+ f.bucket.Put("data", f.genKey("old"), []byte("late"))
+ notEvidence(t, f, "old")
+ })
+ t.Run("an object no generation owns", func(t *testing.T) {
+ f := newEvFixture(t)
+ f.bucket.Put("data", f.location+"/stray.bin", []byte("?"))
+ notEvidence(t, f, "")
+ })
+}
+
+// TestMovesSweepProofRefusesALiveMove (K10, M8): no proof while a move op
+// is on the claim, while the record carries a live move, or while the
+// claim was released by a move the record has not closed.
+func TestMovesSweepProofRefusesALiveMove(t *testing.T) {
+ t.Run("the record carries a live move", func(t *testing.T) {
+ f := newEvFixture(t)
+ f.rec.Move = &provision.MoveState{ID: "mv_next", Incarnation: f.inc, To: "uk", State: provision.MovePreparing}
+ f.lane("mv_next", "move.json")
+ bothRefuse(t, f, false)
+ })
+ for _, op := range []hostclaim.Op{
+ {Kind: hostclaim.KindMoving, ID: "mv_next", Phase: hostclaim.PhaseMoving, Exec: "box"},
+ {Kind: hostclaim.KindLanding, ID: evMoveID, Phase: hostclaim.PhaseCommitted, Exec: "box"},
+ {Kind: hostclaim.KindRestoring, ID: evMoveID, Phase: hostclaim.PhaseOpened, Exec: "box"},
+ {Kind: hostclaim.KindReverting, ID: evMoveID, Phase: hostclaim.PhaseOpened, Exec: "box"},
+ } {
+ // The last three are the record-first close's window (M4): the
+ // record closed the move, the claim has not released it yet.
+ t.Run("the claim holds "+op.Kind, func(t *testing.T) {
+ f := newEvFixture(t)
+ f.setClaim(t, func(c *hostclaim.Claim) { c.Op = op })
+ bothRefuse(t, f, false)
+ })
+ }
+ t.Run("the record names no closed move", func(t *testing.T) {
+ f := newEvFixture(t)
+ f.rec.LastMove = nil
+ bothRefuse(t, f, false)
+ })
+}
+
+// TestMovesProofSurvivesAMetadataPlacement (fix round 1, ruling 2): the
+// durable proof is the record's LastMove, bound to the holder's
+// incarnation, with no move op on the claim. A later operation that
+// replaces the serving op (here CompleteMetadataPlacement, which drops the
+// released_move journal) leaves cleanup running, and so does a serving op
+// released by a move whose record never carried it.
+func TestMovesProofSurvivesAMetadataPlacement(t *testing.T) {
+ t.Run("a metadata placement", func(t *testing.T) {
+ f := newEvFixture(t)
+ f.lane(evMoveID, "move.json")
+ f.bucket.Put("data", f.genKey(evMoveID), []byte("live"))
+ f.setClaim(t, func(c *hostclaim.Claim) {
+ if err := hostops.BeginPlacement("pl_1", "op@example", "meta", evNow, []string{"ie"}, []string{"box"})(c); err != nil {
+ t.Fatal(err)
+ }
+ c.Op.Intent["app_id"] = "app_1"
+ if err := hostops.CompleteMetadataPlacement("pl_1", "app_1")(c); err != nil {
+ t.Fatal(err)
+ }
+ })
+ obs := f.observe(t)
+ if obs.Claim.Op.Kind != hostclaim.KindServing || obs.Claim.Op.Journal["released_move"] != "" {
+ t.Fatalf("the placement left op %+v", obs.Claim.Op)
+ }
+ census := f.census(t)
+ proof, err := f.provider().VerifyMovesSweep(t.Context(), obs, census)
+ if err != nil || proof.MoveID != evMoveID || !proof.RetiredAt.Equal(evAt) {
+ t.Fatalf("proof = %+v, %v", proof, err)
+ }
+ if inc, at, err := f.provider().VerifyRetirement(t.Context(), obs, census); err != nil || inc != f.inc || !at.Equal(evAt) {
+ t.Fatalf("VerifyRetirement = %q, %v, %v", inc, at, err)
+ }
+ if err := f.worker(t, evNow).Run(t.Context()); !errors.Is(err, park.ErrPending) {
+ t.Fatalf("the lane chore waits for its horizon: %v", err)
+ }
+ if _, ok := f.chores(t)["moves/"+evHost]; !ok {
+ t.Fatalf("chores = %+v; want the moves/<host> chore", f.chores(t))
+ }
+ })
+ t.Run("a release by a move the record never carried", func(t *testing.T) {
+ f := newEvFixture(t)
+ f.setClaim(t, func(c *hostclaim.Claim) { c.Op = servingReleasedBy("mv_orphan") })
+ if _, err := f.provider().VerifyMovesSweep(t.Context(), f.observe(t), f.census(t)); err != nil {
+ t.Fatalf("no lane of the orphan, so nothing unproven: %v", err)
+ }
+ f.bucket.Put("data", "moves/"+evHost+"/mv_orphan/move.json", []byte("orphan"))
+ f.rec.LastMove.At = time.Date(2026, 10, 4, 0, 0, 0, 0, time.UTC)
+ bothRefuse(t, f, true)
+ })
+}
+
+// TestMovesEvidenceRefusesAZeroTime (M7, M8): a zero time is never
+// evidence: not the close's, and not a lane object's.
+func TestMovesEvidenceRefusesAZeroTime(t *testing.T) {
+ t.Run("a zero close", func(t *testing.T) {
+ f := newEvFixture(t)
+ f.rec.LastMove.At = time.Time{}
+ bothRefuse(t, f, true)
+ })
+ t.Run("a close not before the provider's clock", func(t *testing.T) {
+ f := newEvFixture(t)
+ f.rec.LastMove.At = evNow
+ bothRefuse(t, f, true)
+ })
+ t.Run("an older lane object with no time", func(t *testing.T) {
+ f := newEvFixture(t)
+ obs := f.observe(t)
+ census := []lease.ObjectVersion{{Key: "moves/" + evHost + "/mv_older/move.json", VersionID: "v1", IsLatest: true}}
+ if _, err := f.provider().VerifyMovesSweep(t.Context(), obs, census); err == nil {
+ t.Fatal("a zero LastModified counted as before the close")
+ } else {
+ wantRefusal(t, err, true)
+ }
+ if _, _, err := f.provider().VerifyRetirement(t.Context(), obs, census); err == nil {
+ t.Fatal("VerifyRetirement answered over a lane with no time")
+ }
+ })
+}
+
+// TestMovesSweepProofRefusesAnUnknownLane (M8): every moves/<host>/<op>/
+// object is the closed move's own, or older than its close. An object of
+// another op written after the close has an unknown writer.
+func TestMovesSweepProofRefusesAnUnknownLane(t *testing.T) {
+ t.Run("an older lane is covered by the close", func(t *testing.T) {
+ f := newEvFixture(t)
+ f.lane("mv_older", "move.json")
+ f.lane("rb_return", "replica/0000/db")
+ if _, err := f.provider().VerifyMovesSweep(t.Context(), f.observe(t), f.census(t)); err != nil {
+ t.Fatal(err)
+ }
+ })
+ t.Run("a lane written after the close", func(t *testing.T) {
+ f := newEvFixture(t)
+ f.rec.LastMove.At = time.Date(2026, 10, 4, 0, 0, 0, 0, time.UTC) // before every write
+ f.lane(evMoveID, "move.json")
+ if _, err := f.provider().VerifyMovesSweep(t.Context(), f.observe(t), f.census(t)); err != nil {
+ t.Fatalf("the closed move's own lane is its own: %v", err)
+ }
+ f.lane("mv_unknown", "move.json")
+ bothRefuse(t, f, true)
+ })
+ t.Run("an object outside any lane", func(t *testing.T) {
+ f := newEvFixture(t)
+ f.bucket.Put("data", "moves/"+evHost+"/stray.json", []byte("?"))
+ bothRefuse(t, f, true)
+ })
+ t.Run("an object outside the host's namespaces", func(t *testing.T) {
+ f := newEvFixture(t)
+ census := []lease.ObjectVersion{{Key: "moves/other.example/" + evMoveID + "/move.json", VersionID: "v1", LastModified: evAt.Add(-time.Hour)}}
+ if _, err := f.provider().VerifyMovesSweep(t.Context(), f.observe(t), census); err == nil {
+ t.Fatal("another host's object counted")
+ }
+ })
+}
+
+// TestMovesEvidenceRefusesAnotherIdentity: the record must be the holder's
+// exact record, and a holderless claim has no record to prove from.
+func TestMovesEvidenceRefusesAnotherIdentity(t *testing.T) {
+ t.Run("a record of another incarnation", func(t *testing.T) {
+ f := newEvFixture(t)
+ f.rec.CreatedAt = evCreated.Add(time.Hour)
+ bothRefuse(t, f, true)
+ })
+ t.Run("a stale observation", func(t *testing.T) {
+ f := newEvFixture(t)
+ obs := f.observe(t)
+ obs.ETag = ""
+ if _, err := f.provider().VerifyMovesSweep(t.Context(), obs, f.census(t)); err == nil {
+ t.Fatal("answered without an etag")
+ }
+ })
+ t.Run("a free claim", func(t *testing.T) {
+ f := newEvFixture(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)
+ }
+ if f.reads != 0 {
+ t.Fatalf("read %d records with no holder to name one", f.reads)
+ }
+ })
+ t.Run("no providers", func(t *testing.T) {
+ f := newEvFixture(t)
+ if _, err := (movesRetirementEvidence{}).VerifyMovesSweep(t.Context(), f.observe(t), nil); err == nil {
+ t.Fatal("answered with no record reader")
+ }
+ if _, _, err := (movesRetirementEvidence{Records: f.provider().Records}).VerifyRetirement(t.Context(), f.observe(t), nil); err == nil {
+ t.Fatal("answered with no clock")
+ }
+ })
+}
+
+// The provider satisfies both slots RegisterProductionClaimMoveParks reads:
+// the regional evidence and, by type assertion, the sweep verifier.
+var (
+ _ hostbox.RegionalEvidence = movesRetirementEvidence{}
+ _ park.MoveSweepVerifier = movesRetirementEvidence{}
+)
+
+// --- against the claim's regional worker, with a fake store ---
+
+type evLease struct{}
+
+func (evLease) Check(context.Context) error { return nil }
+func (evLease) Release(context.Context) error { return nil }
+func (evLease) Close() {}
+
+type evLeases struct{}
+
+func (evLeases) Acquire(context.Context, string, string) (park.HeldLease, error) {
+ return evLease{}, nil
+}
+
+type evEnrollment struct{}
+
+func (evEnrollment) VerifyRegional(context.Context, string, string, time.Duration) error { return nil }
+
+// worker is hostbox.RegionalWorker over park.Engine, wired as the claim's
+// assembly wires them (host/claimassembly.go): the provider is the worker's
+// Evidence and the engine's VerifyMovesSweep. This region (ie) is not the
+// authoritative one, so the holder's location here is residue too.
+func (f *evFixture) worker(t *testing.T, listing time.Time) hostbox.RegionalWorker {
+ t.Helper()
+ seq := 0
+ p := f.provider()
+ location := f.location
+ engine := park.Engine{
+ Store: f.bucket, Claims: f.claims, Bucket: "data", Region: "ie", Leases: evLeases{},
+ Now: func() time.Time { return listing },
+ NewID: func() string { seq++; return fmt.Sprintf("id%d", seq) },
+ View: func(_ context.Context, o hostclaim.Observed) (park.View, error) {
+ return park.View{ClaimETag: o.ETag, Region: "ie", AuthoritativeRegion: "uk",
+ GenerationOf: func(r park.Ref) (string, bool) { return landingGenerationOf(location, r.Key) }}, nil
+ },
+ Authorize: func(context.Context, hostclaim.Observed, hostclaim.Obligation) error { return nil },
+ VerifyMovesSweep: p.VerifyMovesSweep,
+ }
+ return hostbox.RegionalWorker{
+ Regional: hostbox.Regional{Engine: engine, Owner: func(context.Context, hostclaim.Observed) error { return nil }},
+ Enrollment: evEnrollment{},
+ Evidence: p,
+ }
+}
+
+func (f *evFixture) chores(t *testing.T) map[string]hostclaim.ParkScope {
+ t.Helper()
+ out := map[string]hostclaim.ParkScope{}
+ for _, o := range f.observe(t).Claim.Obligations {
+ if o.Park == nil {
+ t.Fatalf("an unscoped chore: %+v", o)
+ }
+ out[o.Park.Location] = *o.Park
+ }
+ return out
+}
+
+// TestAllowedSweepQueuesTheMovesChore: with the proof, the worker queues the
+// moves/<host> chore under the serving session, labelled with the holder's
+// incarnation, its evidence the listing's end, so it waits for its horizon.
+// The holder location's residue in this non-authoritative region takes the
+// close as its evidence: that horizon has passed, so the same pass parks it.
+func TestAllowedSweepQueuesTheMovesChore(t *testing.T) {
+ f := newEvFixture(t)
+ f.lane(evMoveID, "move.json")
+ f.lane("mv_older", "replica/0000/db")
+ left := f.genKey("old") // superseded by the move's own admission
+ f.bucket.Put("data", left, []byte("left behind"))
+ err := f.worker(t, evNow).Run(t.Context())
+ if !errors.Is(err, park.ErrPending) {
+ t.Fatalf("a new sweep waits for its horizon: %v", err)
+ }
+ chores := f.chores(t)
+ moves, ok := chores["moves/"+evHost]
+ if !ok || len(chores) != 1 {
+ t.Fatalf("chores = %+v; want the moves chore alone", chores)
+ }
+ if moves.Incarnation != f.inc || !moves.EvidenceAt.Equal(evNow) || moves.RequestLimit != hostbox.RequestLimit {
+ t.Fatalf("moves chore = %+v; want incarnation %s, evidence %v", moves, f.inc, evNow)
+ }
+ if vs, err := f.bucket.ListVersions(t.Context(), "data", left); err != nil || len(vs) != 0 {
+ t.Fatalf("the location's residue was not parked: %v, %v", vs, err)
+ }
+ if vs, err := f.bucket.ListVersions(t.Context(), "data", "parked/"+f.inc+"/"+evHost+"/"); err != nil || len(vs) == 0 {
+ t.Fatalf("no archive under the holder's incarnation: %v, %v", vs, err)
+ }
+ for _, o := range f.observe(t).Claim.Obligations {
+ if o.Op != "session:2" || o.Owner != "region:ie" {
+ t.Fatalf("chore %+v is not the serving session's regional chore", o)
+ }
+ }
+ if c := f.observe(t).Claim; c.Op.Kind != hostclaim.KindServing || c.Op.Journal["released_move"] != evMoveID {
+ t.Fatalf("the sweep changed the serving op: %+v", c.Op)
+ }
+}
+
+// TestSweepSkipsWhileAMoveStages (Review Focus 3, K10): while a move op is
+// on the claim the worker never asks for evidence; while the record carries
+// a live move under a serving claim the provider refuses and nothing is
+// queued.
+func TestSweepSkipsWhileAMoveStages(t *testing.T) {
+ t.Run("a moving claim", func(t *testing.T) {
+ f := newEvFixture(t)
+ f.setClaim(t, func(c *hostclaim.Claim) {
+ c.Op = hostclaim.Op{Kind: hostclaim.KindMoving, ID: "mv_next", Phase: hostclaim.PhaseMoving, Exec: "box"}
+ })
+ f.rec.Move = &provision.MoveState{ID: "mv_next", Incarnation: f.inc, To: "uk", State: provision.MovePreparing}
+ f.lane("mv_next", "replica/0000/db")
+ if err := f.worker(t, evNow).Run(t.Context()); err != nil {
+ t.Fatal(err)
+ }
+ if len(f.chores(t)) != 0 || f.reads != 0 {
+ t.Fatalf("chores %+v, record reads %d", f.chores(t), f.reads)
+ }
+ })
+ t.Run("a live move on the record", func(t *testing.T) {
+ f := newEvFixture(t)
+ f.rec.Move = &provision.MoveState{ID: "mv_next", Incarnation: f.inc, To: "uk", State: provision.MovePreparing}
+ f.lane("mv_next", "replica/0000/db")
+ err := f.worker(t, evNow).Run(t.Context())
+ var r *MovesSweepRefusal
+ if !errors.As(err, &r) {
+ t.Fatalf("err = %v, want the provider's refusal", err)
+ }
+ if len(f.chores(t)) != 0 {
+ t.Fatalf("chores %+v", f.chores(t))
+ }
+ })
+}
+
+// TestSweepRefusedUnlessTheCloseIsBeforeTheListing (K10): the worker's own
+// Check drops the moves/<host> candidate when the newest retired move's
+// close is not earlier than the listing; the rest of the decision stands.
+func TestSweepRefusedUnlessTheCloseIsBeforeTheListing(t *testing.T) {
+ f := newEvFixture(t)
+ f.lane(evMoveID, "move.json")
+ f.bucket.Put("data", f.genKey("old"), []byte("left behind"))
+ err := f.worker(t, evAt).Run(t.Context())
+ if !errors.Is(err, park.ErrPending) {
+ t.Fatalf("the location chore still queues: %v", err)
+ }
+ chores := f.chores(t)
+ if _, ok := chores["moves/"+evHost]; ok || len(chores) != 1 {
+ t.Fatalf("chores = %+v; want only the location's", chores)
+ }
+ if loc := chores[f.location]; loc.Incarnation != f.inc || !loc.EvidenceAt.Equal(evAt) {
+ t.Fatalf("location chore = %+v; want the close %v as its evidence", loc, evAt)
+ }
+}
+
+// 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.
+func TestSweepScopeMembership(t *testing.T) {
+ f := newEvFixture(t)
+ 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)
+ }
+ if len(f.chores(t)) != 0 {
+ t.Fatalf("chores %+v", f.chores(t))
+ }
+}
+
+// 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) {
+ 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)
+ }
+ if c := f.observe(t).Claim; !c.Free() || len(c.Obligations) != 0 {
+ t.Fatalf("claim = %+v", c)
+ }
+}
+
+// authoritative makes ie the authoritative region, with the marker at the
+// holder's current generation and objects classified by the landing's
+// layout, as Task 9a's view will.
+func (f *evFixture) authoritative(t *testing.T, w *hostbox.RegionalWorker, current string) {
+ t.Helper()
+ marker := f.location + "/generation.json"
+ ref := park.Ref{Key: marker, VersionID: f.bucket.LatestID("data", marker)}
+ location := f.location
+ w.Regional.Engine.View = func(_ context.Context, o hostclaim.Observed) (park.View, error) {
+ return park.View{ClaimETag: o.ETag, Region: "ie", AuthoritativeRegion: "ie", MarkerKnown: true, MarkerGeneration: current, Marker: ref,
+ GenerationOf: func(r park.Ref) (string, bool) { return landingGenerationOf(location, r.Key) }}, nil
+ }
+}
+
+// TestAuthoritativeSweepStampsTheCloseOnlyOnWhatTheMoveRetired (fix round 1,
+// ruling 1): in the authoritative region the engine makes superseded
+// generations park-generation candidates and binds them to
+// VerifyRetirement's time. A generation the move's admission superseded
+// gets the close; when the move's own generation was superseded after the
+// close, the provider refuses and nothing is queued.
+func TestAuthoritativeSweepStampsTheCloseOnlyOnWhatTheMoveRetired(t *testing.T) {
+ listing := evAt.Add(time.Minute) // before the close's horizon, so chores stay queued
+ t.Run("superseded by the move", 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", f.location+"/generation.json", []byte("marker"))
+ f.bucket.Put("data", f.genKey("old"), []byte("superseded by the move"))
+ w := f.worker(t, listing)
+ f.authoritative(t, &w, evMoveID)
+ if err := w.Run(t.Context()); !errors.Is(err, park.ErrPending) {
+ t.Fatalf("the chores wait for their horizons: %v", err)
+ }
+ var gens []hostclaim.Obligation
+ for _, o := range f.observe(t).Claim.Obligations {
+ if o.Kind == "park-generation" {
+ gens = append(gens, o)
+ }
+ }
+ if len(gens) != 1 || gens[0].Generation != "old" || gens[0].Park.Location != f.location || !gens[0].Park.EvidenceAt.Equal(evAt) {
+ t.Fatalf("generation chores = %+v; want one for old at the close %v", gens, evAt)
+ }
+ if moves, ok := f.chores(t)["moves/"+evHost]; !ok || !moves.EvidenceAt.Equal(listing) {
+ t.Fatalf("chores = %+v; want the moves chore at the listing", f.chores(t))
+ }
+ })
+ t.Run("superseded after the close", func(t *testing.T) {
+ f := newEvFixture(t)
+ f.setClaim(t, func(c *hostclaim.Claim) {
+ c.Holder.Admissions, c.Holder.Generation, c.Holder.Session, c.Holder.Stamp = evAdmissions(evMoveID, "later"), "later", 3, 3
+ })
+ f.lane(evMoveID, "move.json")
+ f.bucket.Put("data", f.genKey("later"), []byte("live"))
+ f.bucket.Put("data", f.location+"/generation.json", []byte("marker"))
+ f.bucket.Put("data", f.genKey(evMoveID), []byte("written until after the close"))
+ w := f.worker(t, listing)
+ f.authoritative(t, &w, "later")
+ err := w.Run(t.Context())
+ var ne *NotMovesEvidence
+ if !errors.As(err, &ne) || ne.Generation != evMoveID {
+ t.Fatalf("err = %v, want a *NotMovesEvidence for %s", err, evMoveID)
+ }
+ if len(f.observe(t).Claim.Obligations) != 0 {
+ t.Fatalf("chores %+v", f.observe(t).Claim.Obligations)
+ }
+ })
+}
+
+// TestMovesParkLockMatchesTheEngine (fix round 2): the provider mirrors the
+// park engine's unexported lock predicate (park's isLock over locationOf),
+// so it skips exactly the lease objects Decide skips. park.Decide is the
+// oracle: over a one-key census in a non-authoritative region, where every
+// non-lock object is a candidate, it decides nothing exactly for a lock.
+func TestMovesParkLockMatchesTheEngine(t *testing.T) {
+ f := newEvFixture(t)
+ obs := f.observe(t)
+ view := park.View{ClaimETag: obs.ETag, Region: "ie", AuthoritativeRegion: "uk",
+ GenerationOf: func(r park.Ref) (string, bool) { return landingGenerationOf(f.location, r.Key) }}
+ other := strings.Repeat("ab", 32)
+ for _, key := range []string{
+ "instances/" + evHost + "/lock.json",
+ "instances/" + evHost + "/" + f.inc + "/lock.json",
+ "instances/" + evHost + "/" + other + "/lock.json",
+ "moves/" + evHost + "/lock.json",
+ "moves/" + evHost + "/" + evMoveID + "/lock.json",
+ "instances/" + evHost + "/g-old/lock.json",
+ "instances/" + evHost + "/" + f.inc + "/g-old/lock.json",
+ "instances/" + evHost + "/lock.json.old",
+ "instances/" + evHost + "/0000/db",
+ } {
+ d, err := park.Decide(obs, view, []lease.ObjectVersion{{Key: key, VersionID: "v1", LastModified: evAt.Add(-time.Hour)}}, f.inc)
+ if err != nil {
+ t.Fatalf("%s: %v", key, err)
+ }
+ if engine, mirror := len(d.Candidates) == 0, movesParkLock(evHost, key); engine != mirror {
+ t.Errorf("%s: the engine skips it %v, the provider's mirror says lock %v", key, engine, mirror)
+ }
+ }
+}
+
+// TestLiveLocationLeaseIsSweptNormally (fix round 2): the writer's lease,
+// <holder.location>/lock.json (lease.Acquire), stands at every live
+// location. It is the engine's to skip, never residue the close must
+// vouch for, so the sweep proceeds, scoped and bare alike.
+func TestLiveLocationLeaseIsSweptNormally(t *testing.T) {
+ for name, bare := range map[string]bool{"scoped": false, "bare": true} {
+ t.Run(name, func(t *testing.T) {
+ f := newEvFixture(t)
+ if bare {
+ f.bare(t)
+ }
+ f.lane(evMoveID, "move.json")
+ f.bucket.Put("data", f.genKey(evMoveID), []byte("live"))
+ f.bucket.Put("data", f.location+"/generation.json", []byte("marker"))
+ f.bucket.Put("data", f.location+"/lock.json", []byte("writer lease"))
+ if _, _, err := f.provider().VerifyRetirement(t.Context(), f.observe(t), f.census(t)); err != nil {
+ t.Fatalf("VerifyRetirement refused the writer's lease: %v", err)
+ }
+ listing := evAt.Add(time.Minute)
+ w := f.worker(t, listing)
+ f.authoritative(t, &w, evMoveID)
+ if err := w.Run(t.Context()); !errors.Is(err, park.ErrPending) {
+ t.Fatalf("the lane chore waits for its horizon: %v", err)
+ }
+ chores := f.chores(t)
+ if moves, ok := chores["moves/"+evHost]; !ok || len(chores) != 1 || !moves.EvidenceAt.Equal(listing) {
+ t.Fatalf("chores = %+v; want the moves chore alone", chores)
+ }
+ if vs, err := f.bucket.ListVersions(t.Context(), "data", f.location+"/lock.json"); err != nil || len(vs) != 1 {
+ t.Fatalf("the lease was touched: %v, %v", vs, err)
+ }
+ })
+ }
+}
diff --git a/internal/provision/plan9_b8_census_test.go b/internal/provision/plan9_b8_census_test.go
index 258957971..e5620231e 100644
--- a/internal/provision/plan9_b8_census_test.go
+++ b/internal/provision/plan9_b8_census_test.go
@@ -513,6 +513,12 @@ 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"},
// Copies config acknowledgements to moves/<account>/<app>/<host>/<op>/config-ack-source.json.
{"internal/host/movesfinal.go", "sourceFinal.configAck", "nonrecord", "sel:Put", "cca53109e6945a73b7f04705d8965b3b0997f2b7d2a82fa6d86cffa4b4174e93"},
// Stamps source_parked_at through SetRecordMove, updateRecord and WriteRecord.