| 1 | | Earlier finding | Disposition at `abd90b9f` | |
| 2 | |---|---| |
| 3 | | R1.1 — Expiry signature accepted as production-off | **Fixed:** no signed expiry object. | |
| 4 | | R1.2 — Existing data passes creation checks | **Fixed:** fresh row, empty-prefix checks, and rename/move restrictions. | |
| 5 | | R1.3 — Keep/TTL races expiry | **Fixed:** settle, lock, and recheck `expires_at > 0`. | |
| 6 | | R1.4 — Purge misses current deadline | **Declined-with-reason acceptable:** §11 explicitly defers the pre-existing box race; console early purge checks the marker timestamp. | |
| 7 | | R1.5 — Expiry replay after keep | **Fixed:** no bucket expiry authority. | |
| 8 | | R1.6 — Restore bypasses cap | **Fixed:** console restores reserve capacity; direct-mode exception explicitly accepted. | |
| 9 | | R1.7 — Restrictions enforced only in CLI | **Fixed for the reported paths:** server enforcement specified. | |
| 10 | | R1.8 — Browser/headless restore misses renewal | **Fixed:** shared restore helper and catch-up renewal. | |
| 11 | | R1.9 — Interrupted purge loses short deadline | **Fixed:** deadline remains in the row. | |
| 12 | | R1.10 — Migration disables expiry | **Declined-with-reason acceptable:** §4 describes direct migration and accepts its outcome. | |
| 13 | | R2.1 — Rename brings permanent data under expiry | **Fixed:** shared rename operation refuses temporary apps. | |
| 14 | | R2.2 — Early purge skips email teardown | **Fixed:** provisioning refusals and purge backstop. | |
| 15 | | R2.3 — Failed restore cancels purge deadline | **Fixed:** deadline survives until successful row restore. | |
| 16 | | R2.4 — Missing marker mistaken for restore | **Fixed for the reported completed-purge case:** empty prefix finalizes deletion. A separate safeguard regression remains below. | |
| 17 | | R2.5 — Materialization omits deadline | **Fixed:** marker-bound deadline reconstruction specified. | |
| 18 | | R2.6 — Instance provisioning bypasses store prohibition | **Fixed:** manifest and provisioning paths covered. | |
| 19 | | R2.7 — Old console silently creates permanent app | **Fixed:** distinct endpoint and response verification; §7 now agrees. | |
| 20 | | R2.8 — Expiry bypasses control-plane protection | **Fixed:** creation/setter restrictions and pre-trash check. | |
| 21 | | R3.1 — Expiry condition matches kept app | **Fixed:** positive-expiry check under mutex. | |
| 22 | | R3.2 — Account move brings permanent data under expiry | **Fixed:** shared `MoveApp` operation refuses temporary apps. | |
| 23 | | R3.3 — `expiring` survives restore | **Fixed:** flag removed; settle-first recovery specified. | |
| 24 | | R3.4 — Failed restore frees capacity | **Fixed:** §6 counts durable `restoring` reservations. | |
| 25 | | R3.5 — Temporary app can become control plane | **Fixed:** setter restriction and expiry backstop. | |
| 26 | | R3.6 — Listing command/API shape nonexistent | **Fixed:** new command and additive endpoint specified. | |
| 27 | |
| 28 | 1. **P1 — §§2, 5–6: absence-based recovery drops the existing empty-bucket safeguard.** |
| 29 | Both recovery branches prescribe `PurgeAppRow` when the marker and app objects are absent. But existing materialization deliberately refuses that conclusion when the entire `apps/` listing is empty: a valid but misconfigured deployment bucket otherwise looks exactly like completed deletion ([materialize.go:296](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/temporary-sites/internal/console/materialize.go:296), [materialize.go:388](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/temporary-sites/internal/console/materialize.go:388)). `PurgeAppRow` actually deletes the claim row ([store.go:1491](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/temporary-sites/internal/console/store.go:1491)). |
| 30 | |
| 31 | Start the console against an incorrect empty bucket with temporary apps awaiting purge. Startup materialization preserves their rows, but the proposed early-purge recovery then deletes them. Correcting the bucket leaves the real objects without their console claims or three-day purge authority. |
| 32 | |
| 33 | **Fix:** carry the existing empty-bucket safeguard into `settleApp` and every absent-marker finalization path. Require a successful deployment-wide listing and preserve rows when it is empty. Test startup materialization followed by the expiry pass against an empty replacement bucket. |
| 34 | |
| 35 | 2. **P2 — §§5, 7: the existing Trash page promises recovery for 27 days after automatic destruction.** |
| 36 | The spec changes the CLI deletion output and adds a new listing, but leaves the existing browser deadline calculation unspecified. `TrashedApps` always calculates `PurgesAt` using the 30-day `TrashCooloff` ([view.go:492](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/temporary-sites/internal/console/view.go:492)). The template displays that date and explicitly promises 30 days ([trash.html:10](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/temporary-sites/internal/console/templates/trash.html:10), [trash.html:21](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/temporary-sites/internal/console/templates/trash.html:21)). The trash API likewise returns the 30-day date as `restorable_until` ([apicli.go:1295](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/temporary-sites/internal/console/apicli.go:1295)). |
| 37 | |
| 38 | A temporary app can therefore be destroyed on day three while its recovery screen advertises day thirty. |
| 39 | |
| 40 | **Fix:** specify one effective purge-deadline calculation using the persisted `purge_after` for temporary trash, with the existing 30-day calculation for permanent trash. Use it in the Trash view, deletion confirmation, API response, and CLI output; update the unconditional recovery promises. Test both app classes together. |
| 41 | |
| 42 | Verdict: revise. |