| Earlier finding | Disposition at `d1d9e6cf` |
|---|---|
| R1.1 — Expiry signature accepted as production-off | **Fixed:** no signed expiry object. |
| R1.2 — Existing permanent state passes creation checks | **Not fixed:** route-only apps still pass; new counterexample below. |
| R1.3 — Keep/TTL races expiry | **Fixed:** settle, lock, and recheck positive expiry. |
| R1.4 — Purge misses current deadline | **Declined-with-reason acceptable:** §11 explicitly defers the existing box race; console purge binds to the marker timestamp. |
| R1.5 — Expiry replay after keep | **Fixed:** no bucket expiry authority. |
| R1.6 — Restore bypasses cap | **Fixed:** console restores reserve capacity; direct-mode exception accepted. |
| R1.7 — Restrictions enforced only in CLI | **Fixed for reported paths:** server enforcement specified. |
| R1.8 — Browser/headless restore misses renewal | **Fixed:** shared restore and catch-up renewal. |
| R1.9 — Interrupted purge loses short deadline | **Fixed:** deadline survives in the row. |
| R1.10 — Migration disables expiry | **Declined-with-reason acceptable:** direct migration’s outcome explicitly accepted. |
| R2.1 — Rename brings permanent data under expiry | **Fixed:** shared rename operation refuses temporary apps. |
| R2.2 — Early purge skips email teardown | **Fixed for reported paths:** provisioning refusals and purge backstop. |
| R2.3 — Failed restore cancels purge deadline | **Fixed:** deadline retained until row restore succeeds. |
| R2.4 — Missing marker mistaken for restore | **Fixed for reported case:** empty prefix distinguishes completed purge. |
| R2.5 — Materialization omits deadline | **Fixed:** marker-bound deadline reconstruction specified. |
| R2.6 — Instance provisioning bypasses store prohibition | **Fixed:** manifest and provisioning paths covered. |
| R2.7 — Old console silently creates permanent app | **Fixed:** distinct endpoint and response verification. |
| R2.8 — Expiry bypasses control-plane protection | **Fixed:** creation/setter restrictions and pre-trash check. |
| R3.1 — Expiry condition matches kept app | **Fixed:** positive-expiry check under mutex. |
| R3.2 — Account move brings permanent data under expiry | **Fixed:** shared move operation refuses temporary apps. |
| R3.3 — `expiring` survives restore | **Fixed:** flag removed; settle-first recovery specified. |
| R3.4 — Failed restore frees capacity | **Fixed:** durable `restoring` reservation. |
| R3.5 — Temporary app becomes control plane | **Fixed:** setter restriction and expiry backstop. |
| R3.6 — Listing command/API shape nonexistent | **Fixed:** new command and additive endpoint specified. |
| R4.1 — Recovery bypasses empty-bucket safeguard | **Not fixed completely:** expiry writes can defeat the new safeguard; counterexample below. |
| R4.2 — Trash UI/API promises 30 days | **Fixed:** shared effective deadline and display tests specified. |

1. **P1 — §3: a permanent app represented only by routes can be claimed as temporary.**

   `carlos add` supports a named, account-qualified service backed by an existing TCP upstream, without requiring shipped releases or a console claim ([cli.go:198](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/temporary-sites/cli.go:198)). Such an app can have empty qualified and legacy prefixes.

   The proposed “whole brand-new test” permits this state. `claimApp` checks console ownership and bucket prefixes ([server.go:3472](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/temporary-sites/internal/console/server.go:3472), [server.go:3517](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/temporary-sites/internal/console/server.go:3517)). Its apex check explicitly permits an existing route belonging to the same account/app ([apex.go:92](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/temporary-sites/internal/console/apex.go:92)).

   Create temporary `report` over that permanent service. When it expires, domains reconciliation deletes every matching account/app route, including the original permanent route ([reconcile.go:206](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/temporary-sites/internal/domains/reconcile.go:206)). This violates the brand-new-only ruling without concurrent writes.

   **Fix:** include existing account/app routes in temporary-creation eligibility, including the app’s own apex. Refuse when the required route inventory cannot be established. Test a serving service with no console row and empty bucket prefixes.

2. **P1 — §§2, 5: expiry can manufacture the evidence that defeats the empty-bucket safeguard.**

   Point the console at an incorrect empty bucket with temporary app A awaiting purge and temporary app B still marked live but already expired. Startup materialization preserves A and leaves B live ([materialize.go:311](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/temporary-sites/internal/console/materialize.go:311), [materialize.go:373](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/temporary-sites/internal/console/materialize.go:373)).

   §5 then expires B. `TrashApp` creates its marker without requiring existing app objects ([trash.go:43](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/temporary-sites/internal/release/trash.go:43)). On the following pass, `apps/` is nonempty solely because of that new marker. The §2 tripwire now passes, and A’s absent-marker/absent-objects recovery deletes its claim through `PurgeAppRow` ([store.go:1491](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/temporary-sites/internal/console/store.go:1491)). A’s real objects remain in the correct bucket without their claim or expiry state.

   **Fix:** when the deployment-wide listing trips the empty-bucket safeguard, suspend automatic expiry writes as well as absence-based finalization. Alternatively, verify bucket identity independently of app objects. Extend the test to mixed live-expired and awaiting-purge rows across multiple passes.

Verdict: **revise**.