carlos / platform Public

plan: self-serve moves 3, rev 5 (Astra plan review 4: F1, F2, F4, F5, F7, F10, N1 to N3; the fold is called not restated, a sorted holding fold, hold rebuilds from the latest inputs on view and Reload, dispatch exceptions kept)

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

CI passed — run details

CI log
=== gate ===
.amadan/test-gate.sh
=== launchctx (0 s)
ok  	amadan.net/carlos/platform.git/launchctx	0.015s
?   	amadan.net/carlos/platform.git/launchctx/controlname	[no test files]
?   	amadan.net/carlos/platform.git/launchctx/wireid	[no test files]
=== vet (1 s)
=== gofmt (0 s)
gofmt -l .: clean
=== rest (143 s)
?   	github.com/carlosframework/platform/cmd/changelog	[no test files]
ok  	github.com/carlosframework/platform/cmd/console	3.949s
ok  	github.com/carlosframework/platform/cmd/moveprobe	11.943s
ok  	github.com/carlosframework/platform/cmd/zddprobe	0.749s
ok  	github.com/carlosframework/platform/internal/activator	61.291s
ok  	github.com/carlosframework/platform/internal/agentsock	0.018s
ok  	github.com/carlosframework/platform/internal/alerting	0.007s
ok  	github.com/carlosframework/platform/internal/analytics	0.070s
ok  	github.com/carlosframework/platform/internal/appconfig	3.525s
ok  	github.com/carlosframework/platform/internal/apperr	0.101s
ok  	github.com/carlosframework/platform/internal/applog	0.327s
ok  	github.com/carlosframework/platform/internal/awsinv	0.006s
ok  	github.com/carlosframework/platform/internal/boxchoice	0.006s
ok  	github.com/carlosframework/platform/internal/boxid	0.845s
?   	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.013s
ok  	github.com/carlosframework/platform/internal/caps	0.013s
ok  	github.com/carlosframework/platform/internal/certwarm	0.002s
ok  	github.com/carlosframework/platform/internal/cgroup	0.760s
ok  	github.com/carlosframework/platform/internal/changelog	0.605s
ok  	github.com/carlosframework/platform/internal/claimactivation	1.373s
ok  	github.com/carlosframework/platform/internal/clamd	0.059s
?   	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.248s
ok  	github.com/carlosframework/platform/internal/dbdoor/dbdoortest	0.014s
ok  	github.com/carlosframework/platform/internal/dbverify	0.005s
ok  	github.com/carlosframework/platform/internal/dnsrec	0.003s
ok  	github.com/carlosframework/platform/internal/docsite	0.014s
ok  	github.com/carlosframework/platform/internal/domains	3.333s
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	45.504s
?   	github.com/carlosframework/platform/internal/edgeproof	[no test files]
ok  	github.com/carlosframework/platform/internal/edgesfile	0.008s
ok  	github.com/carlosframework/platform/internal/fleetapi	0.200s
ok  	github.com/carlosframework/platform/internal/gate	1.857s
ok  	github.com/carlosframework/platform/internal/genselect	0.017s
ok  	github.com/carlosframework/platform/internal/geo	0.019s
ok  	github.com/carlosframework/platform/internal/geoip	0.040s
ok  	github.com/carlosframework/platform/internal/historyreadback	0.253s
ok  	github.com/carlosframework/platform/internal/host	139.110s
ok  	github.com/carlosframework/platform/internal/hostactors	6.588s
ok  	github.com/carlosframework/platform/internal/hostactors/awsmembership	0.009s
ok  	github.com/carlosframework/platform/internal/hostbox	3.502s
ok  	github.com/carlosframework/platform/internal/hostclaim	0.981s
ok  	github.com/carlosframework/platform/internal/hostenv	0.008s
ok  	github.com/carlosframework/platform/internal/hostevidence	0.009s
ok  	github.com/carlosframework/platform/internal/hostexec	6.134s
ok  	github.com/carlosframework/platform/internal/hostfence	0.059s
ok  	github.com/carlosframework/platform/internal/hostfiles	10.332s
ok  	github.com/carlosframework/platform/internal/hostfloor	0.076s
ok  	github.com/carlosframework/platform/internal/hostguard	1.072s
ok  	github.com/carlosframework/platform/internal/hostops	2.397s
?   	github.com/carlosframework/platform/internal/hostops/fencerelease	[no test files]
ok  	github.com/carlosframework/platform/internal/hostops/internal/fencerelease	0.003s
ok  	github.com/carlosframework/platform/internal/hostquarantine	0.004s
ok  	github.com/carlosframework/platform/internal/hostrelocate	0.669s
ok  	github.com/carlosframework/platform/internal/hostresolve	0.256s
ok  	github.com/carlosframework/platform/internal/hostwrite	0.004s
ok  	github.com/carlosframework/platform/internal/infraview	0.007s
ok  	github.com/carlosframework/platform/internal/instancectl	5.276s
ok  	github.com/carlosframework/platform/internal/instanceexec	2.854s
ok  	github.com/carlosframework/platform/internal/instancefs	0.015s
ok  	github.com/carlosframework/platform/internal/instancekey	0.005s
ok  	github.com/carlosframework/platform/internal/instanceupdates	0.009s
ok  	github.com/carlosframework/platform/internal/layout	2.159s
ok  	github.com/carlosframework/platform/internal/lbhealth	0.003s
ok  	github.com/carlosframework/platform/internal/lease	10.182s
ok  	github.com/carlosframework/platform/internal/lease/leasetest	0.677s
ok  	github.com/carlosframework/platform/internal/ledger	0.048s
ok  	github.com/carlosframework/platform/internal/mail	0.286s
ok  	github.com/carlosframework/platform/internal/maintenance	0.098s
ok  	github.com/carlosframework/platform/internal/maintenanceexec	5.711s
ok  	github.com/carlosframework/platform/internal/maintenanceguard	0.118s
ok  	github.com/carlosframework/platform/internal/metrics	0.008s
ok  	github.com/carlosframework/platform/internal/moves	21.683s
ok  	github.com/carlosframework/platform/internal/moves/archive	1.115s
ok  	github.com/carlosframework/platform/internal/objstore	0.323s
ok  	github.com/carlosframework/platform/internal/ops	0.268s
ok  	github.com/carlosframework/platform/internal/park	0.354s
ok  	github.com/carlosframework/platform/internal/plans	0.006s
ok  	github.com/carlosframework/platform/internal/privatefile	0.003s
ok  	github.com/carlosframework/platform/internal/provision	14.133s
ok  	github.com/carlosframework/platform/internal/publicrelease	0.010s
?   	github.com/carlosframework/platform/internal/regionname	[no test files]
ok  	github.com/carlosframework/platform/internal/registry	5.880s
ok  	github.com/carlosframework/platform/internal/relay	4.242s
ok  	github.com/carlosframework/platform/internal/release	25.617s
ok  	github.com/carlosframework/platform/internal/rollout	3.964s
ok  	github.com/carlosframework/platform/internal/rolloutwords	0.005s
ok  	github.com/carlosframework/platform/internal/rootaws	0.007s
ok  	github.com/carlosframework/platform/internal/rootparticipation	0.065s
ok  	github.com/carlosframework/platform/internal/rootstate	0.036s
ok  	github.com/carlosframework/platform/internal/runs	0.576s
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.914s
ok  	github.com/carlosframework/platform/internal/seeds	0.020s
ok  	github.com/carlosframework/platform/internal/sim/hostlife	12.431s
ok  	github.com/carlosframework/platform/internal/sim/litestreamfacts	3.942s
ok  	github.com/carlosframework/platform/internal/status	0.149s
ok  	github.com/carlosframework/platform/internal/steering	0.010s
ok  	github.com/carlosframework/platform/internal/stopevidence	0.256s
ok  	github.com/carlosframework/platform/internal/svcrole	0.007s
ok  	github.com/carlosframework/platform/internal/telemetry	0.011s
ok  	github.com/carlosframework/platform/internal/termfence	0.011s
ok  	github.com/carlosframework/platform/internal/tunnelclient	0.005s
ok  	github.com/carlosframework/platform/internal/unixdial	0.007s
ok  	github.com/carlosframework/platform/internal/updatewords	0.006s
ok  	github.com/carlosframework/platform/internal/usage	0.007s
ok  	github.com/carlosframework/platform/internal/version	0.004s
ok  	github.com/carlosframework/platform/internal/vet	1.790s
=== internal_console@0 (83 s)
ok  	github.com/carlosframework/platform/internal/console	81.181s
=== internal_console@1 (38 s)
ok  	github.com/carlosframework/platform/internal/console	35.722s
=== internal_console@2 (42 s)
ok  	github.com/carlosframework/platform/internal/console	39.884s
=== internal_console@3 (37 s)
ok  	github.com/carlosframework/platform/internal/console	35.482s
=== internal_console@4 (38 s)
ok  	github.com/carlosframework/platform/internal/console	35.729s
=== internal_console@5 (35 s)
ok  	github.com/carlosframework/platform/internal/console	33.223s
=== internal_console@6 (37 s)
ok  	github.com/carlosframework/platform/internal/console	34.592s
=== internal_console@7 (44 s)
ok  	github.com/carlosframework/platform/internal/console	41.566s
=== root@0 (33 s)
ok  	github.com/carlosframework/platform	31.192s
=== root@1 (45 s)
ok  	github.com/carlosframework/platform	42.636s
=== root@2 (42 s)
ok  	github.com/carlosframework/platform	40.355s
=== 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
linux/arm64 carlos binary: 50069666 bytes (ceiling 52000000)
--- 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.024s
=== RUN   TestT4StorageProbeMinio
2026/10/22 09:23:34 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18E0CFB1BF95C314, 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.022s
minio contract: PASS
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 49596d873..1e8dc93f5 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,7 +2,7 @@
> **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 4, 2026-10-22. Reviews: rev 1 (`after236/selfserve-plan3-review.md`, 15 findings), rev 2 (`selfserve-plan3-review2.md`, 14), rev 3 (`selfserve-plan3-review3.md`, F1 to F10). Rev 4 answers rev 3's ten and is self-contained (nothing is "as rev 2"; F8). The ranking is restated so that it **cannot** disagree with plan 1's fold (F1, F2): a holding candidate's only power over ready candidates is to **suppress** the stale ready publications of its own target (same `targetHost`, unstamped, lower pair); the ready candidates that survive are folded by the **same function** `foldHomes` uses, so ready-only routing is plan 1's by construction and a differential test pins it; only when no ready candidate survives does a hold answer. The invariants are restated to be consistent with that algorithm and with §6.4 ("eligible under ordinary routing" = not a suppressed stale publication of the moving target); the "real hold beats a stamped alias" test is withdrawn as contradicting §6.4. The decision carries the whole local table it was resolved against (F3). Candidate construction is complete for local ready, local holding (aliases expanded through the writer's own predicate) and remote, with the existing admissions kept (F4). Local holds live in their own atomic snapshot that `Reload` never touches, and the view publishes revision and contents in one immutable value (F5). `Allowed` keeps the tunnel exception; a separate certificate-name enumeration covers holds for warming without making them served evidence (F6). `AliasPublishable` includes the primary, the name exclusions, and no owner parameter (F7). The countdown arithmetic and the inherited contracts are restated in full (F8). The discovery check includes `internal/steering` and a reviewed exclusion list (F9). The census names `internal/edgesfile` and the loader (F10).
+Rev 5, 2026-10-22. Rev 4's review (`selfserve-plan3-review4.md`) is answered: the ready stage **calls the shared fold** and no longer restates it (F2); the invariants are scoped to the four stages and (e), (g), (h), (j) are qualified (F1); the holding stage is a **sorted fold** with a pinned three-hold witness (N2); hold rebuilds read the latest view snapshot and the registry under one mutex and are triggered by both the view and `Reload`, and the view publishes under its own mutex (F5); the dispatch exceptions for accepted hops, door requests and a failed backhaul over a local wildcard are preserved and tested (N1); the home map's wake compares the effective parent set including platform holds (N3); local ready aliases are never platform, `candCert` is constructed explicitly, the unlabelled-box refusal is kept, and the view snapshot crosses packages as an exported `edge.HoldSnapshot` (F4); `AliasPublishable` checks `a.Alias` (F7); the census overlay loads in `plan14_root_reads_test.go` before `compareRootReads` (F10). Reviews: rev 1 (`after236/selfserve-plan3-review.md`, 15 findings), rev 2 (`selfserve-plan3-review2.md`, 14), rev 3 (`selfserve-plan3-review3.md`, F1 to F10). Rev 4 answers rev 3's ten and is self-contained (nothing is "as rev 2"; F8). The ranking is restated so that it **cannot** disagree with plan 1's fold (F1, F2): a holding candidate's only power over ready candidates is to **suppress** the stale ready publications of its own target (same `targetHost`, unstamped, lower pair); the ready candidates that survive are folded by the **same function** `foldHomes` uses, so ready-only routing is plan 1's by construction and a differential test pins it; only when no ready candidate survives does a hold answer. The invariants are restated to be consistent with that algorithm and with §6.4 ("eligible under ordinary routing" = not a suppressed stale publication of the moving target); the "real hold beats a stamped alias" test is withdrawn as contradicting §6.4. The decision carries the whole local table it was resolved against (F3). Candidate construction is complete for local ready, local holding (aliases expanded through the writer's own predicate) and remote, with the existing admissions kept (F4). Local holds live in their own atomic snapshot that `Reload` never touches, and the view publishes revision and contents in one immutable value (F5). `Allowed` keeps the tunnel exception; a separate certificate-name enumeration covers holds for warming without making them served evidence (F6). `AliasPublishable` includes the primary, the name exclusions, and no owner parameter (F7). The countdown arithmetic and the inherited contracts are restated in full (F8). The discovery check includes `internal/steering` and a reviewed exclusion list (F9). The census names `internal/edgesfile` and the loader (F10).
Builds on plan 1 (rev 10: `(incarnation, serving_seq)` on rows and publications, `target_host` on alias publications, the fold's pair rule in `dialBeats`/`foldHomes`, `edgesFileWriteClaims(..., lifetimes bool)`, the cutover gate) and plan 2 (rev 4: `SelfServeMove{DisplayName, OfflineAt, EstimateSeconds}`, `Participants`, `moveCfg.BoxID`, `probeTarget`).
@@ -19,13 +19,13 @@ Builds on plan 1 (rev 10: `(incarnation, serving_seq)` on rows and publications,
- `offline_moves` is never a reason to count a host as served (Task 4's inventory). The claims-on writer publishes no `offline_moves`.
- A remote holding candidate is believed only while `now.Sub(fetchedAt) < holdingValidity` (5 min; at exactly 5 m 0 s it is expired); 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).
-- **Ready-only routing is plan 1's fold.** With no holding candidate in play, `rank` returns exactly what `foldHomes` plus the local-first rule return today (Task 3's differential test); local ready candidates win over remote ones as today (the upstream table is consulted first).
+- **Ready-only routing is plan 1's fold.** The remote ready stage **calls** the shared fold (`foldHomes`'s loop over `dialBeats`, with plan 1 Task 8's pair rule: stamps first, pairs only when both entries are unstamped and share a non-empty target); the plan never restates that rule. With no holding candidate in play, `rank` returns exactly what the fold plus the local-first rule return today (Task 3's differential test); local ready candidates win over remote ones as today (the upstream table is consulted first). The dispatch exceptions of today's `ServeHTTP` are kept: an accepted hop or a door request whose exact name has no local candidate but a local wildcard upstream is served from that wildcard, whatever the remote candidates say; a failed `backhaulTo` (no mesh key) falls through to the local wildcard as today.
- 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 `AliasPublishable` (below).
- The display name is the only request-supplied text on the page, rendered through `html/template`; no other request text (URL, headers, host) appears in the body.
- **The page.** Status 503, `Retry-After: 30`, `Cache-Control: no-store`, `X-Robots-Tag: noindex`, `Content-Type: text/html; charset=utf-8`. Sentences (copy rows pending Paul's review): with a name and an estimate ahead: "<display name> is temporarily offline. Please check back in approximately N minutes."; without a name and an estimate ahead: "This account is temporarily offline. Please check back in approximately N minutes."; no estimate (`until == 0`): "This account is temporarily offline. Please check back in a few minutes."; estimate passed (`until <= now`): "This is taking longer than expected. Please keep checking back."; the probe refusal: "not served here". N = `ceil((until - now) / 60 s)` with a minimum of 1, computed by the server at render time; the client never recomputes N (each reload renders it afresh). The client script: `until` (unix seconds, 0 for none) and `host` are embedded as JSON; while `until > 0 && until > now` it reloads by `location.replace(location.href)` (a GET, so a POST never repeats) every 30 s, and at the instant `now >= until` it replaces the sentence with the taking-longer sentence locally and reloads every 60 s thereafter; with `until == 0` it keeps the no-estimate sentence and reloads every 60 s; the sessionStorage key `carlos-holding-<host>` records the first-seen time (informational; distinct from the pending page's key); any storage exception is caught and never stops the reload. HEAD writes the headers and no body.
- 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; a stamped ready candidate is never suppressed (holds are never stamped).
-- Census: `internal/provision/testdata/plan14-root-reads-self-serve-moves-3.tsv`, loaded by a new `loadRootReadInventory(root+"/internal/provision/testdata/plan14-root-reads-self-serve-moves-3.tsv", &inv, true)` block in `internal/provision/plan14_legacy_identity_test.go` **after every existing overlay block and before the comparison**; `rev3=PROPOSED-self-serve-moves-3`; envelopes `internal/host`, `internal/edge`, `internal/edgesfile` (Task 1's `eligible.go`), `internal/domains`, `internal/registry`, `internal/console` (Task 6's probe), `internal/steering` only if its test file counts (verify; test files do not); the tracked field `route_aliases.explicit`; reviewed dispositions for changed frozen units; `TestPlan9B8RecordWriteCensus` unchanged and green.
+- Census: `internal/provision/testdata/plan14-root-reads-self-serve-moves-3.tsv`, loaded by a new `loadRootReadInventory(root+"/internal/provision/testdata/plan14-root-reads-self-serve-moves-3.tsv", &inv, true)` block in `internal/provision/plan14_root_reads_test.go` (the file that holds `TestPlan14RootReadCensus`) **after every existing overlay block and before `compareRootReads`**; `rev3=PROPOSED-self-serve-moves-3`; envelopes `internal/host`, `internal/edge`, `internal/edgesfile` (Task 1's `eligible.go`), `internal/domains`, `internal/registry`, `internal/console` (Task 6's probe), `internal/steering` only if its test file counts (verify; test files do not); 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
@@ -45,11 +45,11 @@ Builds on plan 1 (rev 10: `(incarnation, serving_seq)` on rows and publications,
- Test: `internal/host/edgesfile_holding_test.go`, `internal/domains/aliases_explicit_test.go`, `internal/registry/alias_explicit_test.go`, `internal/edgesfile/eligible_test.go`
**Interfaces:**
-- `func AliasPublishable(a registry.RouteAlias, primaryServed, shadowed bool) bool` = `a.Host != "" && !strings.HasPrefix(a.Host, "*.") && a.DNSStatus == "pointing" && primaryServed && !shadowed`. `primaryServed` means "the alias's primary row is in this writer's served set or is a held row it publishes". There is **no owner parameter**: ownership validation is `domains.ReconcileAliases`'s (a row that exists has passed it); the owner projection (`servedOwners`) may be empty for a hostname spanning apps and that hostname still publishes and routes publicly (the door refuses it, as today). The writer's alias loop for `hosts` and the holding entries' `Aliases` both call this function.
+- `func AliasPublishable(a registry.RouteAlias, primaryServed, shadowed bool) bool` = `a.Alias != "" && !strings.HasPrefix(a.Alias, "*.") && a.DNSStatus == "pointing" && primaryServed && !shadowed` (`a.Alias` is the published name; `a.Host` is the primary). `primaryServed` means "the alias's primary row is in this writer's served set or is a held row it publishes". There is **no owner parameter**: ownership validation is `domains.ReconcileAliases`'s (a row that exists has passed it); the owner projection (`servedOwners`) may be empty for a hostname spanning apps and that hostname still publishes and routes publicly (the door refuses it, as today). The writer's alias loop for `hosts` and the holding entries' `Aliases` both call this function.
- `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, possibly empty).
- `type holdingSource func(host string) (provision.MoveState, bool)` = `offlineView.holding`.
- **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: r.Host, Platform: r.Platform || r.Provisioned, Account/App: the owner projection of r, Aliases, MoveID: mv.ID, Incarnation: r.Incarnation, ServingSeq: r.ServingSeq, OfflineAt: mv.SelfServe.OfflineAt.Unix() (0 when `SelfServe == nil` or the time is zero), EstimateSeconds: mv.SelfServe.EstimateSeconds (0 when nil), DisplayName: mv.SelfServe.DisplayName ("" when nil)}`; `Aliases` = the `route_aliases` rows with `a.Host == r.Host && a.Explicit && AliasPublishable(a, true, shadowedByServed(a.Alias))`, sorted. **Stop conditions:** a ready row publishes none; a held row whose `HoldOp` the view does not know (no move, or a move with another ID) publishes none; the entry stops when the record no longer carries that move id (the view) or the row turns ready. The target's `landing` row publishes from flip until `ReleaseHoldSeq`. Entries sorted by host. Only in lifetimes mode (`lifetimes == true`); region HA on or off makes no difference.
-- **The view's snapshot (F5):** `type offlineHolds struct { Rev uint64; Holds map[string]provision.MoveState; Failed bool }` immutable once published; `offlineView.snapshot() *offlineHolds` (atomic pointer load); `refresh` publishes `{Rev: prev+1, Holds: new, Failed: false}`; `fail` publishes `{Rev: prev+1, Holds: nil, Failed: true}`; `holding(host)` reads the current snapshot; `offlineView.onChange func(*offlineHolds)` is called after every publication with the snapshot it published (never with a reconstructed one).
+- **The view's snapshot (F5):** `type offlineHolds struct { Rev uint64; Holds map[string]provision.MoveState; Failed bool }` immutable once published; publication (`refresh` and `fail`) runs under `offlineView.pubMu`, so revisions are allocated and stored in one critical section (no two publishers share a revision); `offlineView.snapshot() *offlineHolds` (atomic pointer load); `refresh` publishes `{Rev: prev+1, Holds: new, Failed: false}`; `fail` publishes `{Rev: prev+1, Holds: nil, Failed: true}`; `holding(host)` reads the current snapshot; `offlineView.onChange func(edge.HoldSnapshot)` is called after every publication, inside `pubMu`, with the **adapter** of the snapshot it published: `edge.HoldSnapshot{Rev, Failed, Holds: map[host]edge.HoldState{MoveID, DisplayName, OfflineAt (unix seconds), EstimateSeconds}}` (an exported DTO declared in `internal/edge`; `internal/host` already depends on `internal/edge`, never the reverse).
- [ ] **Step 1: Write the failing tests**: `TestAliasReconcilerWritesProvenance` (a claim-backed alias → explicit; a derived instance alias → not; a legacy row upgraded); `TestDerivedAliasClearsExplicitOnTheSameTarget` (a detached explicit claim whose name is then derived to the same target → `Explicit: false`, the fingerprint changes, the next holding publication omits it); `TestAliasFingerprintIncludesExplicit`; `TestAliasPublishable` (absent primary → false; `*.` name → false; empty name → false; not pointing → false; shadowed → false; pointing with an empty owner projection → true); `TestWriterPublishesHoldingEntriesOnlyInLifetimesMode` (lifetimes on with region HA off and on → entries; lifetimes off with either → none; `applyEdgesClaims` still runs before the served alias construction); `TestHoldingEntryListsOnlyExplicitPublishableAliases` (explicit pointing → listed; explicit not pointing → not; derived exact name → not; shadowed by a served row → not; the synthetic catch-all row → not; a mixed-app prefix host → listed with empty `Account/App`); `TestTargetPublishesWhileLanding`; `TestHoldingEntryStopsWhenTheRecordClearsOrChangesID`; `TestViewSnapshotIsImmutableAndRevisioned` (refresh then fail then refresh → revs 1, 2, 3; `onChange` receives each published value; a reader holding snapshot 1 still sees snapshot 1's holds; the moves pass's listing error publishes a failed snapshot).
- [ ] **Step 2: Run to verify they fail; Step 3: implement; Step 4: run** `go test ./internal/host -run 'Holding|ViewSnapshot' -count=1`, `go test ./internal/domains -run 'Explicit|Provenance' -count=1`, `go test ./internal/registry -run 'Alias' -count=1`, `go test ./internal/edgesfile -run 'AliasPublishable' -count=1`.
@@ -58,29 +58,29 @@ Builds on plan 1 (rev 10: `(incarnation, serving_seq)` on rows and publications,
### 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`; `homeSnapshot.cands map[string][]candidate`; `foldHomes` is **kept as the ready-only fold** and gains the pair rule from plan 1; `buildCandidates(files, self, privateDial) map[string][]candidate` runs the same per-file admission loop; `rank`, `resolveExact`, `resolveRequest`; `Home`, `ExactHome`, `Owner`, `KnownHost`, `Hosts`, `ServedHosts` over the exact resolution; `HoldingHosts()` new), `internal/edge/holding.go` (new: `renderHoldingPage`, `holdingAnswer`, `holdSnapshot`, `RefreshHolds`), `internal/edge/edge.go` (`resolve`, the request paths, `Allowed`, `certNames`), `internal/edge/certsync.go`, `internal/edge/certwarm.go` (`warmSet` adds `certNames()`), `internal/edge/wildcard.go` (exact predicates; `currentHosts` adds held platform rows), the agent (`edge.SetHoldingSource(view)`, `view.onChange = func(s *offlineHolds) { edge.RefreshHolds(s) }`, `edge.WithBoxID(boxID)` where `boxID` is the same value plan 2 gives `moveCfg.BoxID`)
+- Modify: `internal/edge/homemap.go` (`edgesFileView.OfflineMoves []holdingView \`json:"offline_moves"\`` and `fetchedAt time.Time`; `homeSnapshot.cands map[string][]candidate`; `foldHomes` is **kept as the ready-only fold** and gains the pair rule from plan 1; `buildCandidates(files, self, privateDial) map[string][]candidate` runs the same per-file admission loop; `rank`, `resolveExact`, `resolveRequest`; `Home`, `ExactHome`, `Owner`, `KnownHost`, `Hosts`, `ServedHosts` over the exact resolution; `HoldingHosts()` new), `internal/edge/holding.go` (new: `HoldSnapshot`, `HoldState`, `renderHoldingPage`, `holdingAnswer`, `holdSnapshot`, `SetHoldView`, `RefreshHolds`), `internal/edge/edge.go` (`resolve`, the request paths, `Allowed`, `certNames`), `internal/edge/certsync.go`, `internal/edge/certwarm.go` (`warmSet` adds `certNames()`), `internal/edge/wildcard.go` (exact predicates; `currentHosts` adds held platform rows), the agent (`view.onChange = edge.SetHoldView` (which stores the snapshot and calls `RefreshHolds()`), `edge.WithBoxID(boxID)` where `boxID` is the same value plan 2 gives `moveCfg.BoxID`)
- Test: `internal/edge/rank_test.go`, `internal/edge/holding_test.go`, `internal/edge/homemap_test.go`, `internal/edge/decision_test.go`
**Interfaces:**
- `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); table map[string]upstream (local only: the table `up` came from) }`.
-- **Remote constructors** (`buildCandidates`): the per-file admission is `foldHomes`'s today, unchanged: files iterated in sorted key order (`order` = index); `f.Box == ""` or `f.Box == self` skipped (own-box exclusion); `addr = dialAddress(f, own, ownKnown, privateDial)` (so same-region private dialling is retained); `hosts` entries: role `home` with `addr == ""` → cert-only only for a `*.` wildcard key else skipped; role `wildcard` only on a `*.` key else skipped; unknown roles skipped; a ready candidate `{candReady, eip: addr, box, account, app, alias: h.Alias, wildcard: h.Wildcard, platform: h.Platform && !h.Alias && !h.Wildcard, targetHost: h.TargetHost (plan 1; "" when absent) for an alias, the host itself for a route, incarnation/servingSeq (plan 1), stamp: h.Stamp, fetchedAt}`; `offline_moves` entries (only when `addr != ""`, the same requirement a dial target has): one `candHolding` for `Host` `{targetHost: Host, platform: entry.Platform, account/app: entry's, incarnation/servingSeq: entry's, alias: false, hold: &entry}` and one alias `candHolding` per listed alias `{alias: true, targetHost: Host, platform: false, the same owner and pair, hold: &entry}`; holds carry `stamp: 0` always.
-- **Local constructors** (`Reload` for ready, `RefreshHolds` for holds): for every entry `(name, up)` of the table `Reload` just built: a `candReady{local: true, order: -1, alias: up.alias, targetHost: up.route.Host when up.alias else name, account/app: from up.route, incarnation/servingSeq: up.route's pair, platform: up.route.Platform || up.route.Provisioned, wildcard: strings.HasPrefix(name, "*."), stamp: 0 (local candidates' stamps are not consulted: local-first is the rule, as today), up: &up, table: the new table}`. Local holds: for every registry row the hold snapshot knows (`registry.IsHold(r.Status) && r.HoldOp == mv.ID`): one real `candHolding{local, targetHost: r.Host, platform: r.Platform || r.Provisioned, owner from the row, pair from the row, hold: a holdingView built from the row and the move}` and one alias `candHolding{alias: true, targetHost: r.Host, platform: false}` per `route_aliases` row with `a.Host == r.Host && a.Explicit && edgesfile.AliasPublishable(a, true, shadowed)` (the same predicate the writer uses, so the source box serves its own alias the page even though the home map excludes its own publication).
+- **Remote constructors** (`buildCandidates`): the per-file admission is `foldHomes`'s today, unchanged: files iterated in sorted key order (`order` = index); `f.Box == ""` or `f.Box == self` skipped (own-box exclusion); `addr = dialAddress(f, own, ownKnown, privateDial)` (so same-region private dialling is retained); `hosts` entries: role `home` with `addr == ""` → a `candCert{wildcard: true, platform: h.Platform && !h.Alias && !h.Wildcard}` only when `h.Wildcard && strings.HasPrefix(host, "*.")`, else skipped; role `wildcard` → the same `candCert` only when `h.Wildcard && strings.HasPrefix(host, "*.")`, else skipped; unknown roles skipped (`candCert` is constructed explicitly in those two branches and nowhere else); role `home` with an address → a ready candidate `{candReady, eip: addr, box, account, app, alias: h.Alias, wildcard: h.Wildcard, platform: h.Platform && !h.Alias && !h.Wildcard, targetHost: h.TargetHost (plan 1; "" when absent) for an alias, the host itself for a route, incarnation/servingSeq (plan 1), stamp: h.Stamp, fetchedAt}`; `offline_moves` entries (only when `addr != ""`, the same requirement a dial target has): one `candHolding` for `Host` `{targetHost: Host, platform: entry.Platform, account/app: entry's, incarnation/servingSeq: entry's, alias: false, hold: &entry}` and one alias `candHolding` per listed alias `{alias: true, targetHost: Host, platform: false, the same owner and pair, hold: &entry}`; holds carry `stamp: 0` always.
+- **Local constructors** (`Reload` for ready, `RefreshHolds` for holds): for every entry `(name, up)` of the table `Reload` just built: a `candReady{local: true, order: -1, alias: up.alias, targetHost: up.route.Host when up.alias else name, account/app: from up.route, incarnation/servingSeq: up.route's pair, platform: (up.route.Platform || up.route.Provisioned) && !up.alias && !wildcard (an alias is never a platform class, as `currentHosts` already rules), wildcard: strings.HasPrefix(name, "*."), stamp: 0 (local candidates' stamps are not consulted: local-first is the rule, as today), up: &up, table: the new table}`. Local holds: for every registry row the hold snapshot knows (`registry.IsHold(r.Status) && r.HoldOp == mv.ID`): one real `candHolding{local, targetHost: r.Host, platform: r.Platform || r.Provisioned, owner from the row, pair from the row, hold: a holdingView built from the row and the move}` and one alias `candHolding{alias: true, targetHost: r.Host, platform: false}` per `route_aliases` row with `a.Host == r.Host && a.Explicit && edgesfile.AliasPublishable(a, true, shadowed)` (the same predicate the writer uses, so the source box serves its own alias the page even though the home map excludes its own publication).
- `type decision struct { kind decKind (decLocal, decRemote, decHolding, decNone); up *upstream; table map[string]upstream; eip, box, account, app string; stamp int64; platform, wildcard bool; hold *holdingView }`.
- **`rank(name string, cands []candidate, now time.Time) decision`:**
1. remote `candHolding` with `now.Sub(fetchedAt) >= holdingValidity` are removed;
2. `platform`/`wildcard` are the OR over all candidates; `candCert` never decides; nothing else present → `decNone`;
3. **suppression:** for every remaining `candHolding` h: every `candReady` c with `c.targetHost == h.targetHost && c.targetHost != "" && c.stamp == 0 && (c.incarnation, c.servingSeq) < (h.incarnation, h.servingSeq)` is removed (a stale publication of the moving target). Nothing else is removed: a stamped ready, a ready of another target, a ready with an empty target, a ready with an equal or higher pair all survive;
- 4. **ready:** if any `candReady` survives: a local one → `decLocal` with its `up` and `table` (local first, as today; two local candidates for one name do not occur; the table has one entry per name); else the remote survivors folded by `foldHomes`'s rule applied in `order` (`dialBeats` with plan 1's pair rule: a route beats an alias; a dial target beats none; between two of the same kind sharing a **non-empty** `targetHost` the higher pair wins and an equal pair falls to today's rule; otherwise today's rule: a higher stamp wins, else the earlier `order` keeps it) → `decRemote` with the winner's `eip, box, account, app, stamp`;
- 5. **holding:** no ready survives: among `candHolding`, a real (non-alias) hold beats an alias hold; among equals of the same `targetHost` the higher pair wins; otherwise the lowest `order` (local `-1` first) → `decHolding` with `hold`.
-- **Invariants (Task 3 asserts them on the small model):** (a) with no live `candHolding`, `rank` equals the fold-plus-local-first answer; (b) a stamped ready candidate is never suppressed and never loses to a hold; (c) a ready candidate of a different target than every hold is never suppressed and always beats every hold; (d) a hold suppresses exactly the unstamped ready candidates of its own non-empty target with a lower pair; (e) a ready candidate of the hold's target with an equal or higher pair beats it; (f) an expired remote hold neither suppresses nor decides; (g) pairs are compared only between candidates sharing a non-empty target; (h) a real ready beats an alias ready whatever the pairs and stamps (#287); (i) the decision is independent of input order except through `order`; (j) `platform`/`wildcard` and the owner come from the winner's own candidate (classes OR'd), never from an alias for the certificate parent set.
-- `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 `resolveRequest`/`resolveExact` as today's split; `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`; `ServedHosts()` unchanged (ready winners only; **a hold is never served evidence**); `HoldingHosts(now)` new: the exact names (hosts and aliases) whose `resolveExact` is a live `decHolding`.
-- **The local edge:** `type holdSnapshot struct { rev uint64; cands map[string][]candidate }` in `e.holds atomic.Pointer[holdSnapshot]`, written **only** by `RefreshHolds(s *offlineHolds)`: builds the local holding candidates from `s.Holds` (none when `s.Failed` or `s == nil`) and the registry, and installs under `e.holdsMu` only if `s.Rev` is greater than the installed rev (an older rebuild is discarded); after installing, outside the lock, it sends the wildcard manager the same parent-set notification `Reload` sends after installing a new table. `Reload` never writes `e.holds`, so it cannot reinstall a stale hold. `func (e *Edge) resolve(host string, now time.Time) decision`: the exact candidates = `e.current()`'s entry for `host` (as a local ready candidate), `e.holds.Load().cands[host]`, and the home map's exact candidates; through `rank`; `decNone` and no live exact hold → the same over `*.<parent>` (the local wildcard entry and the remote wildcard candidates). **The handler uses `dec.up` and `dec.table`:** path-prefix selection runs over `dec.up.paths` and resolves the target hostname in `dec.table` (`selectUpstream(dec, r.URL.Path) *upstream`, preserving today's cleaned/raw segment matching, exact target lookup and one-hop selection); the login re-resolution looks its path up in the same `dec.table`; the door's `doorOwnsRoute` checks the **finally selected** upstream's route; a remote decision's owner is `dec.account/app`; `backhaulTo` is called with `dec.eip, dec.box`: one decision and one table for the whole request.
+ 4. **ready:** if any `candReady` survives: a local one → `decLocal` with its `up` and `table` (local first, as today; the table has one entry per name); else the remote survivors go through **the shared fold** (`foldHomes`'s loop in `order`, deciding with `dialBeats` as plan 1 Task 8 defines it: the plan does not restate the rule; the fold is one function called from `foldHomes` and from here) → `decRemote` with the winner's `eip, box, account, app, stamp`;
+ 5. **holding (a sorted fold):** no ready survives: iterate the `candHolding` survivors in `order` (local `-1` first); the first is the incumbent; a later candidate **replaces** the incumbent only when (it is real and the incumbent is an alias) or (both are the same kind, share a non-empty `targetHost`, and its pair is higher); otherwise the incumbent stays → `decHolding` with the winner's `hold`. (Witness: A-old alias `(1,1)` at 0, B alias at 1, A-new alias `(1,2)` at 2 → A-new.)
+- **Invariants, scoped to the stages (Task 3 asserts them on the small model):** *suppression:* (d) a hold removes exactly the unstamped ready candidates sharing its non-empty target with a lower pair; (b) a stamped ready candidate is never suppressed; (f) an expired remote hold neither suppresses nor decides; (g) no pair is compared between candidates with different or empty targets, at any stage. *local precedence:* (l) a surviving local ready candidate wins over every remote candidate and every hold. *remote fold:* (a) with no live hold the remote answer equals `foldHomes`'s and the complete answer equals today's dispatch; (h) among the remote survivors a real ready beats an alias ready whatever the pairs and stamps (#287), as the fold's own rule; (c) a surviving ready of a different target than every hold beats every hold. *holding:* (e) a ready candidate of target T that survives suppression (its pair is not lower than that of any live hold of T) beats every hold; (n) the holding winner is the sorted fold's and is independent of input order except through `order`. *classes and ownership:* (j) `platform`/`wildcard` are OR'd over the candidates (classes are facts about the name) while the owner, dial address, stamp and `up`/`table` come from the winner alone; an alias candidate never carries the platform class.
+- `func (m *homeMap) resolveExact(name string, now time.Time) decision` (the exact candidates; never a wildcard; with `m.self == ""` (an unlabelled box) no remote candidate is admitted, so `Home`/`ExactHome` keep today's refusal to dial from an unlabelled box); `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 `resolveRequest`/`resolveExact` as today's split; `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`; **the wake** (`refreshHosts`' `samePlatformSet`) compares the effective parent inputs, `hosts`' platform set **plus the platform real holds**, so a platform hold arriving while the ready fold is unchanged wakes the wildcard manager; `ServedHosts()` unchanged (ready winners only; **a hold is never served evidence**); `HoldingHosts(now)` new: the exact names (hosts and aliases) whose `resolveExact` is a live `decHolding`.
+- **The local edge:** `e.holdView atomic.Pointer[HoldSnapshot]` is set by `SetHoldView(s HoldSnapshot)` (the view's `onChange`), which then calls `RefreshHolds()`. `type holdSnapshot struct { cands map[string][]candidate }` in `e.holds atomic.Pointer[holdSnapshot]` is written **only** by `func (e *Edge) RefreshHolds()`: under `e.holdsMu` it reads the **latest** `e.holdView.Load()` and the registry's current rows and aliases, builds the local holding candidates (none when the view is nil or `Failed`), installs them, and after the lock sends the wildcard manager the same parent-set notification `Reload` sends after installing a new table. Because the inputs are read inside the same lock that installs, rebuilds install in the order they read, so no rebuild can install older inputs than one before it; there is no revision comparison. `RefreshHolds()` is called by `SetHoldView` (a view change) **and at the end of `Reload`** (a registry change: a row turning held, an alias detach, a provenance or DNS eligibility change), so a hold that the registry gains while the view is unchanged is installed on the next `Reload`. `Reload` writes `e.holds` only through that call. `func (e *Edge) resolve(host string, now time.Time) decision`: the exact candidates = `e.current()`'s entry for `host` (as a local ready candidate), `e.holds.Load().cands[host]`, and the home map's exact candidates; through `rank`; `decNone` and no live exact hold → the same over `*.<parent>` (the local wildcard entry and the remote wildcard candidates). **The handler uses `dec.up` and `dec.table`:** path-prefix selection runs over `dec.up.paths` and resolves the target hostname in `dec.table` (`selectUpstream(dec, r.URL.Path) *upstream`, preserving today's cleaned/raw segment matching, exact target lookup and one-hop selection); the login re-resolution looks its path up in the same `dec.table`; the door's `doorOwnsRoute` checks the **finally selected** upstream's route; a remote decision's owner is `dec.account/app`; `backhaulTo` is called with `dec.eip, dec.box`: one decision and one table for the whole request.
- **Request paths:** HTTPS `ServeHTTP`: after `acceptHop`: `decLocal` → the local path from `selectUpstream`; `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 and are non-empty, else `unknownHost`; else the page; `decNone` → an accepted hop → `staleHopRefusal`; else `unknownHost`. HTTP fallback: `decLocal`/`decRemote`/`decHolding` → the HTTPS redirect; `decNone` → today.
- **TLS (F6):** `Allowed(host)`: `*.` → false; the tunnel hostname (`e.tunnels != nil && host == e.tunnels.Host()`) → true, as today; else a local exact `candReady` or a local exact `candHolding` exists for `host`. `deploymentHost = Allowed || KnownHost`. `currentHosts()` = the platform/provisioned non-alias names among the local table **and** the local real holds (so wildcard parents persist through a hold). `func (e *Edge) certNames() []string` = the local hold names (hosts and aliases) plus `homes.HoldingHosts(now)`; `warmSet` considers `certNames()` after `ServedHosts()` under the same exclusions (apps domain, wildcard-covered, `*.`); the issuer election is unchanged. `ServedHosts` is not widened.
- The page: `func renderHoldingPage(displayName string, untilUnix int64, now time.Time) []byte` and `func (e *Edge) holdingAnswer(w, r, h *holdingView)` with `untilUnix = h.OfflineAt + int64(h.EstimateSeconds)` when both are non-zero, else 0; the sentences, N, the headers and the client's branches exactly as the Global Constraints state.
-- [ ] **Step 1: Write the failing tests**: `TestHoldingViewDecodesTheWriterFixture` (`internal/edgesfile/testdata/offline-moves.json`); `TestRankSuppressionAndFold` (the hold suppresses only its target's lower unstamped pairs; the F1 four-candidate set (A-ready `(1,0)`, A-hold `(1,1)`, B-ready `(1,0)`, B-hold `(1,1)`) → the lower-`order` hold; the F2 three-publication set (A-old `(1,0)` at 0, B-ready `(1,0)` at 1, A-new `(1,2)` at 2, no holds) → A-new, as `foldHomes`; `TestHomeMapAStampedAliasDoesNotBeatAnUnstampedRoute` still passing; a stamped alias against a real hold → the stamped alias (§6.4)); `TestResolveExactVersusRequest` (an expired exact hold → the wildcard; a live exact hold blocks it; `KnownHost` false for an invented wildcard label; `Allowed` rejects `*.` and admits the tunnel host); `TestDecisionCarriesTheTable` (a prefix target and the login re-resolution come from `dec.table`; a reload between resolution and dispatch that changes the prefix target's owner and backend does not change what this request serves; the door check runs against the finally selected route); `TestLocalCandidatesAreTheUpstreamTable` (a non-pointing local alias serves; sidecars and backend-less rows absent); `TestLocalHoldsIncludeExplicitAliases` (the own publication excluded from the home map; the source's explicit pointing alias → the page; its non-pointing alias → serves as today; a derived alias → not held); `TestRemoteCandidatesKeepTheAdmissions` (own box excluded; `Box == ""` skipped; unknown role skipped; a file without an address yields cert-only wildcards and no holds; same-region private dialling); `TestLocalAndRemoteArbitrateTogether` (Review Focus 4); `TestEveryRequestPathHonoursTheDecision` (the table above including hops and doors with and without ownership and with an empty-owner hold); `TestLocalHoldsFollowTheViewSnapshot` (a failed snapshot after an installed hold drops it; an older snapshot is discarded; a `Reload` that overlaps a `RefreshHolds` never reinstalls a hold: Reload at rev 7 captured, RefreshHolds(8 failed), Reload finishes → no hold; success → failure → recovery without registry changes; DNS eligibility change); `TestHoldingPageRendersEveryCase` (name, no name, no estimate, past `until`, N = ceil with minimum 1 at 61 s, 60 s, 59 s, 1 s, escaping of `<b>`, HEAD; the script's `until`, both cadences, the local sentence switch at `until`, the storage key and the try/catch 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; the tunnel host still admitted; a holding-only custom domain is in `certNames` and not in `ServedHosts`; an issuer warms it, then a non-issuer's handshake for it succeeds from the shared cache).
+- [ ] **Step 1: Write the failing tests**: `TestHoldingViewDecodesTheWriterFixture` (`internal/edgesfile/testdata/offline-moves.json`); `TestRankSuppressionAndFold` (the hold suppresses only its target's lower unstamped pairs; a retained unstamped `(1,2)` against a stamped `(0,0)` with no hold → the stamped one, as the fold; the three-hold sorted-fold witness → A-new; an old hold `(1,1)`, a ready `(1,2)` and a current hold `(2,1)` of one target → the ready is suppressed and the `(2,1)` hold answers; the F1 four-candidate set (A-ready `(1,0)`, A-hold `(1,1)`, B-ready `(1,0)`, B-hold `(1,1)`) → the lower-`order` hold; the F2 three-publication set (A-old `(1,0)` at 0, B-ready `(1,0)` at 1, A-new `(1,2)` at 2, no holds) → A-new, as `foldHomes`; `TestHomeMapAStampedAliasDoesNotBeatAnUnstampedRoute` still passing; a stamped alias against a real hold → the stamped alias (§6.4)); `TestResolveExactVersusRequest` (an expired exact hold → the wildcard; a live exact hold blocks it; `KnownHost` false for an invented wildcard label; `Allowed` rejects `*.` and admits the tunnel host); `TestDecisionCarriesTheTable` (a prefix target and the login re-resolution come from `dec.table`; a reload between resolution and dispatch that changes the prefix target's owner and backend does not change what this request serves; the door check runs against the finally selected route); `TestLocalCandidatesAreTheUpstreamTable` (a non-pointing local alias serves; sidecars and backend-less rows absent); `TestLocalHoldsIncludeExplicitAliases` (the own publication excluded from the home map; the source's explicit pointing alias → the page; its non-pointing alias → serves as today; a derived alias → not held); `TestRemoteCandidatesKeepTheAdmissions` (own box excluded; `Box == ""` skipped; unknown role skipped; a file without an address yields cert-only wildcards and no holds; same-region private dialling); `TestLocalAndRemoteArbitrateTogether` (Review Focus 4); `TestEveryRequestPathHonoursTheDecision` (the table above including hops and doors with and without ownership and with an empty-owner hold; **the kept exceptions**: a local `*.example` upstream and a remote exact `x.example` publication: an accepted hop → served from the local wildcard, a door request → served from the local wildcard, an ordinary request with no mesh key whose backhaul fails → the local wildcard, exactly as today's `ServeHTTP`; the same three with a hold for `x.example` → hop: the page; door: the page when owned; ordinary: the page); `TestLocalHoldsFollowTheViewAndTheRegistry` (a failed snapshot after an installed hold drops it; a view published while the row is still ready installs no hold, then the row turns held and `Reload` installs it with the view unchanged; an alias detach, a provenance change and a DNS eligibility change each rebuild on `Reload`; a slow rebuild that read view A overlapped by one that read view B installs in read order, so the installed holds are B's; success → failure → recovery without registry changes; two concurrent publishers never share a revision); `TestHoldingPageRendersEveryCase` (name, no name, no estimate, past `until`, N = ceil with minimum 1 at 61 s, 60 s, 59 s, 1 s, escaping of `<b>`, HEAD; the script's `until`, both cadences, the local sentence switch at `until`, the storage key and the try/catch asserted by text); `TestTLSCoverageForHoldingHosts` (real handshakes; a wildcard-only platform host held keeps its parent; a platform hold arriving on the home map after the wildcard manager started, with the ready fold unchanged, wakes it and the parent's certificate serves; an invented label gets no issuance and no challenge read; the tunnel host still admitted; a holding-only custom domain is in `certNames` and not in `ServedHosts`; an issuer warms it, then a non-issuer's handshake for it succeeds from the shared cache).
- [ ] **Step 2: Run to verify they fail; Step 3: implement; Step 4: run** `go test ./internal/edge -run 'Rank|Holding|Resolve|Decision|LocalCandidates|LocalHolds|RemoteCandidates|LocalAndRemote|RequestPath|TLSCoverage|HomeMap' -count=1`.
- [ ] **Step 5: Commit** `edge: holds suppress their target's stale publications and the fold decides the rest; one decision and one table per request; local holds in their own snapshot`.
@@ -89,7 +89,7 @@ Builds on plan 1 (rev 10: `(incarnation, serving_seq)` on rows and publications,
**Files:**
- Test: `internal/edge/alias_test.go`, `internal/edge/rank_property_test.go`
-- [ ] **Step 1: Write the tests**: `TestAliasOfTheMovingInstanceLosesToItsHold` (Review Focus 3); `TestAliasRepointedDuringAMove` (B wins in all six orders, fresh and retained, with unequal unrelated pairs on A; detached, the alias falls to a derived entry of another target); `TestTwoHoldsForOneAlias` (the lower `order`; local first); `TestHoldingAliasNeverBeatsAReadyRouteOfAnotherTarget`; `TestStampedCandidateIsNeverSuppressed`; `TestRankEqualsFoldWithoutHolds` (every candidate set of the small model with no live hold: `rank` == the local-first-then-`foldHomes` answer, including the empty-target exception); `TestRankingProperties` (every candidate set of the small model: invariants (a) to (j)).
+- [ ] **Step 1: Write the tests**: `TestAliasOfTheMovingInstanceLosesToItsHold` (Review Focus 3); `TestAliasRepointedDuringAMove` (B wins in all six orders, fresh and retained, with unequal unrelated pairs on A; detached, the alias falls to a derived entry of another target); `TestTwoHoldsForOneAlias` (the lower `order`; local first); `TestHoldingAliasNeverBeatsAReadyRouteOfAnotherTarget`; `TestStampedCandidateIsNeverSuppressed`; `TestRankEqualsFoldWithoutHolds` (every candidate set of the small model with no live hold: `rank` == the local-first-then-`foldHomes` answer, including the empty-target exception and the stamped-versus-retained case); `TestRankingProperties` (every candidate set of the small model: the scoped invariants (a) to (n)); `TestHoldingSortedFold` (every holding-only set of the model: the winner is the sorted fold's, the same in every input order).
- [ ] **Step 2: Run; fix `rank` if anything fails; Step 3: Commit** `edge: the ranking pinned by witnesses, a differential test against the fold and a small-model property test`.
### Task 4: The decoder inventory and its discovery check
@@ -140,5 +140,5 @@ The reviewed inventory, each with one assertion over the fixture: `edgesfile.Par
- Spec coverage: §6.4 (T2 ranking, T3 witnesses, T5 expiry), §7.4 (T2 page, T6 probe), §11 (T7 reconstruction witness).
- The ranking's ready-only path is the fold itself, so plan 1 and plan 3 cannot disagree (T3's differential test).
- 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`, `offlineHolds`, `offlineView.snapshot/onChange`, `holdingView`, `candidate`/`candKind`/`order`/`table`, `decision`/`decKind`, `buildCandidates`, `rank`, `resolveExact`, `resolveRequest`, `HoldingHosts`, `Edge.resolve`, `selectUpstream`, `holdSnapshot`, `RefreshHolds(*offlineHolds)`, `certNames`, `holdingValidity`, `renderHoldingPage`, `holdingAnswer`, `SetHoldingSource`, `WithBoxID` (package-level `RunOption`), `servedByWriter`, `probeTarget` (plan 2).
+- Names used across tasks: `RouteAlias.Explicit`, `edgesfile.AliasPublishable`, `holdingEntry`, `holdingSource`, `offlineHolds`, `offlineView.snapshot/onChange/pubMu`, `edge.HoldSnapshot`/`HoldState`, `SetHoldView`, `RefreshHolds()`, `holdingView`, `candidate`/`candKind`/`order`/`table`, `decision`/`decKind`, `buildCandidates`, `rank`, `resolveExact`, `resolveRequest`, `HoldingHosts`, `Edge.resolve`, `selectUpstream`, `holdSnapshot`, `certNames`, `holdingValidity`, `renderHoldingPage`, `holdingAnswer`, `SetHoldingSource`, `WithBoxID` (package-level `RunOption`), `servedByWriter`, `probeTarget` (plan 2).
- Review Focus 1 to 5 are pinned by T5, T3, T3, T2, T6.