carlos / platform Public

plan: self-serve moves 3, rev 3 (Astra plan 3 review 2: 14 findings; a two-stage ranking with a small-model property test)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 945d7cd304d51c146e367ef1e00000cddfe9a622 parent b1ce5f3
1 file changed, +68 −76
  • docs/superpowers/plans/2026-10-22-self-serve-moves-3-holding-page.md +68 −76

CI failed — run details

CI log
=== gate ===
.amadan/test-gate.sh
=== launchctx (1 s)
ok  	amadan.net/carlos/platform.git/launchctx	0.014s
?   	amadan.net/carlos/platform.git/launchctx/controlname	[no test files]
?   	amadan.net/carlos/platform.git/launchctx/wireid	[no test files]
=== vet (2 s)
=== gofmt (1 s)
gofmt -l .: clean
=== rest (141 s)
?   	github.com/carlosframework/platform/cmd/changelog	[no test files]
ok  	github.com/carlosframework/platform/cmd/console	5.910s
ok  	github.com/carlosframework/platform/cmd/moveprobe	11.571s
ok  	github.com/carlosframework/platform/cmd/zddprobe	0.704s
ok  	github.com/carlosframework/platform/internal/activator	58.753s
ok  	github.com/carlosframework/platform/internal/agentsock	0.016s
ok  	github.com/carlosframework/platform/internal/alerting	0.005s
ok  	github.com/carlosframework/platform/internal/analytics	0.019s
ok  	github.com/carlosframework/platform/internal/appconfig	2.583s
ok  	github.com/carlosframework/platform/internal/apperr	0.071s
ok  	github.com/carlosframework/platform/internal/applog	0.131s
ok  	github.com/carlosframework/platform/internal/awsinv	0.008s
ok  	github.com/carlosframework/platform/internal/boxchoice	0.004s
ok  	github.com/carlosframework/platform/internal/boxid	0.601s
?   	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.011s
ok  	github.com/carlosframework/platform/internal/caps	0.015s
ok  	github.com/carlosframework/platform/internal/certwarm	0.005s
ok  	github.com/carlosframework/platform/internal/cgroup	0.691s
ok  	github.com/carlosframework/platform/internal/changelog	0.342s
ok  	github.com/carlosframework/platform/internal/claimactivation	0.830s
ok  	github.com/carlosframework/platform/internal/clamd	0.054s
?   	github.com/carlosframework/platform/internal/cliflag	[no test files]
?   	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.218s
ok  	github.com/carlosframework/platform/internal/dbdoor/dbdoortest	0.008s
ok  	github.com/carlosframework/platform/internal/dbverify	0.004s
ok  	github.com/carlosframework/platform/internal/dnsrec	0.004s
ok  	github.com/carlosframework/platform/internal/docsite	0.007s
ok  	github.com/carlosframework/platform/internal/domains	2.669s
ok  	github.com/carlosframework/platform/internal/doorkey	0.007s
ok  	github.com/carlosframework/platform/internal/economics	0.005s
ok  	github.com/carlosframework/platform/internal/edge	44.779s
?   	github.com/carlosframework/platform/internal/edgeproof	[no test files]
ok  	github.com/carlosframework/platform/internal/edgesfile	0.007s
ok  	github.com/carlosframework/platform/internal/fleetapi	0.149s
ok  	github.com/carlosframework/platform/internal/gate	1.689s
ok  	github.com/carlosframework/platform/internal/genselect	0.014s
ok  	github.com/carlosframework/platform/internal/geo	0.019s
ok  	github.com/carlosframework/platform/internal/geoip	0.029s
ok  	github.com/carlosframework/platform/internal/historyreadback	0.167s
ok  	github.com/carlosframework/platform/internal/host	135.116s
ok  	github.com/carlosframework/platform/internal/hostactors	6.050s
ok  	github.com/carlosframework/platform/internal/hostactors/awsmembership	0.012s
ok  	github.com/carlosframework/platform/internal/hostbox	2.993s
ok  	github.com/carlosframework/platform/internal/hostclaim	0.883s
ok  	github.com/carlosframework/platform/internal/hostenv	0.012s
ok  	github.com/carlosframework/platform/internal/hostevidence	0.011s
ok  	github.com/carlosframework/platform/internal/hostexec	5.323s
ok  	github.com/carlosframework/platform/internal/hostfence	0.050s
ok  	github.com/carlosframework/platform/internal/hostfiles	10.236s
ok  	github.com/carlosframework/platform/internal/hostfloor	0.018s
ok  	github.com/carlosframework/platform/internal/hostguard	1.043s
ok  	github.com/carlosframework/platform/internal/hostops	2.029s
?   	github.com/carlosframework/platform/internal/hostops/fencerelease	[no test files]
ok  	github.com/carlosframework/platform/internal/hostops/internal/fencerelease	0.011s
ok  	github.com/carlosframework/platform/internal/hostquarantine	0.011s
ok  	github.com/carlosframework/platform/internal/hostrelocate	0.620s
ok  	github.com/carlosframework/platform/internal/hostresolve	0.195s
ok  	github.com/carlosframework/platform/internal/hostwrite	0.016s
ok  	github.com/carlosframework/platform/internal/infraview	0.017s
ok  	github.com/carlosframework/platform/internal/instancectl	5.234s
ok  	github.com/carlosframework/platform/internal/instanceexec	2.792s
ok  	github.com/carlosframework/platform/internal/instancefs	0.023s
ok  	github.com/carlosframework/platform/internal/instancekey	0.017s
ok  	github.com/carlosframework/platform/internal/instanceupdates	0.017s
ok  	github.com/carlosframework/platform/internal/layout	1.786s
ok  	github.com/carlosframework/platform/internal/lbhealth	0.007s
ok  	github.com/carlosframework/platform/internal/lease	10.140s
ok  	github.com/carlosframework/platform/internal/lease/leasetest	0.511s
ok  	github.com/carlosframework/platform/internal/ledger	0.014s
ok  	github.com/carlosframework/platform/internal/mail	0.217s
ok  	github.com/carlosframework/platform/internal/maintenance	0.089s
ok  	github.com/carlosframework/platform/internal/maintenanceexec	5.549s
ok  	github.com/carlosframework/platform/internal/maintenanceguard	0.085s
ok  	github.com/carlosframework/platform/internal/metrics	0.008s
ok  	github.com/carlosframework/platform/internal/moves	21.219s
ok  	github.com/carlosframework/platform/internal/moves/archive	1.037s
ok  	github.com/carlosframework/platform/internal/objstore	0.337s
ok  	github.com/carlosframework/platform/internal/ops	0.200s
ok  	github.com/carlosframework/platform/internal/park	0.289s
ok  	github.com/carlosframework/platform/internal/plans	0.009s
ok  	github.com/carlosframework/platform/internal/privatefile	0.005s
ok  	github.com/carlosframework/platform/internal/provision	13.147s
ok  	github.com/carlosframework/platform/internal/publicrelease	0.007s
?   	github.com/carlosframework/platform/internal/regionname	[no test files]
ok  	github.com/carlosframework/platform/internal/registry	5.472s
ok  	github.com/carlosframework/platform/internal/relay	7.249s
ok  	github.com/carlosframework/platform/internal/release	24.358s
--- FAIL: TestAMoveOntoTheInstalledGoodBuildWaitsForASignedRollback (5.03s)
    --- FAIL: TestAMoveOntoTheInstalledGoodBuildWaitsForASignedRollback/socket_instance (5.01s)
        holdran_test.go:796: timed out waiting for the move onto v1
FAIL
FAIL	github.com/carlosframework/platform/internal/rollout	8.654s
ok  	github.com/carlosframework/platform/internal/rolloutwords	0.004s
ok  	github.com/carlosframework/platform/internal/rootaws	0.009s
ok  	github.com/carlosframework/platform/internal/rootparticipation	0.061s
ok  	github.com/carlosframework/platform/internal/rootstate	0.026s
ok  	github.com/carlosframework/platform/internal/runs	0.539s
ok  	github.com/carlosframework/platform/internal/s3compat	0.008s
ok  	github.com/carlosframework/platform/internal/safefile	0.004s
ok  	github.com/carlosframework/platform/internal/schedule	2.647s
ok  	github.com/carlosframework/platform/internal/seeds	0.013s
ok  	github.com/carlosframework/platform/internal/sim/hostlife	10.274s
ok  	github.com/carlosframework/platform/internal/sim/litestreamfacts	3.754s
ok  	github.com/carlosframework/platform/internal/status	0.074s
ok  	github.com/carlosframework/platform/internal/steering	0.014s
ok  	github.com/carlosframework/platform/internal/stopevidence	0.205s
ok  	github.com/carlosframework/platform/internal/svcrole	0.003s
ok  	github.com/carlosframework/platform/internal/telemetry	0.005s
ok  	github.com/carlosframework/platform/internal/termfence	0.005s
ok  	github.com/carlosframework/platform/internal/tunnelclient	0.004s
ok  	github.com/carlosframework/platform/internal/unixdial	0.005s
ok  	github.com/carlosframework/platform/internal/updatewords	0.004s
ok  	github.com/carlosframework/platform/internal/usage	0.006s
ok  	github.com/carlosframework/platform/internal/version	0.002s
ok  	github.com/carlosframework/platform/internal/vet	1.681s
FAIL
=== internal_console@0 (79 s)
ok  	github.com/carlosframework/platform/internal/console	77.148s
=== internal_console@1 (36 s)
ok  	github.com/carlosframework/platform/internal/console	34.622s
=== internal_console@2 (41 s)
ok  	github.com/carlosframework/platform/internal/console	39.421s
=== internal_console@3 (36 s)
ok  	github.com/carlosframework/platform/internal/console	35.059s
=== internal_console@4 (36 s)
ok  	github.com/carlosframework/platform/internal/console	34.532s
=== internal_console@5 (34 s)
ok  	github.com/carlosframework/platform/internal/console	32.816s
=== internal_console@6 (35 s)
ok  	github.com/carlosframework/platform/internal/console	33.744s
=== internal_console@7 (42 s)
ok  	github.com/carlosframework/platform/internal/console	40.868s
=== root@0 (31 s)
ok  	github.com/carlosframework/platform	30.068s
=== root@1 (43 s)
ok  	github.com/carlosframework/platform	41.915s
=== root@2 (40 s)
ok  	github.com/carlosframework/platform	38.650s

FAILED: rest
make: *** [Makefile:19: test] Error 1

amadan: step gate failed: exit status 2

amadan: job failed at step gate — its output is above, not at the tail of this log
diff --git a/docs/superpowers/plans/2026-10-22-self-serve-moves-3-holding-page.md b/docs/superpowers/plans/2026-10-22-self-serve-moves-3-holding-page.md
index da4e0f0a5..b83b28939 100644
--- a/docs/superpowers/plans/2026-10-22-self-serve-moves-3-holding-page.md
+++ b/docs/superpowers/plans/2026-10-22-self-serve-moves-3-holding-page.md
@@ -2,138 +2,130 @@
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
-Rev 2, 2026-10-22. Rev 1 was reviewed by Astra (`after236/selfserve-plan3-review.md`, NOT READY, 15 findings). Rev 2 answers them: holding publication is gated on plan 1's `lifetimes` mode, not the region-HA argument; the edge's reader is fully tagged and its candidate constructor carries the file's fetch time and publisher; the fold keeps **every candidate per name** and a single arbitration decides ready, holding and certificate-only entries at lookup time, so the alias rule, expiry and the local-versus-remote question are one computation without store I/O; explicit aliases carry provenance in the registry written by the alias reconciler; TLS coverage keeps wildcard parents and holding hosts; every request path (HTTPS, HTTP, hops, doors, local wildcards) is enumerated; the probe refusal sits at the one forwarding boundary and the identity compared is the pinned box id; the response wrapper covers implicit headers and flushes; one renderer contract with the client behaviour stated; a complete decoder inventory; and the §11 witnesses that need a holding publication.
+Rev 3, 2026-10-22. Reviews: rev 1 (`after236/selfserve-plan3-review.md`, 15 findings), rev 2 (`selfserve-plan3-review2.md`, 14 findings). Rev 3 answers rev 2's fourteen: the arbitration is restated as a **ranking in two stages** (real candidates first, as today's rule; then alias candidates grouped by target, each group resolved on its own, live groups beating held ones, groups compared only by stamp and today's order, never by pair across targets) and pinned by a small-model **property test** over every candidate set, so the three-publication alias witness and the real-route precedence test hold by construction; exact resolution and request resolution are separate, so wildcard fallback works after an exact hold expires while the TLS predicates stay exact and reject wildcard labels; the decision carries the upstream snapshot it chose, so dispatch, path selection, login re-resolution, owner checks and dialing all consume one decision; local candidates come from the upstream table Reload builds, so no eligibility changes; explicit-alias provenance is part of the reconciler's equality and the fingerprint; the offline view carries a revision and both its refresh and its failure paths wake the edge; hops and doors have a stated answer for every decision; the probe wrapper ignores informational responses, delegates `Hijack`, and probe requests bypass the cache; the renderer's `until` and the client's branches are stated; the decoder audit separates the reviewed inventory from a type-resolved discovery check; and the census and reconstruction witnesses are completed.
-Builds on plan 1 (lifetimes, `(incarnation, serving_seq)` on rows and publications, `target_host` on alias publications, `edgesFileWriteClaims(..., lifetimes bool)`, the fold's pair rule) and plan 2 (`SelfServeMove{DisplayName, OfflineAt, EstimateSeconds}` on the record, `Participants`, `moveCfg.BoxID`, the job's health phase and `probeTarget`).
+Builds on plan 1 (rev 9: `(incarnation, serving_seq)` on rows and publications, `target_host` on alias publications, the fold's pair rule, `edgesFileWriteClaims(..., lifetimes bool)`) and plan 2 (rev 4: `SelfServeMove{DisplayName, OfflineAt, EstimateSeconds}`, `Participants`, `moveCfg.BoxID`, `probeTarget`).
-**Goal:** From `start` until the target's row is ready and published, every edge answers the moving host and its explicit aliases with a 503 holding page ("Shop's account is temporarily offline. Please check back in approximately N minutes."), ordered against ready routes by the pair, never believed for more than five minutes after its last successful fetch; and the console's health probe can prove the target box itself answered.
+**Goal:** From `start` until the target's row is ready and published, every edge answers the moving host and its explicit aliases with a 503 holding page, ordered against ready routes by the pair, never believed for more than five minutes after its last successful fetch; and the console's health probe can prove the target box itself answered.
-**Architecture:** Every lifetimes-mode box publishes its held offline-move rows as holding entries in a new edges-file field `offline_moves`, separate from `hosts` so no decoder counts them as served. The edge keeps, per name, the list of candidates its files publish (ready routes, holding entries, certificate-only entries) and resolves a request by one arbitration over the local registry's candidates and the remote files' candidates: stamp first, real route over alias, then the pair within one lifetime, with the alias rule for different targets, and holding entries counted only while fresh. The same arbitration answers routing, the holding page and certificate admission. A `X-Carlos-Probe: local` request is refused with 503 at the forwarding boundary and stamped `X-Carlos-Served-By: <box id>` where served locally.
+**Architecture:** Every lifetimes-mode box publishes its held offline-move rows as holding entries in a new edges-file field `offline_moves`, separate from `hosts`. The edge keeps every candidate per name (ready routes, holding entries, certificate-only entries; local and remote) and resolves a request by one ranking. The same ranking's exact form answers certificate admission. A `X-Carlos-Probe: local` request is refused with 503 at the forwarding boundary and stamped `X-Carlos-Served-By: <box id>` on the final response where served locally.
-**Tech Stack:** Go. `internal/host/edgesfile.go` (the writer), `internal/edge` (home map, `ServeHTTP`, `pending.go`, `certsync.go`, `wildcard.go`), `internal/edgesfile`, `internal/domains` (the alias reconciler), `internal/registry` (`route_aliases`), `internal/console` (the probe), the agent's wiring.
+**Tech Stack:** Go. `internal/host/edgesfile.go`, `internal/edge`, `internal/edgesfile`, `internal/domains`, `internal/registry`, `internal/console`, the agent's wiring.
-**Spec:** `docs/superpowers/specs/2026-10-21-self-serve-offline-moves-design.md` §6.4 (Publication, Rendering, TLS, The display name, Aliases, Freshness, Reopening), §7.4 (the probe mode), §11.
+**Spec:** `docs/superpowers/specs/2026-10-21-self-serve-offline-moves-design.md` §6.4, §7.4, §11.
## Global Constraints
-- `offline_moves` is never a reason to count a host as served: every decoder in Task 4's inventory reads `hosts` as today.
-- A holding entry is believed only while `now - fetchedAt < holdingValidity` (5 min), `fetchedAt` being when this edge last fetched **that box's file** successfully; a carried-forward file keeps its old `fetchedAt`. Expiry is applied at lookup time by re-arbitrating the retained candidates, never by store I/O, so the answer after expiry is exactly what the retained ready candidates give (today's routing), including after a whole refresh fails.
-- Explicit aliases only: the writer lists the alias rows whose registry row is `Explicit` and eligible (`dns_status == "pointing"`, not shadowed by a served row, the owner rule); derived aliases are never listed.
-- The display name is the only request-supplied text on the page, rendered through `html/template`; no header, path or diagnostic reaches it.
-- The page is a 503 with `Retry-After: 30`, `Cache-Control: no-store`, `X-Robots-Tag: noindex`; its script reloads by `location.replace` of the current URL (a GET, so a POST never repeats) every 30 s until `until`, then shows the taking-longer sentence and keeps reloading every 60 s; its sessionStorage key is `carlos-holding-<host>`, distinct from the pending page's.
-- No change to a claims-on box's publication or to claim-stamp precedence: the claims-on writer publishes no `offline_moves`; a stamped entry beats every holding entry.
-- Copy rows pending Paul's review: "<display name> is temporarily offline. Please check back in approximately N minutes."; "This account is temporarily offline. Please check back in approximately N minutes."; "This account is temporarily offline. Please check back in a few minutes." (no estimate); "This is taking longer than expected. Please keep checking back."; "not served here".
-- Census: the box units take rows in `plan14-root-reads-self-serve-moves-3.tsv`, loaded last by a new block at the end of `TestPlan14RootReadCensus`, `rev3=PROPOSED-self-serve-moves-3`, with reviewed dispositions for the changed envelopes (`internal/host`, `internal/edge`, `internal/domains`, `internal/registry`); the record-write census is unchanged by this plan (no record writer changes) and `TestPlan9B8RecordWriteCensus` stays green; the registry's new `explicit` column is a tracked field.
+- `offline_moves` is never a reason to count a host as served (Task 4's inventory).
+- A remote holding candidate is believed only while `now - fetchedAt < holdingValidity` (5 min); expiry is applied at ranking time from the retained candidates, never by store I/O; the answer after expiry is what the remaining candidates give, including the wildcard fallback.
+- **Exact resolution and request resolution are different questions.** `Allowed`, `KnownHost` and `ExactHome` use exact candidates only and reject `*.` names as today; request resolution tries the exact name and falls back to the wildcard parent only when the exact name has no **effective** routing candidate (a live exact holding winner blocks the fallback).
+- Local ready candidates are the entries of the upstream table `Reload` builds (`e.current()`): its eligibility (ready status, a usable backend, no sidecars, aliases whenever their primary has an upstream, shadowing) is unchanged. DNS pointing gates **publication** of aliases, not local service.
+- Explicit aliases only in holding publications: the alias row is `Explicit` and eligible for publication (`dns_status == "pointing"`, not shadowed, the owner rule).
+- The display name is the only request-supplied text on the page, rendered through `html/template`.
+- The page: 503, `Retry-After: 30`, `Cache-Control: no-store`, `X-Robots-Tag: noindex`; the client reloads by `location.replace` of the current URL every 30 s while `until` is ahead, shows the taking-longer sentence at `until` and reloads every 60 s after; with no `until` it keeps the no-estimate sentence and reloads every 60 s; its sessionStorage key is `carlos-holding-<host>`; storage failures never stop the reload.
+- No change to a claims-on box's publication or to claim-stamp precedence among ready candidates; the real-route-over-alias rule (#287, `TestHomeMapAStampedAliasDoesNotBeatAnUnstampedRoute`) stays before stamps.
+- Copy rows pending Paul's review: the four sentences of rev 2 and "not served here".
+- Census: `plan14-root-reads-self-serve-moves-3.tsv` loaded last by a new block at the end of `TestPlan14RootReadCensus`, `rev3=PROPOSED-self-serve-moves-3`, envelopes `internal/host`, `internal/edge`, `internal/domains`, `internal/registry`, `internal/console` (Task 6's probe), the tracked field `route_aliases.explicit`, reviewed dispositions for changed frozen units; `TestPlan9B8RecordWriteCensus` unchanged and green.
- Local tests: touched packages with `-run`; CI runs the full suite.
## Review Focus
-1. A holding entry fetched 4 m 59 s ago versus 5 m 1 s ago; a whole refresh that fails (list, every get, some gets) after the entry was fetched: the first is honoured, the others give the retained ready route (Task 5, `TestHoldingEntriesExpireFromTheFetchClock`).
-2. An explicit alias repointed from the moving instance to another during the move, with the three publications in every order: the new target's ready route wins over the hold, fresh and retained; detached, the alias falls to a derived entry of another target (Task 3, `TestAliasRepointedDuringAMove`).
-3. The stale ready alias publication of the instance being moved loses to its own hold; a legacy alias publication without `target_host` is a different target and wins (Task 3, `TestAliasOfTheMovingInstanceLosesToItsHold`).
-4. The source box's own edge with a local hold `(1, 1)` and a retained remote ready `(1, 0)`: the page; a remote ready `(1, 2)` after open: backhaul; a local ready alias to A against a fetched hold for A at a higher pair: the page (Task 2, `TestLocalAndRemoteArbitrateTogether`).
-5. A probe reaching the ordinary, wildcard-exact and door-authorised forwarding paths: 503 "not served here", no dial; served locally: `X-Carlos-Served-By` names this box id on an implicit 200, a HEAD and a flushed response (Task 6, `TestProbeModeNeverPassesABackhauledAnswer`).
+1. Expiry: a remote exact hold over a retained exact ready `(1, 0)`, over a local wildcard upstream, and over a remote wildcard route, each at 4 m 59 s and 5 m 1 s with no refresh in between, and after a whole refresh fails: the page, then today's answer (the retained route, or the wildcard) (Task 5, `TestHoldingEntriesExpireFromTheFetchClock`).
+2. The alias ranking on every candidate set of the small model (up to four candidates, two targets, pairs up to 2, stamps 0 or 1, fresh or expired): the stated invariants hold (Task 3, `TestAliasRankingProperties`).
+3. The stale ready alias of the moving instance loses to its own hold; a legacy alias publication without `target_host` is a different target and wins (Task 3, `TestAliasOfTheMovingInstanceLosesToItsHold`).
+4. The source box's own edge: a local hold `(1, 1)` against a retained remote ready `(1, 0)` → the page; a remote ready `(1, 2)` → backhaul; a local ready alias to A against a fetched hold for A at a higher pair → the page; a non-pointing local alias keeps serving (Task 2, `TestLocalAndRemoteArbitrateTogether`).
+5. A probe through every forwarding path → 503 and no dial; served locally through the real writer stack with gzip and the cache on, with a `103` before the final response and a forged final `X-Carlos-Served-By` → the final response carries this box's id and is not cached (Task 6, `TestProbeModeThroughTheRealStack`).
---
### Task 1: Alias provenance and the writer's holding entries
**Files:**
-- Modify: `internal/registry/registry.go` (`route_aliases` gains `explicit INTEGER NOT NULL DEFAULT 0` in its ALTER list; `RouteAlias.Explicit bool`; `UpsertAlias` and `ListAliases` carry it), `internal/domains/aliases.go` (`ReconcileAliases`: an alias written for a claim with `Route != ""` is `Explicit: true`; `deriveInstanceAliases` writes `Explicit: false`; a legacy row (explicit 0) whose name matches a current claim is upgraded on the next reconcile), `internal/host/edgesfile.go` (`edgesFile.OfflineMoves []holdingEntry \`json:"offline_moves,omitempty"\``; `edgesFileWriteClaims(ctx, s, reg, ident, eip, last, refresh, claims, lifetimes bool, holds holdingSource)`: holding publication **only with `lifetimes`**, whatever `claims` is; a shared `aliasEligible(a registry.RouteAlias, served map[string]edgeHost, owners map[string]*hostOwner) bool` used by the served-alias loop and the holding entries), `internal/host/movesoffline.go` (`func (v *offlineView) holding(host string) (provision.MoveState, bool)` from the snapshot; nil or failed snapshot → false), `internal/host/agent.go` (`holds` from the armed view)
-- Test: `internal/host/edgesfile_holding_test.go`, `internal/domains/aliases_explicit_test.go`
+- Modify: `internal/registry/registry.go` (`route_aliases.explicit INTEGER NOT NULL DEFAULT 0`; `RouteAlias.Explicit bool`; `UpsertAlias`'s conflict clause sets `explicit=excluded.explicit`; `ListAliases` scans it; `Fingerprint`'s alias part includes it), `internal/domains/aliases.go` (claim-backed aliases `Explicit: true`; `deriveInstanceAliases` writes `Explicit: false` **and its skip test includes `Explicit`**: a row with the same target and owner but `Explicit: true` is rewritten to false when the claim is gone; a legacy row matching a current claim is upgraded), `internal/host/edgesfile.go` (`edgesFile.OfflineMoves []holdingEntry \`json:"offline_moves,omitempty"\``; `edgesFileWriteClaims(ctx, s, reg, ident, eip, last, refresh, claims, lifetimes bool, holds holdingSource)`), `internal/edgesfile/eligible.go` (new, importable: `func AliasPublishable(a registry.RouteAlias, primaryServed bool, shadowed bool, ownerOK bool) bool` = `a.DNSStatus == "pointing" && !shadowed && ownerOK` with `primaryServed` meaning "the primary is a served **or held** row": the writer passes its own served/held sets and owner projection; the ownership validation itself stays in `domains.ReconcileAliases`), `internal/host/movesoffline.go` (`offlineView.holding(host)`; `offlineView.rev uint64` incremented by `refresh` **and** `fail`; `offlineView.onChange func(rev uint64)` the edge subscribes to), `internal/host/movespass.go` (the listing error path at line 1081 calls `w.cfg.Offline.fail()` before returning), `internal/host/agent.go` (wiring)
+- Test: `internal/host/edgesfile_holding_test.go`, `internal/domains/aliases_explicit_test.go`, `internal/registry/alias_explicit_test.go`
**Interfaces:**
-- `type holdingEntry struct { Host string \`json:"host"\`; Platform bool \`json:"platform,omitempty"\`; Aliases []string \`json:"aliases,omitempty"\`; MoveID string \`json:"move_id"\`; Incarnation uint64 \`json:"incarnation,omitempty"\`; ServingSeq uint64 \`json:"serving_seq,omitempty"\`; OfflineAt int64 \`json:"offline_at,omitempty"\`; EstimateSeconds int \`json:"estimate_seconds,omitempty"\`; DisplayName string \`json:"display_name,omitempty"\` }`; `type holdingSource func(host string) (provision.MoveState, bool)`.
-- The rule per registry row `r`: `registry.IsHold(r.Status) && r.HoldOp != ""` and `holds(r.Host)` returns a move with `ID == r.HoldOp` → one entry `{Host, Platform: r.Platform || r.Provisioned, Aliases, MoveID, Incarnation: r.Incarnation, ServingSeq: r.ServingSeq, OfflineAt: unix seconds of mv.SelfServe.OfflineAt (0 when nil or no SelfServe), EstimateSeconds, DisplayName}`; `Aliases` = the `route_aliases` rows with `Host == r.Host && Explicit && aliasEligible`. A ready row, or a hold the view does not know, publishes none; the entry stops when the record no longer carries that move id (the view) or the row is ready. The target's `landing` row publishes from flip until `ReleaseHoldSeq`. Entries sorted by host.
+- `type holdingEntry struct { Host string \`json:"host"\`; Platform bool \`json:"platform,omitempty"\`; Account string \`json:"account,omitempty"\`; App string \`json:"app,omitempty"\`; Aliases []string \`json:"aliases,omitempty"\`; MoveID string \`json:"move_id"\`; Incarnation uint64 \`json:"incarnation,omitempty"\`; ServingSeq uint64 \`json:"serving_seq,omitempty"\`; OfflineAt int64 \`json:"offline_at,omitempty"\`; EstimateSeconds int \`json:"estimate_seconds,omitempty"\`; DisplayName string \`json:"display_name,omitempty"\` }` (`Account`/`App` from the row's owner projection, so a door can authorise); `type holdingSource func(host string) (provision.MoveState, bool)`.
+- The rule per row and the stop conditions as rev 2; `Aliases` = the `route_aliases` rows with `Host == r.Host && Explicit && AliasPublishable(a, true, shadowed, ownerOK)`.
-- [ ] **Step 1: Write the failing tests**: `TestAliasReconcilerWritesProvenance` (a claim-backed alias → explicit; a derived instance alias → not; a legacy row upgraded); `TestWriterPublishesHoldingEntriesOnlyInLifetimesMode` (lifetimes on with region HA off and on → entries; lifetimes off with either → none; `applyEdgesClaims` still runs before the served alias construction); `TestHoldingEntryListsOnlyExplicitEligibleAliases` (explicit pointing → listed; explicit not pointing → not; derived exact name → not; shadowed by a served row → not); `TestTargetPublishesWhileLanding`; `TestHoldingEntryStopsWhenTheRecordClearsOrChangesID` (the row still held, the view's move id changes → no entry).
-- [ ] **Step 2: Run to verify they fail**: `go test ./internal/host -run 'Holding' -count=1`, `go test ./internal/domains -run 'Provenance' -count=1`, `go test ./internal/registry -run 'Alias' -count=1`.
-- [ ] **Step 3: Implement.**
-- [ ] **Step 4: Run to verify they pass**, plus `go test ./internal/host -run 'EdgesFile' -count=1`.
-- [ ] **Step 5: Commit** `edges: explicit alias provenance in the registry; the lifetimes writer publishes held offline-move rows as holding entries`.
+- [ ] **Step 1: Write the failing tests**: rev 2's plus `TestDerivedAliasClearsExplicitOnTheSameTarget` (a detached explicit claim whose name is then derived to the same target → the row becomes `Explicit: false`, the fingerprint changes, the next holding publication omits it); `TestAliasFingerprintIncludesExplicit`; `TestViewRevisionBumpsOnRefreshAndFail` (and the moves pass's listing error marks the view failed).
+- [ ] **Step 2: Run to verify they fail; Step 3: implement; Step 4: run** `go test ./internal/host -run 'Holding|ViewRevision' -count=1`, `go test ./internal/domains -run 'Explicit|Provenance' -count=1`, `go test ./internal/registry -run 'Alias' -count=1`.
+- [ ] **Step 5: Commit** `edges: explicit alias provenance in the registry and the reconciler; the lifetimes writer publishes held rows as holding entries; the offline view carries a revision`.
-### Task 2: Candidates, one arbitration, the page, and the local edge
+### Task 2: Candidates, the ranking, the page, and the local edge
**Files:**
-- Modify: `internal/edge/homemap.go` (`edgesFileView.OfflineMoves []holdingView \`json:"offline_moves"\`` and `fetchedAt time.Time` (set by `refreshHosts` on a successful parse; carried forward otherwise); `homeSnapshot` gains `cands map[string][]candidate` (every candidate per name, immutable once published) beside `hosts` (which becomes the fold's answer at `now` for the platform set and the wake only); `foldHomes` builds candidates; `arbitrate(cands []candidate, now time.Time) decision` is the one rule; `lookupHome`, `Home`, `ExactHome`, `Owner`, `KnownHost`, `Hosts`, `ServedHosts` resolve through `arbitrate` at call time), `internal/edge/holding.go` (new: `renderHoldingPage`, `holdingAnswer`), `internal/edge/edge.go` (`Reload` builds `localCands map[string][]candidate` from the registry: ready rows (`Status == "ready"`) as ready candidates with their pair, alias rows (`aliasEligible`'s registry half: pointing) as alias candidates with `targetHost`, held rows whose `HoldOp` the `HoldingSource` knows as holding candidates with the view's move; `e.resolve(host, now) decision` merges local and remote candidates and arbitrates; `ServeHTTP` and `httpFallbackHandler` act on the decision), `internal/edge/certsync.go`, `internal/edge/wildcard.go` (certificate admission and the wildcard parents through the same resolution), the agent (`edge.SetHoldingSource(view.holding)`, `edge.RefreshHolds()` after every `offlineView.refresh`, `edge.WithBoxID(boxID)`)
-- Test: `internal/edge/arbitrate_test.go`, `internal/edge/holding_test.go`, `internal/edge/homemap_test.go`
+- Modify: `internal/edge/homemap.go` (`edgesFileView.OfflineMoves []holdingView`, `fetchedAt`; `homeSnapshot.cands map[string][]candidate`; `foldHomes` builds candidates; `rank(name, cands, now) decision`; `resolveExact`, `resolveRequest`; `Home`, `ExactHome`, `Owner`, `KnownHost`, `Hosts`, `ServedHosts` over the exact resolution), `internal/edge/holding.go` (new), `internal/edge/edge.go` (`Reload` builds `localCands` **from the upstream table it just built**: one ready candidate per table entry with `up` attached (alias flag, target = the primary's host for an alias entry, owner from `up.route`, the pair from `up.route.Incarnation/ServingSeq`), plus one holding candidate per held row the holding source knows (owner from the row); `e.resolve(host, now) decision`; the request paths below), `internal/edge/certsync.go`, `internal/edge/wildcard.go` (exact predicates), the agent (`edge.SetHoldingSource`, the view's `onChange` → `edge.RefreshHolds(rev)`, `edge.WithBoxID`)
+- Test: `internal/edge/rank_test.go`, `internal/edge/holding_test.go`, `internal/edge/homemap_test.go`
**Interfaces:**
-- `type holdingView struct { Host string \`json:"host"\`; Platform bool \`json:"platform"\`; Aliases []string \`json:"aliases"\`; MoveID string \`json:"move_id"\`; Incarnation uint64 \`json:"incarnation"\`; ServingSeq uint64 \`json:"serving_seq"\`; OfflineAt int64 \`json:"offline_at"\`; EstimateSeconds int \`json:"estimate_seconds"\`; DisplayName string \`json:"display_name"\` }`.
-- `type candidate struct { kind candKind (candReady, candHolding, candCert); eip, box string; local bool; alias, wildcard, platform bool; targetHost string; incarnation, servingSeq uint64; stamp int64; fetchedAt time.Time; hold *holdingView }`. The constructor for a remote file copies `fetchedAt`, `box` and `eip` (`dialAddress`) into every candidate of that file; a holding entry yields one `candHolding` for its host (`targetHost = host`, `alias false`) and one `candHolding` per listed alias (`alias true`, `targetHost = host`); a ready alias entry carries its `target_host`; a certificate-only wildcard entry is `candCert`.
-- `type decision struct { kind decKind (decLocal, decRemote, decHolding, decNone); eip, box string; account, app string; platform, wildcard bool; hold *holdingView }`.
-- **`arbitrate(cands, now)`**, exactly: (1) drop `candHolding` with `now.Sub(fetchedAt) >= holdingValidity` (local holds have no expiry; they come from this box's registry); (2) `platform`/`wildcard` flags are the OR over every candidate (certificate classes merge, never decide); (3) `candCert` never decides; if nothing else remains → `decNone` (with the merged flags for the certificate parents); (4) **stamp:** if any remaining candidate is stamped, the highest stamp among stamped ready candidates wins (holding candidates are never stamped); (5) **real route over alias:** if any remaining candidate is a non-alias ready or holding candidate, alias candidates are dropped; (6) **the alias rule** (only alias candidates remain): if any ready alias's `targetHost` differs from every holding alias's `targetHost` (an empty `targetHost` counts as different), the holding aliases are dropped; (7) among what remains, the highest `(incarnation, servingSeq)` wins; a tie between a ready and a holding candidate → the ready one; a tie among ready candidates → the existing rule (the earlier file; a local candidate is "earliest"); a tie among holding candidates → the earlier file; (8) the winner's whole routing state is the decision: a ready winner → `decLocal` or `decRemote` with its `eip`, `box`, owner; a holding winner → `decHolding` with its view.
-- `func (m *homeMap) resolve(host string, now time.Time) decision` (exact name; wildcard fallback `*.<parent>` only when the exact name has **no** candidates at all, so a holding winner never falls through to a wildcard); `Home`/`ExactHome` answer only `decRemote`; `Owner` answers only a `decRemote` with an owner; `KnownHost(host, now)` is true when the decision is `decRemote` or `decHolding`; `Hosts()` (the wildcard parent set) includes a `decHolding` winner's host when the hold is `platform`.
-- `func (e *Edge) resolve(host string, now time.Time) decision`: the local candidates for `host` (and for its wildcard parent when the exact name has none) plus the remote ones, arbitrated together; the result drives: `decLocal` → the existing upstream path; `decRemote` → `backhaulTo`; `decHolding` → `holdingAnswer`; `decNone` → today's answers (`unknownHost`: pending or 404 or fallback).
-- **Insertion points, enumerated**: `ServeHTTP` (HTTPS): after `acceptHop`, replace `lookupUpstreamPath`'s decision with `e.resolve(host, now)`; a hopped request whose decision is `decHolding` → the page (no further forwarding, so the one-hop guarantee holds); `decRemote` on a hopped request → `staleHopRefusal` as today; a door caller (`isDoor`) → `decLocal` requires `doorOwnsRoute` as today; `decRemote` requires `Owner` to match the door as today; `decHolding` → the page; the local-wildcard branch and the `!ok` branch are both subsumed. `httpFallbackHandler` (HTTP): `decLocal`, `decRemote`, `decHolding` → the HTTPS redirect (a known answering host); `decNone` → today. `Allowed(host)` (TLS SNI): `decLocal` or `decHolding` with a local candidate; `deploymentHost` → `Allowed || KnownHost`. `currentHosts` (the wildcard parents and cert sync): the local ready **and local held** platform/provisioned non-alias rows; `homeMap.Hosts()` as above.
-- The page: `func renderHoldingPage(displayName string, untilUnix int64, now time.Time) []byte`: `untilUnix > now.Unix()` → with a name "<name> is temporarily offline. Please check back in approximately N minutes." (N = ceil((until − now)/60), minimum 1), without a name the generic sentence with N; `untilUnix == 0` → "This account is temporarily offline. Please check back in a few minutes."; `untilUnix <= now.Unix()` → "This is taking longer than expected. Please keep checking back."; the script carries `until` and the reload cadence; `func (e *Edge) holdingAnswer(w, r, h holdingView)` sets the headers and writes the page (HEAD writes none).
-- `localHolds` refresh: `RefreshHolds()` rebuilds the local candidates' holding part from the registry and the view and swaps the local candidate map atomically; `Watch`'s fingerprint reload does the same; a nil or failed view → no local holding candidates (fail open to today's answer).
-
-- [ ] **Step 1: Write the failing tests**: `TestHoldingViewDecodesTheWriterFixture` (a fixture emitted by the real writer; every field asserted); `TestArbitrate` (a table over every rule: stamp over hold; real over alias; the alias rule with three publications in every order; pair within one lifetime; ties; expiry; cert-only never decides; the winner's state copied); `TestResolveNeverFallsThroughAHoldingWinner`; `TestLocalAndRemoteArbitrateTogether` (the three cases of Review Focus 4, on a source edge and on a target edge); `TestHoldingPageRendersEveryCase` (name, no name, no estimate, past until, `</script>` in the name escaped; headers; HEAD); `TestEveryRequestPathHonoursTheDecision` (HTTPS local, remote, holding, none; HTTP each; a hopped request; a door caller; a local wildcard upstream against a fetched hold for the exact name); `TestLocalHoldsRefreshWithTheView` (the view clears the move while the row stays held → no page; startup before the view is readable → today's answer; a later view → the page without a registry change); `TestTLSCoverageForHoldingHosts` (real handshakes: a wildcard-only platform host held → its parent stays in `currentHosts` and `certFor` serves; an explicit alias held → `Allowed`; expired → not; a non-issuer; an arbitrary wildcard label refused).
-- [ ] **Step 2: Run to verify they fail**: `go test ./internal/edge -run 'Arbitrate|Holding|Resolve|LocalAndRemote|RequestPath|TLSCoverage' -count=1`.
-- [ ] **Step 3: Implement.**
-- [ ] **Step 4: Run to verify they pass**, plus `go test ./internal/edge -run 'HomeMap|Fold|Pending|Cert' -count=1`.
-- [ ] **Step 5: Commit** `edge: one arbitration over local and remote candidates decides routing, the holding page and certificate admission`.
-
-### Task 3: The alias rule's witnesses
+- `type holdingView struct { Host string \`json:"host"\`; Platform bool \`json:"platform"\`; Account string \`json:"account"\`; App string \`json:"app"\`; Aliases []string \`json:"aliases"\`; MoveID string \`json:"move_id"\`; Incarnation uint64 \`json:"incarnation"\`; ServingSeq uint64 \`json:"serving_seq"\`; OfflineAt int64 \`json:"offline_at"\`; EstimateSeconds int \`json:"estimate_seconds"\`; DisplayName string \`json:"display_name"\` }`.
+- `type candidate struct { kind candKind (candReady, candHolding, candCert); local bool; order int (the file's sorted index; local is -1); eip, box string; account, app string; alias, wildcard, platform bool; targetHost string; incarnation, servingSeq uint64; stamp int64; fetchedAt time.Time; hold *holdingView; up *upstream (local only) }`. Remote constructors copy the file's `fetchedAt`, `box`, `order` and `eip`; a holding entry yields one real `candHolding` for its host (`targetHost = host`) and one alias `candHolding` per listed alias (`targetHost = host`), all with the entry's owner.
+- `type decision struct { kind decKind (decLocal, decRemote, decHolding, decNone); up *upstream; eip, box, account, app string; stamp int64; platform, wildcard bool; hold *holdingView }`.
+- **`rank(name, cands, now)`**: (1) remote `candHolding` with `now.Sub(fetchedAt) >= holdingValidity` are removed; (2) `platform`/`wildcard` are the OR over all candidates; `candCert` never decides; nothing else → `decNone`; (3) **stage one, real candidates** (non-alias ready or holding): if any exist, alias candidates are dropped and the real winner is: the highest-stamped stamped ready candidate if any is stamped (holds are never stamped); else the highest `(incarnation, servingSeq)`; a tie between ready and holding → ready; a tie among ready → the lowest `order`; among holding → the lowest `order`; (4) **stage two, alias candidates only**: group by `targetHost` (an empty target is its own group); within a group the winner is: a stamped ready if any (highest stamp); else the highest pair, ready beating holding on a tie, lowest `order` among equals; a group whose winner is ready is **live**, otherwise **held**; if any live group exists → the winner among live groups' winners by (highest stamp, then lowest `order`) **never by pair across targets**, and every held group is dropped; if no live group → the held winner with the lowest `order`; (5) the winner's whole state is the decision (`decLocal` carries `up`; `decRemote` carries `eip, box, account, app, stamp`; `decHolding` carries `hold`).
+- Invariants the property test asserts (Task 3): a real candidate always beats every alias candidate; a stamped ready real candidate beats every other real candidate; a holding candidate never beats a stamped candidate; a ready alias of target T beats a holding alias of a different target whatever the pairs; a holding alias of T beats a ready alias of T with a lower pair and loses to one with a higher or equal pair; an expired remote hold decides nothing; pairs are never compared across different targets; the decision is independent of input order except through `order`.
+- `func (m *homeMap) resolveExact(name string, now time.Time) decision` (the exact candidates; never a wildcard); `func (m *homeMap) resolveRequest(name string, now time.Time) decision` (exact; if `decNone` and the exact name has no live `decHolding` → `rank` over `*.<parent>`'s candidates); `Home`/`ExactHome` answer only `decRemote` from `resolveExact`/`resolveRequest` as today's split (`Home` request, `ExactHome` exact); `Owner` from the same decision; `KnownHost(name, now)` = `resolveExact` is `decRemote` or `decHolding` and `name` is not a `*.` label; `Hosts()` adds a `decHolding` winner's host when `platform`.
+- `func (e *Edge) resolve(host string, now time.Time) decision`: local exact candidates (and local wildcard entries when the exact name has none) merged with the remote candidates, through `rank`, with the wildcard fallback rule above; **the handler uses `dec.up`** for local dispatch: path-prefix selection runs over `dec.up.paths`, the login re-resolution looks up the path in the same snapshot, the door's `doorOwnsRoute` checks `dec.up.route`, a remote decision's `Owner` is `dec.account/app`, and `backhaulTo` is called with `dec.eip, dec.box`: one decision for the whole request.
+- **Request paths**: HTTPS `ServeHTTP`: after `acceptHop`: `decLocal` → the local path (as today from `dec.up`); `decRemote` → an accepted hop → `staleHopRefusal`; a door → `Owner` must match else `unknownHost`; else `backhaulTo`; `decHolding` → an accepted hop → the page (no forwarding); a door → the page only when `hold.Account/App` equal the door's, else `unknownHost`; else the page; `decNone` → an accepted hop → `staleHopRefusal` (today); else `unknownHost` (today). HTTP fallback: `decLocal`/`decRemote`/`decHolding` → the HTTPS redirect; `decNone` → today. `Allowed(host)` = a local exact ready or local exact holding candidate and not a `*.` name; `deploymentHost` = `Allowed || KnownHost`; `currentHosts` = the local exact ready **and held** platform/provisioned non-alias rows (so wildcard parents persist through a hold); `homeMap.Hosts()` as above.
+- The page: `func renderHoldingPage(displayName string, untilUnix int64, now time.Time) []byte` with `untilUnix = OfflineAt + EstimateSeconds` when both are non-zero, else 0; the sentences and client branches as the Global Constraints state; `func (e *Edge) holdingAnswer(w, r, h holdingView)`.
+- `RefreshHolds(rev uint64)`: rebuilds the local holding candidates; rebuilds are serialised by a mutex and a rebuild for an older `rev` than the installed one is discarded; `rev` from a failed view → no local holding candidates.
+
+- [ ] **Step 1: Write the failing tests**: `TestHoldingViewDecodesTheWriterFixture`; `TestRankStageOne` (the real rules incl. the existing `TestHomeMapAStampedAliasDoesNotBeatAnUnstampedRoute` still passing, and a stamped alias against a real holding candidate → the hold); `TestRankStageTwo` (the three-publication case in all six orders; the `(1,2)`-ready-A/`(1,1)`-hold-A/`(9,1)`-hold-B case → A; two holds → the lower `order`); `TestResolveExactVersusRequest` (an expired exact hold → the wildcard; a live exact hold blocks it; `KnownHost` false for an invented wildcard label; `Allowed` rejects `*.`); `TestDecisionCarriesTheUpstream` (path prefixes and the login re-resolution use `dec.up`; a reload between resolution and dispatch does not change the served upstream); `TestLocalCandidatesAreTheUpstreamTable` (a non-pointing local alias serves; sidecars and backend-less rows absent); `TestLocalAndRemoteArbitrateTogether`; `TestEveryRequestPathHonoursTheDecision` (the table above including hops and doors with and without ownership); `TestLocalHoldsFollowTheViewRevision` (a failed view after an installed hold drops it; an older rebuild is discarded); `TestHoldingPageRendersEveryCase` (name, no name, no estimate, past until, escaping, HEAD; the script's `until`, cadences, storage key and no-estimate branch asserted by text); `TestTLSCoverageForHoldingHosts` (real handshakes; a wildcard-only platform host held keeps its parent; an invented label gets no issuance and no challenge read).
+- [ ] **Step 2: Run to verify they fail; Step 3: implement; Step 4: run** `go test ./internal/edge -run 'Rank|Holding|Resolve|Decision|LocalCandidates|LocalAndRemote|RequestPath|TLSCoverage|HomeMap' -count=1`.
+- [ ] **Step 5: Commit** `edge: one ranking over local and remote candidates decides routing, the holding page and certificate admission, exact and request resolution apart`.
+
+### Task 3: The alias ranking's witnesses
**Files:**
-- Test: `internal/edge/alias_test.go`
+- Test: `internal/edge/alias_test.go`, `internal/edge/rank_property_test.go`
-The rule is Task 2's steps (5) to (7); this task pins it.
+- [ ] **Step 1: Write the tests**: `TestAliasOfTheMovingInstanceLosesToItsHold`; `TestAliasRepointedDuringAMove` (B wins in all six orders, fresh and retained, with unequal unrelated pairs on A); `TestTwoHoldsForOneAlias` (the lower `order`); `TestHoldingAliasNeverBeatsARealRoute`; `TestStampedAliasBeatsEveryHoldInItsGroup`; `TestAliasRankingProperties` (enumerate every candidate set of the small model and assert Task 2's invariants).
+- [ ] **Step 2: Run; fix `rank` if anything fails; Step 3: Commit** `edge: the alias ranking pinned by witnesses and a small-model property test`.
-- [ ] **Step 1: Write the failing tests**: `TestAliasOfTheMovingInstanceLosesToItsHold` (A's stale ready alias `(1, 0)` target A vs A's hold `(1, 1)` → the page; the legacy publication without target → the route); `TestAliasRepointedDuringAMove` (A's stale ready alias target A `(1, 0)`, B's ready alias target B `(1, 0)`, A's holding alias `(1, 1)`: B wins in all six orders, fresh and retained; detached from B, a derived entry whose primary is C → C); `TestTwoHoldsForOneAlias`; `TestHoldingAliasNeverBeatsARealRoute`; `TestStampedAliasBeatsEveryHold`.
-- [ ] **Step 2: Run to verify they fail; Step 3: fix `arbitrate` if they find anything; Step 4: run** `go test ./internal/edge -run 'Alias|Arbitrate' -count=1`.
-- [ ] **Step 5: Commit** `edge: the alias rule pinned in every publication order`.
-
-### Task 4: The decoder audit
+### Task 4: The decoder inventory and its discovery check
**Files:**
-- Test: `internal/edgesfile/offline_moves_test.go`, `internal/console/edges_offline_moves_test.go`, `internal/domains/aliases_offline_moves_test.go`, `internal/steering/offline_moves_test.go`, `internal/host/edgesfile_decoders_test.go`, `internal/hostactors/references_offline_moves_test.go`
-- Create: `internal/edgesfile/testdata/offline-moves.json` (one served route, one holding entry for another host with one alias, one certificate-only wildcard entry)
+- Test: as rev 2 plus `internal/host/movesreport_offline_moves_test.go`, `internal/host/movespass_offline_moves_test.go`
+- Create: `internal/edgesfile/testdata/offline-moves.json`
-The inventory, each with its assertion: `edgesfile.Parse` (`File.Hosts` has the route, not the hold); `edgesfile.ReadSnapshot`/`ReadLiveSnapshot` (`PlacementView.Hosts`); `edgesfile.DeploymentRoutes` (with fresh status evidence so a served route is present; the hold absent); the console's `loadEdgesJoin` (`instancesplacement.go`), `offlineServes` (`apiinstancemove_offline.go`), `boxchoice` and `regioncandidates` through their decision functions over the fixture's snapshot (a candidate box is chosen for the served route's region; the held host contributes nothing), `apidomains.go`'s read (the held alias is not "taken"); `domains.ReconcileAliases` through its `DeploymentRoutes` input (the held alias is not a served route); `internal/steering` (`deployment.go`, `steering.go`: the desired state ignores the hold); `internal/host`: `moveTargets` (`movespass.go`), `loadMoveFleet` (`movesreport.go`), the writer's prior-publication recovery and `seedEdgesFileGate` (`edgesfile.go`), the home map (`homemap.go`: the hold is a candidate, not a served host); `internal/hostactors/references.go` (the box-id reader ignores it). `TestEveryEdgesFileDecoderIsAudited` lists every file that unmarshals an edges file (found by `go/ast`: a `json.Unmarshal` whose target type has a `hosts` tag, or a call to `edgesfile.Parse`/`ReadSnapshot`/`ReadLiveSnapshot`/`DeploymentRoutes`) and fails on one missing from this list.
+The reviewed inventory with one assertion each: `edgesfile.Parse`, `ReadSnapshot`/`ReadLiveSnapshot`, `DeploymentRoutes` (with fresh status evidence), `EdgeIPs` and `LiveSnapshot` (classified unaffected, asserted unchanged on the fixture), the console's `loadEdgesJoin`, `offlineServes`, `boxchoice` and `regioncandidates` through their decision functions, `apidomains.go`'s read, `domains.ReconcileAliases` through its `DeploymentRoutes` input, `internal/steering`, `internal/host`'s `moveTargets`, `loadMoveFleet`, the writer's prior-publication recovery and `seedEdgesFileGate` (an unchanged-gate round trip after `offline_moves` is added), the home map, `internal/hostactors/references.go`. The discovery check `TestEveryEdgesFileReadSiteIsInventoried` (go/ast with type resolution): every call to an `edgesfile` reader, and every `json.Unmarshal`/`(*json.Decoder).Decode` whose target type is declared in `edgesfile`, `edge`, `host`, `hostactors` or `console` and either carries a top-level `hosts` or `box_id` JSON tag or is reached through a wrapper of those readers, must be in the inventory; fixtures for the narrow box-id reader and for an unrelated `hosts` struct prove the detector's shape.
-- [ ] **Step 1: Write the tests** (they pass against the current decoders and pin the contract). **Step 2: Commit** `edges: every edges-file decoder ignores offline_moves for served-ness`.
+- [ ] **Step 1: Write the tests. Step 2: Commit** `edges: every edges-file read site is inventoried and ignores offline_moves for served-ness`.
### Task 5: Freshness
**Files:**
-- Modify: `internal/edge/homemap.go` (`refreshHosts` sets `fetchedAt` on a successful parse and carries the old value forward with a carried-forward file; `arbitrate` applies the five-minute rule at call time)
+- Modify: `internal/edge/homemap.go` (`fetchedAt` on a successful parse, carried forward otherwise; `rank` applies the rule)
- Test: `internal/edge/homemap_test.go`
-- [ ] **Step 1: Write the failing tests**: `TestHoldingEntriesExpireFromTheFetchClock` (4 m 59 s → the page; 5 m 1 s → the retained ready route, with no refresh in between; a failed `List`; every `Get` failing; a partial failure where this box's file fails and others succeed; the exact boundary); `TestExpiredHoldFallsBackToTheRetainedRoute` (the fold selected the hold over a retained ready `(1, 0)`; after expiry the route answers and `KnownHost` is still true for it); `TestExpiredHoldNoLongerAdmitsACertificate` (a holding-only host after expiry → `KnownHost` false).
-- [ ] **Step 2: Run to verify they fail; Step 3: implement; Step 4: run** `go test ./internal/edge -run 'Expire|Holding' -count=1`.
-- [ ] **Step 5: Commit** `edge: holding entries are believed for five minutes from their last successful fetch and expire without I/O`.
+- [ ] **Step 1: Write the failing tests**: `TestHoldingEntriesExpireFromTheFetchClock` (Review Focus 1's cases including the local and remote wildcard fallbacks and a failed `List`, every `Get`, and a partial failure); `TestExpiredHoldFallsBackToTheRetainedRoute`; `TestExpiredHoldNoLongerAdmitsACertificate`.
+- [ ] **Step 2: Run; Step 3: implement; Step 4: run** `go test ./internal/edge -run 'Expire|Holding' -count=1`. **Step 5: Commit** `edge: holding entries expire five minutes after their fetch, with today's fallback including wildcards`.
### Task 6: Probe mode and the console's probe
**Files:**
-- Modify: `internal/edge/edge.go` (`probe := r.Header.Get("X-Carlos-Probe") == "local"`; **in `backhaulTo`** (the one forwarding boundary, reached by the ordinary path, the wildcard-exact path and the door-authorised path): `probe` → 503 "not served here", no dial, before the owner and hop checks' side effects; for `decLocal` with `probe`: `w = &servedByWriter{ResponseWriter: w, box: e.boxID}`; `Edge.WithBoxID(id string)`), `internal/edge/servedby.go` (new: `servedByWriter`), `internal/console/selfserve_health.go` (`probeTarget` sends `X-Carlos-Probe: local` and requires `X-Carlos-Served-By == row.Participants.Target.BoxID` and `x-carlos-version == the sealed build`)
+- Modify: `internal/edge/edge.go` (`probe` detection; the refusal in `backhaulTo`; for `decLocal` with `probe`: `cacheable = false` (no cache lookup, no `maybeStore`) and `w = &servedByWriter{...}` installed **outermost**, before the gzip writer and `countingWriter`; `Edge.WithBoxID`), `internal/edge/servedby.go` (new), `internal/console/selfserve_health.go` (`probeTarget` requires `X-Carlos-Served-By == row.Participants.Target.BoxID` and the sealed build)
- Test: `internal/edge/probe_test.go`, `internal/console/selfserve_health_test.go`
**Interfaces:**
-- `type servedByWriter struct { http.ResponseWriter; box string; wrote bool }`: `WriteHeader(code)` and the first `Write` (an implicit 200) both call `commit()`, which deletes every `X-Carlos-Served-By` value and sets this box's id once; `Flush()` commits then flushes when the underlying writer is an `http.Flusher`; `Unwrap()` returns the underlying writer so `http.ResponseController` (hijack, flush, deadlines) keeps working; the wrapper is placed outermost for the probe request only, so ordinary responses (and their caching) are untouched; probe responses carry `Cache-Control: no-store` added at commit. The generation proxy's `X-Carlos-Version` stays authoritative.
+- `type servedByWriter struct { http.ResponseWriter; box string; final bool }`: `WriteHeader(code)`: `code < 200` → pass through (informational, no commit); otherwise commit once (`Del` every `X-Carlos-Served-By`, `Set` the box id, `Set Cache-Control: no-store`) and pass through; the first `Write` without a prior final `WriteHeader` → commit with 200 then write; `Flush()` → commit (200) if not yet, then flush when the underlying is an `http.Flusher`; `Hijack()` → commit is skipped (an upgrade's 101 is written on the connection by the hijacker; the probe never upgrades), delegate to the underlying `http.Hijacker` **directly** (so `countingWriter`'s assertion finds it); `Unwrap()` for `http.ResponseController`.
-- [ ] **Step 1: Write the failing tests**: `TestProbeModeNeverPassesABackhauledAnswer` (the three forwarding paths → 503 and no dial; ordinary requests on those paths unchanged); `TestServedByStampedOnEveryCommitPath` (an explicit `WriteHeader`, an implicit 200 from `Write`, a HEAD, a `Flush` before any write, multiple app-supplied values replaced; a streaming upgrade still works through `Unwrap`); `TestConsoleProbeRequiresServedByAndVersion` (missing header, another id, a label instead of an id, wrong or missing version, a 3xx, the edge's 503 → fail; the pinned id and version → pass).
-- [ ] **Step 2: Run to verify they fail; Step 3: implement; Step 4: run** `go test ./internal/edge -run 'Probe|ServedBy' -count=1`, `go test ./internal/console -run 'ConsoleProbe' -count=1`.
-- [ ] **Step 5: Commit** `edge: probe mode refuses to forward at the one boundary and stamps the serving box id; the console's health probe requires it`.
+- [ ] **Step 1: Write the failing tests**: `TestProbeModeNeverPassesABackhauledAnswer` (the three forwarding paths); `TestProbeModeThroughTheRealStack` (`Edge.ServeHTTP` with gzip and the cache on: an implicit 200, a HEAD, a `Flush`, a `103` then `200` with a forged final header → the final response carries this box's id; the response is not stored and a later ordinary request misses the cache; an upgrade request on a non-probe path still hijacks); `TestConsoleProbeRequiresServedByAndVersion`.
+- [ ] **Step 2: Run; Step 3: implement; Step 4: run** `go test ./internal/edge -run 'Probe|ServedBy' -count=1`, `go test ./internal/console -run 'ConsoleProbe' -count=1`. **Step 5: Commit** `edge: probe mode refuses to forward and stamps the serving box on the final response through the real writer stack`.
### Task 7: End to end
**Files:**
-- Test: `internal/e2e/selfserve/holding_test.go` (plan 2's harness with a third edge process reading the edges files, and a custom domain attached as an explicit alias)
+- Test: `internal/e2e/selfserve/holding_test.go`
-- [ ] **Step 1: Write the tests**: `TestHoldingPageOnAThirdEdgeAndACustomDomain` (from start to the target's opened publication the third edge answers 503 with the page for the host and the alias; after the open the page is replaced by the backhaul to the target **before** the health phase passes; an app that then fails on the target answers as the app does, no hold returns; the source box's own edge answers the page for its held row); `TestAbortAfterLandingThenSourceReconstructionWithRetainedTargetHold` (the §11 witness: abort from landed; delete the source row; reconcile reconstructs it at `(1, 3)`; the target's holding publication at `(1, 1)` is retained on the third edge; the reconstructed route wins); `TestReadyRouteWinsOverStaleHoldAndHoldOverStaleRoute`; `TestFirstMoveWithPairlessRetainedPublication`; `TestRecreatedHostOutranksOldHoldingAndReadyPublications`; `TestCurrentHoldBeatsPreviousLifetimesReadyRoute`; `TestEdgeRestartedIntoMixedPublications`; `TestExpiredHoldingEntryFallsBackIncludingAfterAWholeRefreshFails`; `TestAliasWitnessesEndToEnd`.
-- [ ] **Step 2: Run them; fix what they find within this plan's scope.**
-- [ ] **Step 3: Run both census tests and the cross-builds**; write `docs/superpowers/notes/census-proposal-self-serve-moves-3.md` with the overlay's loader block, the changed envelopes and the registry field.
-- [ ] **Step 4: Commit** `self-serve moves: the holding page end to end on a third edge and a custom domain`; push.
+- [ ] **Step 1: Write the tests**: rev 2's list with the reconstruction witness made exact: `TestAbortAfterLandingThenSourceReconstructionWithRetainedTargetHold` (start → source held `(1, 1)`; flip; abort → source reopened `(1, 2)`; finish → `move_seq` 3; **after finish** delete the source row; reconcile mints `(1, 3)` and the edges file publishes it; the target's holding publication at `(1, 1)` is retained on the third edge; the reconstructed route wins the request).
+- [ ] **Step 2: Run them; fix what they find within this plan's scope. Step 3: both census tests and the cross-builds; the census note with the console envelope. Step 4: Commit** `self-serve moves: the holding page end to end`; push.
---
## Self-review notes
-- Spec coverage: §6.4 Publication (T1), Rendering (T2), TLS (T2), The display name (T2), Aliases (T2, T3), the decoder audit (T4), Freshness (T5), Reopening (T2's arbitration and T7's witness), §7.4 probe mode (T6), §11's holding witnesses (T7).
-- Known limit, stated: the page's client script is pinned by assertions on the rendered script's text (`until`, the cadence, the storage key) and by the server-rendered sentences; no browser runs in CI.
-- Names used across tasks: `RouteAlias.Explicit`, `aliasEligible`, `holdingEntry`, `holdingSource`, `holdingView`, `candidate`/`candKind`, `decision`/`decKind`, `arbitrate`, `homeMap.resolve`, `Edge.resolve`, `holdingValidity`, `renderHoldingPage`, `holdingAnswer`, `SetHoldingSource`, `RefreshHolds`, `WithBoxID`, `servedByWriter`, `probeTarget` (plan 2).
+- Spec coverage as rev 2; the arbitration is now a ranking with invariants (T2, T3) and exact-versus-request resolution (T2, T5).
+- Known limit, stated: the page's client script is pinned by text assertions on each branch; no browser runs in CI.
+- Names used across tasks: `RouteAlias.Explicit`, `edgesfile.AliasPublishable`, `holdingEntry`, `holdingSource`, `offlineView.rev/onChange`, `holdingView`, `candidate`/`candKind`/`order`, `decision`/`decKind`, `rank`, `resolveExact`, `resolveRequest`, `Edge.resolve`, `holdingValidity`, `renderHoldingPage`, `holdingAnswer`, `SetHoldingSource`, `RefreshHolds(rev)`, `WithBoxID`, `servedByWriter`, `probeTarget` (plan 2).
- Review Focus 1 to 5 are pinned by T5, T3, T3, T2, T6.