carlos / platform Public

sim: a dirty return ends the source's files protection, and its install parks the pre-move generations, only when the target admitted the move (Astra joint round 4 item 2)

eaa08175 treated every dirty return as having a target admission, but
src_covered false makes a return dirty when the target never admitted
(releaseTarget's seal-return). movesinstall.go's targetAdmitted (1b-next)
decides from the claim's recorded admissions alone: with none of the
move's generation, holder.generation is still the pre-move one, the
source's live state, and the return's install parks nothing.

The model mirrors it: targetAdmitted (an admission whose generation is the
move's op id); dirtyReturned requires it in filesHome and in the oracle
(servableRegions reads the admissions itself, whatever the rules), and the
install-park step parks nothing without it. New rule
dirty-return-needs-admission drops the check on the protocol side; witness
wDirtyReturnWithoutAdmission (the source's write unreplicated through the
seal, the target stalled before its probe, the landing rolled back dirty)
holds with the rule and violates INV-30 without it.
TestFilesWitnessesStuckWithoutINV30 skips the new witness: its loss has no
liveness consequence in the scenario, so INV-30 is its only oracle.

One-box COUNTERS and traces unchanged; 300-seed two-box COUNTERS unchanged;
unmutated 3,000 two-box seeds, 0 failed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev 3b895b42f5edcd1e326685b8d165ff8725f93332 parent eaa0817
6 files changed, +94 −8
  • internal/sim/hostlife/moves_test.go +9 −0
  • internal/sim/hostlife/regionha_completion_witness_test.go +44 −0
  • internal/sim/hostlife/regionha_test.go +33 −7
  • internal/sim/hostlife/regionha_witness_test.go +5 −1
  • internal/sim/hostlife/rules_test.go +1 −0
  • internal/sim/hostlife/witness_test.go +2 −0

CI passed — run details

CI log
=== gate ===
go: downloading github.com/aws/aws-sdk-go-v2/service/sns v1.43.0
go: downloading github.com/oschwald/maxminddb-golang v1.13.1
go: downloading github.com/klauspost/compress v1.18.0
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	320.993s
?   	github.com/carlosframework/platform/cmd/changelog	[no test files]
ok  	github.com/carlosframework/platform/cmd/console	3.066s
ok  	github.com/carlosframework/platform/cmd/moveprobe	92.244s
ok  	github.com/carlosframework/platform/internal/activator	208.925s
ok  	github.com/carlosframework/platform/internal/agentsock	0.020s
ok  	github.com/carlosframework/platform/internal/alerting	0.003s
ok  	github.com/carlosframework/platform/internal/analytics	0.019s
ok  	github.com/carlosframework/platform/internal/appconfig	2.251s
ok  	github.com/carlosframework/platform/internal/apperr	0.077s
ok  	github.com/carlosframework/platform/internal/applog	0.140s
ok  	github.com/carlosframework/platform/internal/awsinv	0.006s
ok  	github.com/carlosframework/platform/internal/boxchoice	0.005s
ok  	github.com/carlosframework/platform/internal/boxid	0.995s
?   	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.008s
ok  	github.com/carlosframework/platform/internal/caps	0.012s
ok  	github.com/carlosframework/platform/internal/certwarm	0.002s
ok  	github.com/carlosframework/platform/internal/cgroup	0.193s
ok  	github.com/carlosframework/platform/internal/changelog	0.260s
ok  	github.com/carlosframework/platform/internal/clamd	0.054s
?   	github.com/carlosframework/platform/internal/cliflag	[no test files]
ok  	github.com/carlosframework/platform/internal/console	356.205s
?   	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	5.554s
ok  	github.com/carlosframework/platform/internal/dbdoor/dbdoortest	0.014s
ok  	github.com/carlosframework/platform/internal/dbverify	0.014s
ok  	github.com/carlosframework/platform/internal/dnsrec	0.014s
ok  	github.com/carlosframework/platform/internal/docsite	0.014s
ok  	github.com/carlosframework/platform/internal/domains	1.974s
ok  	github.com/carlosframework/platform/internal/doorkey	0.014s
ok  	github.com/carlosframework/platform/internal/economics	0.014s
ok  	github.com/carlosframework/platform/internal/edge	25.768s
?   	github.com/carlosframework/platform/internal/edgeproof	[no test files]
ok  	github.com/carlosframework/platform/internal/edgesfile	0.014s
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.015s
ok  	github.com/carlosframework/platform/internal/geo	0.019s
ok  	github.com/carlosframework/platform/internal/geoip	0.033s
ok  	github.com/carlosframework/platform/internal/host	54.726s
ok  	github.com/carlosframework/platform/internal/hostactors	0.572s
ok  	github.com/carlosframework/platform/internal/hostbox	1.283s
ok  	github.com/carlosframework/platform/internal/hostclaim	0.274s
ok  	github.com/carlosframework/platform/internal/hostevidence	0.019s
ok  	github.com/carlosframework/platform/internal/hostexec	0.309s
ok  	github.com/carlosframework/platform/internal/hostfence	0.046s
ok  	github.com/carlosframework/platform/internal/hostfiles	4.136s
ok  	github.com/carlosframework/platform/internal/hostfloor	0.024s
ok  	github.com/carlosframework/platform/internal/hostguard	0.146s
ok  	github.com/carlosframework/platform/internal/hostops	1.370s
?   	github.com/carlosframework/platform/internal/hostops/fencerelease	[no test files]
ok  	github.com/carlosframework/platform/internal/hostops/internal/fencerelease	0.017s
ok  	github.com/carlosframework/platform/internal/hostquarantine	0.002s
ok  	github.com/carlosframework/platform/internal/hostresolve	0.151s
ok  	github.com/carlosframework/platform/internal/hostwrite	0.003s
ok  	github.com/carlosframework/platform/internal/infraview	0.002s
ok  	github.com/carlosframework/platform/internal/instancectl	1.703s
ok  	github.com/carlosframework/platform/internal/instanceexec	2.696s
ok  	github.com/carlosframework/platform/internal/instancefs	0.003s
ok  	github.com/carlosframework/platform/internal/instancekey	0.002s
ok  	github.com/carlosframework/platform/internal/instanceupdates	0.013s
ok  	github.com/carlosframework/platform/internal/layout	1.327s
ok  	github.com/carlosframework/platform/internal/lease	9.432s
ok  	github.com/carlosframework/platform/internal/lease/leasetest	0.007s
ok  	github.com/carlosframework/platform/internal/ledger	0.011s
ok  	github.com/carlosframework/platform/internal/mail	0.148s
ok  	github.com/carlosframework/platform/internal/maintenance	0.079s
ok  	github.com/carlosframework/platform/internal/maintenanceexec	5.525s
ok  	github.com/carlosframework/platform/internal/maintenanceguard	0.068s
ok  	github.com/carlosframework/platform/internal/metrics	0.005s
ok  	github.com/carlosframework/platform/internal/moves	21.007s
ok  	github.com/carlosframework/platform/internal/moves/archive	0.734s
ok  	github.com/carlosframework/platform/internal/objstore	0.158s
ok  	github.com/carlosframework/platform/internal/ops	0.155s
ok  	github.com/carlosframework/platform/internal/park	0.188s
ok  	github.com/carlosframework/platform/internal/privatefile	0.003s
ok  	github.com/carlosframework/platform/internal/provision	3.127s
ok  	github.com/carlosframework/platform/internal/publicrelease	0.011s
?   	github.com/carlosframework/platform/internal/regionname	[no test files]
ok  	github.com/carlosframework/platform/internal/registry	3.674s
ok  	github.com/carlosframework/platform/internal/relay	7.226s
ok  	github.com/carlosframework/platform/internal/release	19.472s
ok  	github.com/carlosframework/platform/internal/rollout	3.224s
ok  	github.com/carlosframework/platform/internal/rolloutwords	0.003s
ok  	github.com/carlosframework/platform/internal/rootaws	0.007s
ok  	github.com/carlosframework/platform/internal/s3compat	0.005s
ok  	github.com/carlosframework/platform/internal/safefile	0.004s
ok  	github.com/carlosframework/platform/internal/schedule	2.206s
ok  	github.com/carlosframework/platform/internal/sim/hostlife	8.332s
ok  	github.com/carlosframework/platform/internal/sim/litestreamfacts	4.157s
ok  	github.com/carlosframework/platform/internal/status	0.035s
ok  	github.com/carlosframework/platform/internal/steering	0.006s
ok  	github.com/carlosframework/platform/internal/stopevidence	0.122s
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.001s
ok  	github.com/carlosframework/platform/internal/vet	1.546s
=== 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.022s
=== RUN   TestT4StorageProbeMinio
2026/10/12 20:03:33 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18DDE0D0BA8F9893, 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.020s
minio contract: PASS
diff --git a/internal/sim/hostlife/moves_test.go b/internal/sim/hostlife/moves_test.go
index 56ae4313f..86d143936 100644
--- a/internal/sim/hostlife/moves_test.go
+++ b/internal/sim/hostlife/moves_test.go
@@ -1627,6 +1627,15 @@ func (w *world) recoverSource(b *box, c *claim) {
if !w.evidence(b, op.RollbackID) {
return
}
+ if on("dirty-return-needs-admission") && !targetAdmitted(c) {
+ // movesinstall.go's parking (fix rounds 3 and 4): the target never
+ // admitted this move, so holder.generation is still the pre-move
+ // one, the source's live state; there is no residue, and nothing
+ // is parked. The mutation parks as if the target had admitted
+ w.stats["rollback: dirty return parks nothing, the target never admitted"]++
+ w.cas(c, func(n *claim) bool { n.Op.SrcPhase = "install-copy"; return sameOp(n.Op.ID, op.ID) })
+ return
+ }
if w.parkStep(b, c.Holder.Location, fmt.Sprintf("superseded/%s/%s/return/", op.ID, host), op.StopAt+horizonH, w.opScope(c, c.Holder.Location), w.keepFilesLive(b.Region, confine(c.Holder.Location))) {
w.cas(c, func(n *claim) bool { n.Op.SrcPhase = "install-copy"; return sameOp(n.Op.ID, op.ID) })
}
diff --git a/internal/sim/hostlife/regionha_completion_witness_test.go b/internal/sim/hostlife/regionha_completion_witness_test.go
index 166577cbc..38e2bdba7 100644
--- a/internal/sim/hostlife/regionha_completion_witness_test.go
+++ b/internal/sim/hostlife/regionha_completion_witness_test.go
@@ -724,3 +724,47 @@ func TestDirtyReturnParksPreMoveFiles(t *testing.T) {
w.finish(t)
w.mustHold(t)
}
+
+// wDirtyReturnWithoutAdmission (dirty-return-needs-admission, Astra joint
+// round 4 item 2): the source's last write is not yet replicated at the
+// cut, so the seal does not cover its replica (src_covered false), and the
+// move's target stalls before its probe is admitted, so the landing rolls
+// back. The return is dirty (the seal is returned) although the target
+// never admitted: holder.generation is still the pre-move one, the source
+// keeps the files home, INV-30 counts the source region, and the return's
+// install parks nothing (movesinstall.go's targetAdmitted). Without the
+// admission check the install park retires the manifest the source's next
+// install would pin (INV-30).
+func wDirtyReturnWithoutAdmission(t *testing.T, w *world) {
+ w.twoBox()
+ src, sib := w.boxes[0], w.boxes[3]
+ w.serve(src, 1, app0)
+ w.running(t, src)
+ w.published(t, src)
+ w.appWrite(src) // not replicated before the cut: the seal is all that holds it
+ loc := w.claim.Holder.Location
+ w.holdKey(loc) // the source's uploads stay in flight through the seal
+ w.moveTo(1)
+ if w.claim.Op.Kind != "moving" {
+ t.Fatalf("witness: the move did not start: %s", w.state())
+ }
+ tgt := w.boxByID(w.claim.Op.Tgt)
+ if !w.reach(t, 600, "the flip, before the target's probe", func() bool { return w.claim.Op.Kind == "landing" }, func() { w.worker(src); w.reconcile(src); w.worker(tgt); w.reconcile(tgt); w.runConsole() }) {
+ return
+ }
+ w.hold = nil
+ w.completeHeld(loc)
+ if targetAdmitted(w.claim) || w.claim.Op.SrcCovered {
+ t.Fatalf("witness: not the scenario (target admitted %v, src_covered %v): %s", targetAdmitted(w.claim), w.claim.Op.SrcCovered, w.state())
+ }
+ // the target stalls: the landing's deadline passes with no probe
+ w.drive(w.claim.Op.CutDeadline+12-w.now, never, w.act(sib), w.runConsole, w.recoveryPass, w.operator)
+ if !w.reach(t, 1500, "the dirty return's install past its park", func() bool {
+ c := w.claim
+ return c.Op.Kind == "restoring" && c.Op.Dirty && c.Op.SrcPhase != "waiting" && c.Op.SrcPhase != "claimed" && c.Op.SrcPhase != "install-park" || c.Op.Kind == "serving"
+ }, w.actAll(), w.recoveryPass, w.runConsole) {
+ return
+ }
+ w.reach(t, 1500, "the return serving on the source", func() bool { c := w.claim; return c.Op.Kind == "serving" && c.Op.Exec == src.ID }, w.actAll(), w.recoveryPass, w.runConsole, func() { w.wake(src) })
+ w.finish(t)
+}
diff --git a/internal/sim/hostlife/regionha_test.go b/internal/sim/hostlife/regionha_test.go
index 1b815e678..b8bc19a8e 100644
--- a/internal/sim/hostlife/regionha_test.go
+++ b/internal/sim/hostlife/regionha_test.go
@@ -2165,7 +2165,7 @@ func (w *world) filesLiveSet(c *claim, r int, k string) bool {
// commit, and in a rollback's released phase, a park-generation queued
// during the move archives the live manifest in the source region.
func (w *world) filesHome(c *claim, r int) bool {
- if dirtyReturned(c) && r == c.Op.SrcRegion && on("files-home-move-phases") {
+ if dirtyReturned(c, on("dirty-return-needs-admission")) && r == c.Op.SrcRegion && on("files-home-move-phases") {
return false // D6: the dirty return's install parks the pre-move generations
}
if e := w.execBox(c); e != nil && e.Region == r {
@@ -2207,10 +2207,13 @@ type pinWatch struct {
// op.SrcGen again there); a rollback's source region. Creating,
// deleting, parking and purging have no executor and name none.
func (w *world) servableRegions(c *claim) []int {
- if c == nil || c.Holder == nil || dirtyReturned(c) {
- // after a dirty return the source's next install comes from the
- // return lane, never from a pinned manifest of the pre-move
- // generations, which D6 retires by design (moves a5e8c13f)
+ if c == nil || c.Holder == nil || dirtyReturned(c, true) {
+ // after a dirty return of a move its target admitted, the source's
+ // next install comes from the return lane, never from a pinned
+ // manifest of the pre-move generations, which D6 retires by design
+ // (moves a5e8c13f). The oracle reads the claim's admissions itself,
+ // whatever the rules say: a dirty return the target never admitted
+ // keeps the pre-move generation as the source's live state
return nil
}
var out []int
@@ -2637,6 +2640,29 @@ func (w *world) relocJournalRelease(b *box, c *claim, pending bool) {
// admission decides (moves' movesAuthoritative, a5e8c13f): the target's,
// so the source region is not authoritative, and the return's install
// park retires the pre-move generations (D6).
-func dirtyReturned(c *claim) bool {
- return c != nil && c.Holder != nil && c.Op.Kind == "restoring" && c.Op.Dirty && c.Op.SrcPhase != "waiting" && c.Holder.Gen != c.Op.RollbackID
+func dirtyReturned(c *claim, needAdmission bool) bool {
+ if c == nil || c.Holder == nil || c.Op.Kind != "restoring" || !c.Op.Dirty || c.Op.SrcPhase == "waiting" || c.Holder.Gen == c.Op.RollbackID {
+ return false
+ }
+ return !needAdmission || targetAdmitted(c)
+}
+
+// targetAdmitted is movesinstall.go's targetAdmitted (Astra round 4, item
+// 2): the claim's recorded admissions hold one of this move's generation
+// (the landing's, installGen: the move's op id). A dirty return can come
+// without one: src_covered false makes the return dirty even when the
+// target never admitted (releaseTarget's seal-return), and then
+// holder.generation is still the pre-move one, the source's live state,
+// which the return's install keeps and parks nothing of. Decided from the
+// claim's admissions only, never from the landing phase.
+func targetAdmitted(c *claim) bool {
+ if c == nil || c.Holder == nil {
+ return false
+ }
+ for _, a := range c.Holder.Admissions {
+ if a.Gen == c.Op.ID {
+ return true
+ }
+ }
+ return false
}
diff --git a/internal/sim/hostlife/regionha_witness_test.go b/internal/sim/hostlife/regionha_witness_test.go
index 5b089d40d..f2a017aa7 100644
--- a/internal/sim/hostlife/regionha_witness_test.go
+++ b/internal/sim/hostlife/regionha_witness_test.go
@@ -1262,7 +1262,11 @@ func TestFilesWitnessesStuckWithoutINV30(t *testing.T) {
defer func() { mutated = saved }()
n := 0
for _, x := range witnesses {
- if x.want != "INV-30" {
+ if x.want != "INV-30" || x.rule == "dirty-return-needs-admission" {
+ // dirty-return-needs-admission's loss has no liveness consequence
+ // in its scenario (the dirty return serves from the returned
+ // seal): INV-30, the independent preservation invariant, is its
+ // only oracle (Astra joint round 4)
continue
}
n++
diff --git a/internal/sim/hostlife/rules_test.go b/internal/sim/hostlife/rules_test.go
index 5d1ebacee..9cb452f4c 100644
--- a/internal/sim/hostlife/rules_test.go
+++ b/internal/sim/hostlife/rules_test.go
@@ -222,6 +222,7 @@ var allRules = []string{
"reloc-completed-catchup-hold", // finding 3, ruling 85 (hostbox completedHold, 1b-next a092ddc2): a source whose reconcile runs only after the target completed holds its row for the completed op until its park-local retires it; the mutation leaves the row its release left ready (INV-33; wDelayedSourceHoldsItsRow)
"reloc-completion-fenced-exempt", // ruling 86 (1b-next ca282378: StartGuard's fencedCompletion, ProductionAuthority.completionFence): a delete or purge whose caller verified the completion box's termination fence passes the barrier, as a failover with its fenced source does; the mutation verifies nothing, and a completion box terminated before its discharge while a purge holds admission closed blocks both the purge and the failover for ever (INV-3; wPurgeOverAFencedCompletionBox)
"reloc-probe-stop-flush", // ruling 87 (1b-next 6a4eadde, EndProbe through flushStopped): the probe stop stops the writers, flushes the database and files and records the positions before the activator's fence and root's stop, and with another owner's fence it refuses after the flush; a restart before the record has no replicator, so it refuses until ruling 67's deadline; the mutation stops with nothing flushed, and the first serving wake declares writes the probe session acknowledged (INV-34; wProbeStopFlushes)
+ "dirty-return-needs-admission", // Astra joint round 4 item 2 (moves movesinstall.go's targetAdmitted, movessources.go's restoring case): a dirty return ends the source region's files protection, and its install parks the pre-move generations, only when the claim records an admission of this move's generation by the target; src_covered false makes a return dirty without one, and then the pre-move generation stays the source's live state and nothing is parked; the mutation drops the admission check, and the install park retires the manifest the source's next install would pin (INV-30; wDirtyReturnWithoutAdmission)
"reloc-admits-probe-only", // L18 joint round 2 finding 3 (hostclaim relocatingAdmission): a relocating claim admits only op.exec's cold probe in probing, so between StopWriters' launch-journal closure and Collect's admission fence no wake of the source admits a writer; the mutation admits the source's wake in starting and releasing, as the model did before (TestNoAdmissionBetweenStopAndFence)
"reloc-c1-failed-release-refused", // finding 4, claim ask C1 open (QueueRelocationRelease requires the source in releasing): a live failed target cannot release in probe-failed and reaches stuck at the release deadline, and the operator's resolve stops it; the mutation releases as if C1 had landed (a fidelity guard: TestFailedTargetCannotReleaseWithoutC1)
"reloc-c11-later-attempt-refused", // finding 4, claim ask C11 open (Preflight's ErrLaterAttemptUnlaunchable, ruling 58): after P's opening a later cold probe attempt is refused before it is counted, which is probe-failed; the mutation counts and makes it as if C11 had landed (a fidelity guard: TestLaterAttemptRefusedWithoutC11)
diff --git a/internal/sim/hostlife/witness_test.go b/internal/sim/hostlife/witness_test.go
index 33c36dc7f..ebdce19c6 100644
--- a/internal/sim/hostlife/witness_test.go
+++ b/internal/sim/hostlife/witness_test.go
@@ -291,6 +291,8 @@ var witnesses = []witness{
"finding 3, ruling 85: the source releases, its reconcile is delayed until the target completed, and its next reconcile holds its row for the completed op until its park-local retires it; without the catch-up hold the row its release left ready stays ready", wDelayedSourceHoldsItsRow},
{"reloc-probe-stop-flush", "reloc-probe-stop-flush", "INV-34",
"ruling 87: the probe session acknowledges a write its replicator has not shipped; the probe stop flushes it before root's stop, so the first serving wake, cold, declares nothing the probe session acknowledged; stopped with nothing flushed, that wake declares the write lost", wProbeStopFlushes},
+ {"dirty-return-needs-admission", "dirty-return-needs-admission", "INV-30",
+ "Astra joint round 4: the source's unreplicated write makes the seal src_covered false and the target stalls before its probe, so the landing rolls back dirty with no target admission; the source keeps the files home and its install parks nothing; without the admission check the install park retires the manifest the source's next install would pin", wDirtyReturnWithoutAdmission},
{"reloc-completion-fenced-exempt", "reloc-completion-fenced-exempt", "INV-3",
"ruling 86, seed 2224: a purge closes the app's admission and the completion box is terminated before its discharge; once its fence record exists the purge verifies it, starts over the unmarked completion and frees the host; without the exemption the barrier refuses the purge and the closed admission the failover, for ever", wPurgeOverAFencedCompletionBox},
}