# Nearest Region by Default: Implementation Plan

> **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.

**Revision:** 7. A one-page summary for review (what ships per release,
the task list, the copy-review gate, the rollout and the risks) is in
`2026-10-02-nearest-region-default-SUMMARY.md`, beside this file.

- Rev 1 (9513d52c) was reviewed by Astra
  (`CODEX-PLAN-NEAREST-REGION-R1.md`, beside this file): not ready, with
  8 P1 and 10 P2. Every finding was checked against the code and accepted.
- Rev 2 (fcf15ae9) folded in spec rev 6's ruling R9: placed instances need
  the deployment's secrets key.
- Rev 3 answers Astra's round 2 (`CODEX-PLAN-NEAREST-REGION-R2.md`).
- Rev 4 answers Astra's round 3 (`CODEX-PLAN-NEAREST-REGION-R3.md`) and
  carries spec rev 6.1's R9a note (`region_known`, `placement_known`).
- Rev 5 answers Astra's round 4 (`CODEX-PLAN-NEAREST-REGION-R4.md`) and
  carries spec rev 6.2 (`instance_domain`, approved by Paul 2026-10-03).
- Rev 6 answers Astra's round 5 (`CODEX-PLAN-NEAREST-REGION-R5.md`), and
  marks the flagship's secrets-key staging done (Rollout step 2).
- Rev 7 answers Astra's round 6 (`CODEX-PLAN-NEAREST-REGION-R6.md`).
- The code blocks the findings touched were **proved**: extracted verbatim
  into a throwaway worktree at the branch base, built and vetted, and each
  prescribed test was run before and after its implementation step.
- The six dispositions tables, with the proof results, are at the end.

**Goal:** Give every new app a region at creation, preselected from a timezone-then-IP guess, and have new instances default to that region. Never place an instance automatically on a box that cannot open the deployment's secrets. Existing apps, older CLIs and DNS stay unchanged.

**Architecture:**

- **Boxes** publish four more facts in their edges file: `serves_shared_pool`,
  `accepts_inbound`, and `secrets_key_fp` (the fingerprint of their secrets
  key's public half). Together these let the console tell which regions can
  really run an ordinary instance.
- **A new package, `internal/geo`,** turns timezones and IPs into points and
  picks the great-circle nearest eligible region.
- **The console:**
  - stores `region` and `region_source` on the `apps` row, resolved inside
    `claimApp` after it knows whether the claim is an adoption;
  - fills an empty instance region from the app's region before the
    existing checks;
  - refuses explicit creates into a region provably without the secrets key
    (R9);
  - adds `app_default` to `GET …/regions`, and a POST guess endpoint.
- **The CLI** sends a timezone hint and gains `--region` where the spec says.

All non-home writes sit behind `CONSOLE_APP_REGIONS=on` together with a named
home.

**Tech Stack:**

- Go 1.26 and SQLite (modernc).
- An embedded IANA tzdata `zone.tab` and its links.
- `github.com/oschwald/maxminddb-golang` v1.13.1 for the optional
  `CONSOLE_GEOIP_DB`.
- `crypto/ecdh` P-256, as already used by `internal/appconfig`.
- `net/http` and `html/template`.

**Spec:** `docs/superpowers/specs/2026-10-01-nearest-region-default-design.md`
(rev 6.2, approved by Paul 2026-10-02, including the R9 amendment, the R9a
note and the rev 6.2 `instance_domain` note approved 2026-10-03). Read it
beside this plan. Section numbers below (§N) are the spec's.

## Global Constraints

- **Scope is placement only (R8).** No DNS writes, no `Steering` stamp, no
  converger change, no `CARLOS_STEERING_ZONE_ID`.
- **Nothing writes a non-home app region** unless `CONSOLE_APP_REGIONS=on`
  **and** `CONSOLE_HOME_REGION` is set (§5.6). With the switch off, the door's
  fill still runs for rows that already hold a region.
- **`region` is stored in the record vocabulary, where `""` is the home**
  (§5.1). `region_source` ∈ {`chosen`, `timezone`, `ip`, `default`, `""`}.
- **Never stored, logged, audited, put in a URL, or echoed** in a response or
  error: a timezone, a client IP, a coordinate, or any secrets key
  (§6.5, R9). That includes errors returned by dependencies, which are
  logged as fixed categories, never verbatim, wherever they could carry an
  address.
- **Every non-null guess passes**
  `instanceRegionAllowed(m.Regions, homeRegionIn(code))` (§6.4).
- **Automatic choices need positive eligibility on one box**: reachable;
  `hibernation_configured`, `serves_shared_pool` and `accepts_inbound` all
  `true`; and `secrets_key_fp` equal to the fingerprint of the console's
  `CARLOS_SECRETS_PUBKEY` (§6.1, R9). The home is exempt.
- **Explicit requests keep today's gates**, plus one refusal (R9, C31): a
  socket-backed or replica create into a region where every reachable,
  hosting box is a current writer and none holds the deployment's key.
- **App-derived defaults never pass on the dormant gate.** An edges-read
  failure is a 503 with C28 (§7).
- **Pooled manifests and fleet-placed apps get no instance guess** (§6.1).
- **Wire changes are additive** (AGENTS.md).
- **Copy.**
  - Every user-facing or operator-facing string is reviewed **before** it is
    written into any product file. Task 0 runs the copy-review skill on the
    complete inventory and records the approved text in
    `docs/superpowers/plans/2026-10-02-nearest-region-copy.json`.
  - Later tasks copy strings from that record verbatim.
  - Task 25 proves every recorded string is present, byte for byte, in the
    file the record names.
  - No em dashes in any of it.
- **Build and test gate**, clean before every commit that touches Go:
  `go build ./...`, `go vet ./...`, `gofmt -l .` (empty), and
  `go test ./... -count=1`. `make test` runs all of it.
- **Cross-compile gate.** The five CI targets, exactly as
  `.amadan/ci.d/20-cross@5` runs them:
  `CGO_ENABLED=0 GOOS=<os> GOARCH=<arch> go build ./...` for darwin/arm64,
  darwin/amd64, linux/arm64, windows/amd64 and windows/arm64. Every loop
  below records each failure and ends with `[ "$fail" = 0 ] || exit 1`. Run
  each such block as one script (`bash -c` or a tool call), so that a failed
  target stops it before the commit lines that follow (codex plan-R2 10).
- **Test environment.**
  - Use the default Go build cache (`~/.cache/go-build`, writable inside the
    sandbox). Do not set `GOCACHE`, `GOMODCACHE` or `GOTMPDIR`.
  - Scratch goes under `$TMPDIR`, which is on disk.
  - **Operator rule for this machine (socket tests).** The operator
    instructs that ONLY the Unix-socket test packages run with `TMPDIR`
    under `/tmp/g`: `internal/host`, `internal/activator`,
    `internal/edge`, and any other package whose tests bind a Unix socket
    (today also `internal/agentsock`, `internal/instancectl`,
    `internal/instanceexec`, `internal/unixdial` and `internal/vet`). A
    Unix socket path is capped at 108 bytes, and the on-disk `TMPDIR` here
    (`/home/paulca/.cache/tmp/claude-1001/...`) is too deep for Go's test
    directories plus the socket names under them. The directory is small
    and is deleted after the run. `/tmp/g` is shared, so each run makes its
    own directory under it and deletes only that; nothing else in `/tmp/g`
    is touched. Everything else uses the on-disk `TMPDIR`. CI is
    unaffected: it has no such path limit and sets nothing here. Every
    socket-test command in this plan goes through this function:

    ```bash
    socktest() { # go test args...; TMPDIR is a fresh short dir, deleted after
      mkdir -p /tmp/g || return 1
      local d rc
      d="$(mktemp -d /tmp/g/nr.XXXXXX)" || return 1
      TMPDIR="$d" go test "$@"; rc=$?
      rm -rf "$d"
      return "$rc"
    }
    ```
  - Console tests need an on-disk `TMPDIR` (the sandbox's `$TMPDIR` is one),
    because an empty `/tmp/.git` breaks Go's VCS stamping.
- **CLI tests that shell out to the carlos binary** build their env through
  `carlosTestEnv` and set `cmd.Dir` to a scratch dir (AGENTS.md).
- **Flags are documented.** `docs/site/cli.md` documents every new flag. No
  new commands.
- **Commits.** End every commit message with
  `Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>`. No push.
  `git config` only with `--worktree`.

## Review Focus

The five inputs most likely to bite a person that no spec-derived test covers.
Each is pinned by a test in the task named.

1. **Real-machine timezone text.** An `/etc/timezone` with a trailing newline,
   `TZ=":"`, `TZ="localtime"`, `TZ="posixrules"` and a POSIX rule string must
   give a clean name or nothing. Pinned in Task 19 and Task 3.
2. **IPv6 and IPv4-mapped clients.**
   - `::ffff:81.2.69.142` locates as `81.2.69.142`.
   - A public IPv6 address looked up in an IPv4-only database is a miss whose
     log line never contains the address.
   - Private and loopback addresses never reach the reader.

   Pinned in Task 5.
3. **A region that stops being eligible between rendering the new-app page and
   submitting it** (for example, its box loses the secrets key). The form
   re-renders with C5 and no app row is written. Pinned in Task 13.
4. **Garbage hints at the guess endpoint.** `"ip":"not-an-ip"` and a 10 KB
   `"tz"` are treated as absent: a 200, never a 400 or 500. Pinned in
   Task 15.
5. **A claim that fails after the region was resolved.** A lost insert race
   writes no row and logs no `app region` line. A held apex refuses before
   resolution and logs nothing either. Pinned in Task 12.

---

## File map

| File | Responsibility | Tasks |
| --- | --- | --- |
| `docs/superpowers/plans/2026-10-02-nearest-region-copy.json` | the approved copy record | 0 |
| `internal/provision/reconcile.go` | `ReconcileConfig.SharedInstanceEligibility()` | 1 |
| `internal/host/edgesfile.go` | publish `serves_shared_pool`, `accepts_inbound`, `secrets_key_fp` | 1, 1b |
| `internal/appconfig/fingerprint.go` | `KeyFingerprint`, the one renderer of `secrets_key_fp` | 1b |
| `internal/geo/*` | points, nearest, tz data, AWS region table, GeoIP | 2 to 5 |
| `scripts/update-tzgeo.sh` | refresh the embedded tz files | 3 |
| `internal/console/regioncopy.go`, `regionmsgs.go` | console copy and API messages | 6, 7 |
| `internal/console/store.go` | columns, rebuild list, accessors | 8 |
| `internal/console/server.go` (Config), `regionconfig.go`, `cmd/console/main.go` | switch, GeoIP, expected secrets fingerprint, test seams | 9 |
| `internal/console/regioncandidates.go` | strict edges read, candidates, eligibility incl. R9, policy | 10 |
| `internal/console/regionguess.go` | `guessRegion` | 11 |
| `internal/console/regionclaim.go`, `server.go`, `apicli.go` | `regionReq`, explicit validation, claim resolution, API create | 12 |
| `internal/console/regionpicker.go`, `templates/app_new.html` | the new-app picker and its POST and guess route | 13 |
| `internal/console/apiinstances.go` | the fill, app-derived eligibility, the R9 explicit refusal, response fields | 14 |
| `internal/console/regiondefault.go`, `apiregions.go`, `authcli.go` | `app_default`, names, `POST …/regions/guess` | 15 |
| `templates/app_instances.html`, `server.go` | Instances tab lines | 16 |
| `internal/console/regiondrift.go` | roll-forward discrepancy log | 17 |
| `region_copy.go`, `timezone.go`, `apps_region.go` (root) | CLI copy, timezone, region line | 18, 19, 20 |
| `apimode.go`, `apps.go`, `deploy.go`, `canary.go`, `instances_cli.go` | CLI wire and flags | 20 to 22 |
| `deploy_region_e2e_test.go`, `canary_region_e2e_test.go` (root) | command-level proofs | 21, 22 |
| `scripts/region-service-gate.sh`, `internal/edge/servicegate_test.go` | the service gate and its real-backhaul proof | 23 |
| `docs/site/cli.md`, `README.md` | docs, release checklist, floor | 24 |
| `approved_copy_test.go` (root) | proves the record is applied | 25 |

Order: Task 0 first, because no product file may hold an unreviewed string.
Then 1, 1b, 2 to 25 in order.

---

### Task 0: Copy inventory, review and the approved record (before any product file)

**Files:**
- Create: `docs/superpowers/plans/2026-10-02-nearest-region-copy.json`

**Interfaces:**
- Produces:
  - the approved copy record, read by every later task and asserted by
    Task 25;
  - its shape:

```json
{
  "approved_by": "Paul",
  "approved_on": "YYYY-MM-DD",
  "entries": [
    {"id": "C1", "file": "internal/console/regioncopy.go", "text": "Region"}
  ]
}
```

  `text` is the exact byte sequence the named file must contain. Go format
  verbs (`%s`, `%v`, `%q`, `%d`) and shell variables (`$label`) stay as
  written, because the file contains them.

- [ ] **Step 0: Resolve the fixture commit (network, read-only)**

F1 names the MaxMind-DB commit Task 5 fetches its fixtures from, so the
commit is fixed BEFORE review and the reviewed text is the text Task 5
writes (codex plan-R3 3). Nothing substitutes into an approved string later.

```bash
sha=$(git ls-remote https://github.com/maxmind/MaxMind-DB refs/heads/main | cut -f1)
[ ${#sha} = 40 ] && [ -z "${sha//[0-9a-f]/}" ] || { echo "could not resolve MaxMind-DB main"; exit 1; }
echo "$sha"
```

F1's draft, with that sha written in place of `<sha>` before the review:

```
The .mmdb files and GeoIP2-Country-Test.json are MaxMind's published test
data from github.com/maxmind/MaxMind-DB, dual licensed MIT or Apache-2.0
(LICENSE-MIT, LICENSE-APACHE beside this file). All were fetched at commit
<sha>. They contain test records only, not real geolocation data.
```

- [ ] **Step 1: Assemble the complete inventory**

Every string this plan writes into a product file, with its draft. The
drafts are spec §19's where an id is a C-number. The rest are added by this
plan and marked "plan".

| id | file | draft |
| --- | --- | --- |
| C1 | `internal/console/regioncopy.go` | `Region` |
| C2 | same | `Where this app's instances run. We picked the region nearest to you. Choose another if most of your users are somewhere else.` |
| C3 | same | `%s (nearest to you)` |
| C5 | same | `That region can't take new apps right now. Choose another.` |
| C7 | same | `New instances start in %s, this app's region.` |
| C8 | same | `New instances can't start in %s, this app's region, right now. Pass a region to place one elsewhere.` |
| C21 | same | `created app %s in %s` |
| C24 | same | `IP location by DB-IP.` (confirm DB-IP's required wording at <https://db-ip.com/db/lite.php>) |
| R-absent | `internal/console/regionmsgs.go` | `no box runs there now` |
| R-not-hosting | same | `boxes there can't start instances` |
| R-no-secrets-key | same | `boxes there can't open this deployment's secrets` |
| R-not-eligible | same | `boxes there don't take ordinary instances` |
| C16 | same | `this app's region %s can't take new instances right now (%s). Pass a region to place this instance elsewhere. Regions that can: %s.` |
| C17 | same | `this app's region %s is not one its instances may use. Allowed: %s. Pass a region to choose one.` |
| C18 | same | `region %s is not one this platform runs in. Choose from: %s.` |
| C19 | same | `region %s can't take new apps right now. Choose from: %s.` |
| C16b (plan) | same | `this app's region %s can't take new instances right now (%s), and no other region this app may use can either. Try again later.` (C16 when no region would pass) |
| C17b (plan) | same | `this app's region %s is not one its instances may use, and no region they may use can take new instances right now. Try again later.` (C17 when no allowed region would pass) |
| C18b (plan) | same | `region %s is not one this platform runs in. Run the command without --region.` (C18 when no region would pass) |
| C19b (plan) | same | `region %s can't take new apps right now. Run the command without --region.` (C19 when no region would pass) |
| C20 | same | `could not read this app's region. Try again.` |
| C26 | same | `this app's region %s is also one of this instance's replica placements. Pass a region for the instance, or drop %s from replicas.` |
| C27 | same | `could not read this app's instance policy, so there is no region suggestion. Try again.` |
| C28 | same | `could not confirm that this app's region %s can take new instances. Try again, or pass a region.` |
| C29 | same | `possible placement discrepancy: %s of %s has no region, but %s's region is %s. It may be a deliberate home override; review it.` |
| C31 | same | `region %s can't run instances right now: its boxes don't hold this deployment's secrets key, so an instance there would start without its config or secrets. Choose another region.` |
| O1 (plan) | `internal/console/regionconfig.go` | `console: CONSOLE_APP_REGIONS=on but CONSOLE_HOME_REGION is unset, so app regions stay off until the home is named` |
| O2 (plan) | same | `console: CARLOS_SECRETS_PUBKEY is unset, so no region outside the home can be chosen automatically` |
| O3 (plan) | `cmd/console/main.go` | `console: CONSOLE_GEOIP_DB %s is unusable, so the IP step of region guesses is off: %v` |
| O4 (plan) | same | `console: GeoIP database loaded from %s` |
| O5 (plan) | `internal/geo/mmdb.go` | `geo: a GeoIP lookup failed and was treated as a miss` |
| O6 (plan) | `internal/console/regioncandidates.go` | `console: boxes for region %q disagree on their AWS region %v; using %s` |
| O7 (plan) | same | `console: region %q has no point in the shipped AWS region table (AWS region %q), so region guesses leave it out` |
| O8 (plan) | `internal/console/server.go` | `console: app region %s/%s: %s (%s)` |
| O9 (plan) | `internal/console/regiondrift.go` | `console: placement discrepancy check %s/%s: only the first %d records were checked` |
| O10 (plan) | `internal/host/edgesfile.go` | `agent: CARLOS_SECRETS_KEY does not parse, so no secrets_key_fp is published: %v` |
| O11 (plan) | `internal/console/apiinstances.go` | `console: api instances create %s/%s: read app region: %v` |
| O12 (plan) | same | `console: api instances create %s/%s: read edges: %v` |
| O13 (plan) | `internal/console/regioncandidates.go` | `console: region policy %s/%s: read manifest: %v` |
| O14 (plan) | same | `console: region policy %s/%s: open registry: %v` |
| O15 (plan) | same | `console: region policy %s/%s: placement: %v` |
| O16 (plan) | `internal/console/apicli.go` | `console: claim %s/%s: read back region: %v` |
| O17 (plan) | `apimode.go` | `regions: unreadable response: %w` |
| O18 (plan) | `internal/console/regiondrift.go` | `console: placement discrepancy check %s/%s: %v` |
| O19 (plan) | same | `console: placement discrepancy check: %v` |
| O20 (plan) | `apimode.go` | `instance: unreadable response: %w` |
| C9 | `region_copy.go` | `Region: %s (from your timezone). Use --region to choose another.` |
| C10 | same | `Region: %s (from your network location). Use --region to choose another.` |
| C11 | same | `Region: %s (the platform default). Use --region to choose another.` |
| C12 | same | `Region: %s.` |
| C13a | same | `This console does not record an app region yet, so --region was not applied.` |
| C13b | same | `This console is not assigning app regions yet, so --region was not applied. This app's instances start in %s.` |
| C14 | same | `channel %s, %s` |
| C15 | same | `%s already runs in %s. --region only applies when deploy creates an app. To place one instance elsewhere, use carlos instances create --region.` |
| C15b (plan) | same | `could not read %s's region (%v), so deploy stopped before shipping anything. Try again, or run without --region.` |
| C15c (plan) | same | `the console could not read %s's region, so deploy stopped before shipping anything. Try again, or run without --region.` |
| C25 | same | `canary %s already runs in %s. --region applies only when canary creates the host. Run carlos canary remove first to start it somewhere else.` **Note for Paul:** the verb is `carlos canary rm` (or `delete`); there is no `remove`. |
| C30 | same | `%s runs on your fleet %s, which decides where every instance runs, so --region does not apply. Run the command without --region.` |
| C30b (plan) | same | `this console can't say where %s's instances run, so --region can't be checked. Update the console, or run the command without --region.` |
| C30c (plan) | same | `could not confirm where canary %s runs, so nothing was changed. Try again, or run the command without --region.` |
| C22a | same | `region this app's instances start in (default: the region nearest you)` |
| C22b | same | `region for the app this deploy creates (default: the region nearest you)` |
| C22c | same | `region for the canary instance (default: the app's region)` |
| C22d | same | `region for this instance (default: the app's region, subject to the manifest's allow-list)` |
| U1 (plan) | `apps.go` | ` [--region <code>]` in the `apps create` usage string |
| U2 (plan) | `deploy.go` | ` [--region <code>]` in the `deploy` usage string |
| U3 (plan) | `canary.go` | `[--region <code>]` in `canaryUsage` |
| S1 (plan) | `scripts/update-tzgeo.sh` | `usage: update-tzgeo.sh <tzdata version, e.g. 2026b>` |
| S2 (plan) | same | `gpg not found: signature NOT checked` |
| S3 (plan) | same | `internal/geo/data refreshed to tzdata $v` |
| G1 (plan) | `scripts/region-service-gate.sh` | `usage: region-service-gate.sh <probe-host> <expected-version> [<edge-eip> ...]` |
| G2 (plan) | same | `ok   $label: $status $version` |
| G3 (plan) | same | `FAIL $label: request failed` |
| G4 (plan) | same | `FAIL $label: status $status` |
| G5 (plan) | same | `FAIL $label: X-Carlos-Version '$version', want '$want'` |
| G6 (plan) | same | `FAIL $label: connected to $ip, not $eip` |
| C23 | `internal/geo/awsregions.go` | the 34 place names of spec §19, one entry each (`C23-us-east-1` and so on) |
| D1 to D4 | `docs/site/cli.md` | the four paragraphs in Task 24 Step 1 |
| D5 to D10 | `README.md` | the six passages in Task 24 Step 2 |
| F1 (plan) | `internal/geo/testdata/README` | the fixture provenance note below, with `<sha>` already replaced by the commit Step 0 resolved |

- [ ] **Step 2: Run the copy review**

Invoke the `copy-review` skill with the whole inventory: every row, with
where it appears and the C-number's spec context. Do not write any of these
strings into a product file before the review returns. Wait for Paul's
edits.

- [ ] **Step 3: Write the approved record**

Write the JSON file with one entry per row. Use Paul's approved text verbatim
(the draft where he left it unchanged), in each string's code form. That
means Go verbs in place of `{…}` placeholders, and the file the row names.
Set `approved_on` to the review date.

- [ ] **Step 4: Commit**

```bash
git add docs/superpowers/plans/2026-10-02-nearest-region-copy.json
git commit -m "plan: approved copy record for nearest-region (copy review)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```

From here, every task that writes a string from the inventory copies it from
the record. The code blocks below show the drafts. Where the record differs,
the record wins, and the task's tests are updated to match.

---

### Task 1: Boxes publish shared-pool and inbound eligibility

**Files:**
- Modify: `internal/provision/reconcile.go` (beside `poolServed`, `:1339`)
- Modify: `internal/host/edgesfile.go:155-206` (struct), `:600-611` (ident in `edgesFileLoop`)
- Test: `internal/provision/reconcile_eligibility_test.go` (create), `internal/host/edgesfile_test.go`

**Interfaces:**
- Consumes: `provision.ReconcileConfig{Pools []string; Inbound string}`,
  `provision.InboundNone`, `poolServed`, and `provisionTickConfig(getenv)
  provision.ReconcileConfig` (`internal/host/agent.go:2030`).
- Produces:
  - `func (cfg ReconcileConfig) SharedInstanceEligibility() (sharedPool, inbound bool)`;
  - `edgesFile.ServesSharedPool bool` (`json:"serves_shared_pool"`, not
    omitempty);
  - `edgesFile.AcceptsInbound bool` (`json:"accepts_inbound"`, not
    omitempty).

- [ ] **Step 1: Write the failing provision test**

```go
// internal/provision/reconcile_eligibility_test.go
package provision

import "testing"

// SharedInstanceEligibility must be the reconciler's own predicates, never a
// second reading of the environment (spec §6.1, codex R3-3).
func TestSharedInstanceEligibilityIsTheMintPredicate(t *testing.T) {
	cases := []struct {
		pools          []string
		inbound        string
		shared, accept bool
	}{
		{nil, "", true, true},
		{[]string{"tito"}, "", false, true},
		{[]string{"", "tito"}, "", true, true},
		{nil, InboundNone, true, false},
		{[]string{"tito"}, InboundNone, false, false},
	}
	for _, c := range cases {
		cfg := ReconcileConfig{Pools: c.pools, Inbound: c.inbound}
		shared, inbound := cfg.SharedInstanceEligibility()
		if shared != poolServed(c.pools, "") || inbound != (c.inbound != InboundNone) {
			t.Fatalf("%+v: eligibility diverges from the mint predicates", c)
		}
		if shared != c.shared || inbound != c.accept {
			t.Fatalf("%+v: got (%v, %v), want (%v, %v)", c, shared, inbound, c.shared, c.accept)
		}
	}
}

// spec §16, codex plan-R3 4: each published answer pairs with the reconcile
// pass's own behaviour on an ORDINARY record (the shared "" pool, routed).
// A box that publishes both true mints it; a box that publishes either
// false refuses it, so the console's eligibility never offers a region
// whose box would refuse the record.
func TestEligibilityPairsWithTheReconcileRefusal(t *testing.T) {
	for _, c := range []struct {
		pools   []string
		inbound string
	}{
		{nil, ""}, {[]string{"tito"}, ""}, {[]string{"", "tito"}, ""},
		{nil, InboundNone}, {[]string{"tito"}, InboundNone},
	} {
		s, sqid := setup(t)
		if err := PutManifest(ctx, s, sqid, "tito", Manifest{Schema: 1, App: "tito", Domain: "go.tito.io"}); err != nil {
			t.Fatal(err)
		}
		rec := acmeRecord(sqid)
		rec.Pool = ""
		if err := CreateRecord(ctx, s, rec); err != nil {
			t.Fatal(err)
		}
		reg := regWith(t)
		cfg := ReconcileConfig{Region: "ie", Pools: c.pools, DataDir: "/data", Inbound: c.inbound}
		if err := Reconcile(ctx, s, reg, cfg); err != nil {
			t.Fatal(err)
		}
		rt, err := reg.Get(rec.Host)
		minted := err == nil && rt != nil
		shared, inbound := cfg.SharedInstanceEligibility()
		if minted != (shared && inbound) {
			t.Errorf("pools %q inbound %q: minted %v, but the box publishes serves_shared_pool=%v accepts_inbound=%v", c.pools, c.inbound, minted, shared, inbound)
		}
	}
}
```

`setup`, `acmeRecord`, `regWith` and `ctx` are `reconcile_test.go`'s (same
package). `setup`'s manifest is pooled, so the test replaces it with a
shared-pool one before creating the ordinary record.

- [ ] **Step 2: Run it to verify it fails**

Run: `go test ./internal/provision -run 'TestSharedInstanceEligibilityIsTheMintPredicate|TestEligibilityPairsWithTheReconcileRefusal' -count=1`
Expected: FAIL, `cfg.SharedInstanceEligibility undefined`.

- [ ] **Step 3: Implement it beside `poolServed`**

```go
// SharedInstanceEligibility answers, from this box's own reconcile config,
// whether it mints an ORDINARY instance record: one in the shared ""
// pool, needing inbound service. The edges file publishes both answers
// (serves_shared_pool, accepts_inbound) so the console never offers an
// automatic choice of a region whose boxes would refuse the record
// (nearest-region spec §6.1). It is the reconciler's own predicates —
// poolServed and the CARLOS_INBOUND=none refusal at reconcile.go:925 — so
// publication and minting cannot disagree.
func (cfg ReconcileConfig) SharedInstanceEligibility() (sharedPool, inbound bool) {
	return poolServed(cfg.Pools, ""), cfg.Inbound != InboundNone
}
```

- [ ] **Step 4: Run it to verify it passes**

Run: `go test ./internal/provision -run 'TestSharedInstanceEligibilityIsTheMintPredicate|TestEligibilityPairsWithTheReconcileRefusal' -count=1`
Expected: PASS.

Then a mutation check on the pairing: make `SharedInstanceEligibility`
return `true, true`, and re-run `TestEligibilityPairsWithTheReconcileRefusal`.
It must FAIL on every config the reconciler refuses. Revert, and re-run:
PASS.

- [ ] **Step 5: Write the failing edges-file tests**

Append to `internal/host/edgesfile_test.go`:

```go
// The two eligibility fields are NOT omitempty: absent means "a writer
// that predates the field" (unknown), false means "no" (spec §6.1).
func TestEdgesFileRawWireBodyEligibilityFields(t *testing.T) {
	for _, want := range []bool{true, false} {
		s, reg := release.NewDirStore(t.TempDir()), edgesTestRegistry(t)
		seedRoute(t, reg, "app.example.com")
		var last []byte
		ident := edgesFile{Box: "quiet-harbour", Region: "eu-west-2", Placement: "uk",
			ServesSharedPool: want, AcceptsInbound: want}
		if err := edgesFilePass(context.Background(), s, reg, ident, "203.0.113.10", &last); err != nil {
			t.Fatal(err)
		}
		got, _, err := s.Get(context.Background(), "control/edges/quiet-harbour.json")
		if err != nil {
			t.Fatal(err)
		}
		var raw map[string]any
		if err := json.Unmarshal(got, &raw); err != nil {
			t.Fatal(err)
		}
		for _, k := range []string{"serves_shared_pool", "accepts_inbound"} {
			v, present := raw[k]
			if !present || v != want {
				t.Fatalf("%s = %v (present %v), want %v always published", k, v, present, want)
			}
		}
	}
}

// What the loop publishes is the reconcile config's own answer, for every
// spelling an operator might write. CARLOS_POOLS=",tito" parses to
// ["tito"] (agent.go's parser drops empty elements), so it does NOT serve
// the shared pool and must publish false (codex R3-3).
func TestEdgesFileEligibilityFollowsTheReconcileConfig(t *testing.T) {
	cases := []struct {
		pools, inbound string
		shared, accept bool
	}{
		{"", "", true, true},
		{"tito", "", false, true},
		{",tito", "", false, true},
		{"", "none", true, false},
		{"tito", "NONE", false, false},
	}
	for _, c := range cases {
		getenv := func(k string) string {
			switch k {
			case "CARLOS_POOLS":
				return c.pools
			case "CARLOS_INBOUND":
				return c.inbound
			}
			return ""
		}
		shared, inbound := edgesFileEligibility(getenv)
		cfg := provisionTickConfig(getenv)
		wantShared, wantInbound := cfg.SharedInstanceEligibility()
		if shared != wantShared || inbound != wantInbound {
			t.Fatalf("%+v: published (%v, %v) but the reconciler says (%v, %v)", c, shared, inbound, wantShared, wantInbound)
		}
		if shared != c.shared || inbound != c.accept {
			t.Fatalf("%+v: got (%v, %v), want (%v, %v)", c, shared, inbound, c.shared, c.accept)
		}
	}
}
```

- [ ] **Step 6: Run them to verify they fail**

Run: `socktest ./internal/host -run 'TestEdgesFile(RawWireBodyEligibilityFields|EligibilityFollowsTheReconcileConfig)' -count=1`
Expected: FAIL, unknown fields `ServesSharedPool` and `AcceptsInbound`, and
`edgesFileEligibility` undefined.

- [ ] **Step 7: Implement the fields, the helper and the loop wiring**

In `edgesFile`, after `HibernationConfigured`:

```go
	// ServesSharedPool and AcceptsInbound are this box's own answers to
	// "would my reconcile pass mint an ORDINARY instance record" — one in
	// the shared "" pool, needing inbound service (nearest-region spec
	// §6.1). Deliberately NOT omitempty, HibernationConfigured's precedent:
	// absent is a writer that predates the field (unknown), false is a box
	// that said no. The console needs both positively true before it makes
	// an AUTOMATIC choice of this box's region; explicit requests ignore
	// them. Computed from the same ReconcileConfig the reconcile pass uses
	// (edgesFileEligibility), never a second reading of the environment.
	ServesSharedPool bool `json:"serves_shared_pool"`
	AcceptsInbound   bool `json:"accepts_inbound"`
```

Below `edgesFileRegion`:

```go
// edgesFileEligibility derives the two published eligibility answers from
// the reconcile config this box actually runs with — provisionTickConfig,
// the same parse the provision tick uses — so a spelling the reconciler
// reads one way can never be published another.
func edgesFileEligibility(getenv func(string) string) (sharedPool, inbound bool) {
	return provisionTickConfig(getenv).SharedInstanceEligibility()
}
```

In `edgesFileLoop`, build the ident with the two new fields:

```go
	sharedPool, inbound := edgesFileEligibility(os.Getenv)
	ident := edgesFile{
		Box:              box,
		Region:           region,
		Placement:        strings.TrimSpace(os.Getenv("CARLOS_REGION")),
		DataBucket:       strings.TrimSpace(os.Getenv("CARLOS_HIBERNATE_BUCKET")),
		DataBucketRegion: strings.TrimSpace(os.Getenv("CARLOS_HIBERNATE_REGION")),
		// Same env, same trim, one line apart, deliberately: this boolean
		// must never disagree with the bucket beside it, and the way to
		// guarantee that is to derive both from the one read rather than to
		// ask two questions that could drift.
		HibernationConfigured: strings.TrimSpace(os.Getenv("CARLOS_HIBERNATE_BUCKET")) != "",
		ServesSharedPool:      sharedPool,
		AcceptsInbound:        inbound,
	}
```

- [ ] **Step 8: Run the host package to verify it passes**

First update the four raw-wire goldens in `internal/host/edgesfile_test.go`,
because the schema really did grow and both keys are always published
(`:315`, `:357`, `:376` and `:929`, in `TestEdgesFileRawWireBodyMatchesSchema`,
`TestEdgesFileRawWireBodyRegionField` and
`TestEdgesFileRawWireBodyPlacementField`):

```bash
sed -i 's/"hibernation_configured":false,"generation":"` +/"hibernation_configured":false,"serves_shared_pool":false,"accepts_inbound":false,"generation":"` +/' internal/host/edgesfile_test.go
test "$(grep -c '"serves_shared_pool":false,"accepts_inbound":false' internal/host/edgesfile_test.go)" = 4
```

Run: `socktest ./internal/host -count=1 && go test ./internal/provision -count=1`
Expected: PASS. Without the golden update, those three tests fail on exactly
the two new keys (seen in the round-3 proof).

- [ ] **Step 9: Cross-compile, then commit**

Run:

```bash
fail=0
for t in darwin/arm64 darwin/amd64 linux/arm64 windows/amd64 windows/arm64; do
  CGO_ENABLED=0 GOOS=${t%/*} GOARCH=${t#*/} go build ./... || { echo "FAILED $t"; fail=1; }
done
[ "$fail" = 0 ] || exit 1  # stops this block before any commit below it
```

Expected: no `FAILED` line. `internal/host` is linked into the CLI.

```bash
git add internal/provision/reconcile.go internal/provision/reconcile_eligibility_test.go internal/host/edgesfile.go internal/host/edgesfile_test.go
git commit -m "edges file: publish serves_shared_pool and accepts_inbound (nearest-region §6.1)

Computed from the reconcile config the box mints with, so an automatic
region choice can never land on a box that refuses ordinary instances.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```

---

### Task 1b: Boxes publish their secrets-key fingerprint (R9)

**Files:**
- Create: `internal/appconfig/fingerprint.go`, `internal/appconfig/fingerprint_test.go`
- Modify: `internal/host/edgesfile.go` (the struct after `AcceptsInbound`, `edgesFileLoop`'s ident)
- Test: `internal/host/edgesfile_test.go`

**Interfaces:**
- Consumes: `appconfig.ParsePrivateKey`, `appconfig.ParsePublicKey`
  (`internal/appconfig/crypto.go:59`, `:73`), and Task 1's `edgesFile`.
- Produces:
  - `func KeyFingerprint(pub *ecdh.PublicKey) string`, which returns
    `"sha256:" + lowercase hex(sha256(pub.Bytes()))`;
  - `func PublicFingerprint(b64 string) (string, error)`, the fingerprint of
    a base64 `CARLOS_SECRETS_PUBKEY`;
  - `edgesFile.SecretsKeyFP string` (`json:"secrets_key_fp,omitempty"`);
  - `func edgesFileSecretsFP(getenv func(string) string) string`.

The field name and its omitempty shape are the move-any-app spec's
(`design-moves-any-region` at e41c4fef, §6). The format is fixed by this
spec's §6.1 for both, and `KeyFingerprint` is its only renderer.

- [ ] **Step 1: Write the failing `appconfig` test**

```go
// internal/appconfig/fingerprint_test.go
package appconfig

import (
	"bytes"
	"crypto/ecdh"
	"encoding/base64"
	"regexp"
	"testing"
)

// A box computes the fingerprint from its PRIVATE key's public half; the
// console computes it from CARLOS_SECRETS_PUBKEY. They must agree byte for
// byte (nearest-region spec §6.1, R9).
func TestKeyFingerprintAgreesAcrossHalves(t *testing.T) {
	priv, err := ecdh.P256().NewPrivateKey(bytes.Repeat([]byte{1}, 32))
	if err != nil {
		t.Fatal(err)
	}
	fromPriv := KeyFingerprint(priv.PublicKey())
	fromPub, err := PublicFingerprint(base64.StdEncoding.EncodeToString(priv.PublicKey().Bytes()))
	if err != nil {
		t.Fatal(err)
	}
	if fromPriv != fromPub {
		t.Fatalf("private half %q != public half %q", fromPriv, fromPub)
	}
	if !regexp.MustCompile(`^sha256:[0-9a-f]{64}$`).MatchString(fromPriv) {
		t.Fatalf("format = %q", fromPriv)
	}
	other, _ := ecdh.P256().NewPrivateKey(bytes.Repeat([]byte{2}, 32))
	if KeyFingerprint(other.PublicKey()) == fromPriv {
		t.Fatal("two keys share a fingerprint")
	}
	if _, err := PublicFingerprint("not base64!"); err == nil {
		t.Fatal("garbage parsed")
	}
}
```

- [ ] **Step 2: Run it to verify it fails**

Run: `go test ./internal/appconfig -run TestKeyFingerprint -count=1`
Expected: FAIL, undefined `KeyFingerprint`.

- [ ] **Step 3: Implement `fingerprint.go`**

```go
package appconfig

import (
	"crypto/ecdh"
	"crypto/sha256"
	"encoding/hex"
)

// KeyFingerprint names a secrets keypair without revealing either half:
// "sha256:" and the lowercase hex SHA-256 of the public key's uncompressed
// SEC1 bytes — the same bytes CARLOS_SECRETS_PUBKEY base64-decodes to. It
// is the ONE renderer of the edges file's secrets_key_fp, shared by the
// nearest-region spec (§6.1, R9) and the move-any-app spec (§6, T2): a box
// that can open this deployment's sealed config publishes exactly the
// string the console derives from its own public key.
func KeyFingerprint(pub *ecdh.PublicKey) string {
	sum := sha256.Sum256(pub.Bytes())
	return "sha256:" + hex.EncodeToString(sum[:])
}

// PublicFingerprint is KeyFingerprint of a base64 CARLOS_SECRETS_PUBKEY.
func PublicFingerprint(b64 string) (string, error) {
	pub, err := ParsePublicKey(b64, "CARLOS_SECRETS_PUBKEY")
	if err != nil {
		return "", err
	}
	return KeyFingerprint(pub), nil
}
```

- [ ] **Step 4: Run it to verify it passes**

Run: `go test ./internal/appconfig -run TestKeyFingerprint -count=1`
Expected: PASS.

- [ ] **Step 5: Write the failing edges-file tests**

Append to `internal/host/edgesfile_test.go`:

```go
// R9: the box publishes the fingerprint of its secrets key's public half,
// never the key; no key, or a key that does not parse, publishes nothing.
func TestEdgesFileSecretsKeyFingerprint(t *testing.T) {
	priv, err := ecdh.P256().NewPrivateKey(bytes.Repeat([]byte{1}, 32))
	if err != nil {
		t.Fatal(err)
	}
	privB64 := base64.StdEncoding.EncodeToString(priv.Bytes())
	pubB64 := base64.StdEncoding.EncodeToString(priv.PublicKey().Bytes())
	env := func(v string) func(string) string {
		return func(k string) string {
			if k == "CARLOS_SECRETS_KEY" {
				return v
			}
			return ""
		}
	}
	want := appconfig.KeyFingerprint(priv.PublicKey())
	if got := edgesFileSecretsFP(env(privB64)); got != want {
		t.Fatalf("with a key = %q, want %q", got, want)
	}
	for _, v := range []string{"", "not base64!", base64.StdEncoding.EncodeToString([]byte("short"))} {
		if got := edgesFileSecretsFP(env(v)); got != "" {
			t.Errorf("CARLOS_SECRETS_KEY=%q published %q, want nothing", v, got)
		}
	}

	for _, fp := range []string{want, ""} {
		s, reg := release.NewDirStore(t.TempDir()), edgesTestRegistry(t)
		seedRoute(t, reg, "app.example.com")
		var last []byte
		ident := edgesFile{Box: "quiet-harbour", Region: "eu-west-2", Placement: "uk", SecretsKeyFP: fp}
		if err := edgesFilePass(context.Background(), s, reg, ident, "203.0.113.10", &last); err != nil {
			t.Fatal(err)
		}
		body, _, err := s.Get(context.Background(), "control/edges/quiet-harbour.json")
		if err != nil {
			t.Fatal(err)
		}
		has := strings.Contains(string(body), `"secrets_key_fp"`)
		if has != (fp != "") {
			t.Fatalf("fp %q: key present = %v in %s", fp, has, body)
		}
		for _, secret := range []string{privB64, pubB64} {
			if strings.Contains(string(body), secret) {
				t.Fatal("the edges file carries key material")
			}
		}
	}
}
```

Add `bytes`, `crypto/ecdh`, `encoding/base64` and
`github.com/carlosframework/platform/internal/appconfig` to the test file's
imports.

- [ ] **Step 6: Run it to verify it fails**

Run: `socktest ./internal/host -run TestEdgesFileSecretsKeyFingerprint -count=1`
Expected: FAIL, unknown field `SecretsKeyFP`.

- [ ] **Step 7: Implement the field, the helper and the loop wiring**

In `edgesFile`, after `AcceptsInbound`:

```go
	// SecretsKeyFP is the fingerprint of this box's CARLOS_SECRETS_KEY's
	// public half (appconfig.KeyFingerprint), NEVER the key. Additive and
	// omitempty, the move-any-app spec's shape: absent means this box holds
	// no key, or one that does not parse — so it renders no exec config,
	// and an instance placed here would run with none (nearest-region spec
	// R9). The console requires it to equal the fingerprint of its own
	// CARLOS_SECRETS_PUBKEY before it places anything here automatically.
	SecretsKeyFP string `json:"secrets_key_fp,omitempty"`
```

Below `edgesFileEligibility`:

```go
// edgesFileSecretsFP fingerprints the box's secrets key, read the same way
// the config loops read it (agent.go configSyncLoop). The key itself never
// leaves this function.
func edgesFileSecretsFP(getenv func(string) string) string {
	raw := strings.TrimSpace(getenv("CARLOS_SECRETS_KEY"))
	if raw == "" {
		return ""
	}
	priv, err := appconfig.ParsePrivateKey(raw, "CARLOS_SECRETS_KEY")
	if err != nil {
		log.Printf("agent: CARLOS_SECRETS_KEY does not parse, so no secrets_key_fp is published: %v", err)
		return ""
	}
	return appconfig.KeyFingerprint(priv.PublicKey())
}
```

The log text is O10 from the copy record. `ParsePrivateKey`'s errors name
the variable and the parse failure, never the value.

Add `"github.com/carlosframework/platform/internal/appconfig"` to
`edgesfile.go`'s imports.

In `edgesFileLoop`'s ident literal, add:

```go
		SecretsKeyFP:          edgesFileSecretsFP(os.Getenv),
```

- [ ] **Step 8: Run the tests to verify they pass, then cross-compile**

Run:

```bash
socktest ./internal/host -count=1 && go test ./internal/appconfig -count=1
fail=0
for t in darwin/arm64 darwin/amd64 linux/arm64 windows/amd64 windows/arm64; do
  CGO_ENABLED=0 GOOS=${t%/*} GOARCH=${t#*/} go build ./... || { echo "FAILED $t"; fail=1; }
done
[ "$fail" = 0 ] || exit 1  # stops this block before any commit below it
```

Expected: PASS, and no `FAILED` line.

- [ ] **Step 9: Commit**

```bash
git add internal/appconfig/fingerprint.go internal/appconfig/fingerprint_test.go internal/host/edgesfile.go internal/host/edgesfile_test.go
git commit -m "edges file: publish secrets_key_fp, the fingerprint of the box's secrets key (nearest-region R9)

Same field and shape as the move-any-app spec; appconfig.KeyFingerprint
is the one renderer both use.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```

---

### Task 2: `internal/geo` points, distance and the nearest pick

**Files:**
- Create: `internal/geo/point.go`, `internal/geo/nearest.go`
- Test: `internal/geo/nearest_test.go`

**Interfaces:**
- Consumes: `LookupAWSRegion` from Task 4. To keep this task testable on its
  own, `Nearest` takes a `pointOf func(awsRegion string) (Point, bool)`
  parameter, and Task 4 adds the exported wrapper.
- Produces:
  - `type Point struct{ Lat, Lon float64 }`;
  - `func DistanceKm(a, b Point) float64`;
  - `type Candidate struct{ Code string; Home bool; AWSRegion string }`;
  - `func NearestWith(p Point, cands []Candidate, pointOf func(string) (Point, bool)) (code string, ok bool)`.

- [ ] **Step 1: Write the failing tests**

```go
// internal/geo/nearest_test.go
package geo

import (
	"math"
	"testing"
)

var testPoints = map[string]Point{
	"eu-west-1":      {53.35, -6.26},
	"eu-west-2":      {51.51, -0.13},
	"ap-southeast-2": {-33.87, 151.21},
}

func testPointOf(r string) (Point, bool) { p, ok := testPoints[r]; return p, ok }

func TestDistanceKmDublinLondon(t *testing.T) {
	d := DistanceKm(testPoints["eu-west-1"], testPoints["eu-west-2"])
	if math.Abs(d-464) > 5 {
		t.Fatalf("Dublin to London = %.1f km, want about 464", d)
	}
}

func TestNearest(t *testing.T) {
	london := Point{51.5, -0.12}
	sydney := Point{-33.9, 151.2}
	home := Candidate{Code: "", Home: true, AWSRegion: "eu-west-1"}
	uk := Candidate{Code: "uk", AWSRegion: "eu-west-2"}
	au := Candidate{Code: "au", AWSRegion: "ap-southeast-2"}

	cases := []struct {
		name   string
		p      Point
		cands  []Candidate
		want   string
		wantOK bool
	}{
		{"single", london, []Candidate{home}, "", true},
		{"clear winner", london, []Candidate{home, uk, au}, "uk", true},
		{"far side", sydney, []Candidate{home, uk, au}, "au", true},
		{"home with no point gives no answer", london,
			[]Candidate{{Code: "", Home: true, AWSRegion: ""}, uk}, "", false},
		{"non-home with no point is left out", london,
			[]Candidate{home, {Code: "zz", AWSRegion: "xx-nowhere-1"}}, "", true},
		{"empty", london, nil, "", false},
		{"tie goes to the home", testPoints["eu-west-2"],
			[]Candidate{{Code: "uk2", AWSRegion: "eu-west-2"}, {Code: "", Home: true, AWSRegion: "eu-west-2"}}, "", true},
		{"non-home tie goes to the smaller code", testPoints["eu-west-2"],
			[]Candidate{{Code: "uk2", AWSRegion: "eu-west-2"}, {Code: "uk", AWSRegion: "eu-west-2"}}, "uk", true},
	}
	for _, c := range cases {
		got, ok := NearestWith(c.p, c.cands, testPointOf)
		if got != c.want || ok != c.wantOK {
			t.Errorf("%s: got (%q, %v), want (%q, %v)", c.name, got, ok, c.want, c.wantOK)
		}
	}
}
```

- [ ] **Step 2: Run them to verify they fail**

Run: `go test ./internal/geo -count=1`
Expected: FAIL, because the package does not exist.

- [ ] **Step 3: Implement `point.go`**

```go
// Package geo turns a timezone or an IP into a point on the globe and picks
// the nearest of a set of AWS regions (nearest-region spec §6). It holds
// points only for the length of one call: nothing here stores, logs or
// returns a caller's coordinates.
package geo

import "math"

// Point is a latitude and longitude in degrees.
type Point struct{ Lat, Lon float64 }

// earthRadiusKm is the IUGG mean Earth radius.
const earthRadiusKm = 6371.0088

// DistanceKm is the great-circle (haversine) distance between a and b.
func DistanceKm(a, b Point) float64 {
	rad := func(d float64) float64 { return d * math.Pi / 180 }
	dLat := rad(b.Lat - a.Lat)
	dLon := rad(b.Lon - a.Lon)
	h := math.Sin(dLat/2)*math.Sin(dLat/2) +
		math.Cos(rad(a.Lat))*math.Cos(rad(b.Lat))*math.Sin(dLon/2)*math.Sin(dLon/2)
	return 2 * earthRadiusKm * math.Asin(math.Sqrt(h))
}
```

- [ ] **Step 4: Implement `nearest.go`**

```go
package geo

import "math"

// Candidate is one region a guess may pick. Code is in the record
// vocabulary ("" is the deployment's home); the caller has already
// filtered to eligible, policy-allowed candidates.
type Candidate struct {
	Code      string
	Home      bool
	AWSRegion string
}

// NearestWith returns the candidate nearest p, measured to each
// candidate's AWS region through pointOf.
//
//   - A candidate whose region has no point is left out.
//   - If the HOME is among cands and has no point, ok is false: picking
//     among the rest could choose a far region over an unmeasured near
//     home, and the home is where today's behaviour lives (spec §6.3).
//   - Distances are compared in whole kilometres. A tie goes to the home,
//     then to the lexically smallest code, so every console answers alike.
func NearestWith(p Point, cands []Candidate, pointOf func(string) (Point, bool)) (code string, ok bool) {
	best := -1
	bestKm := math.MaxInt
	for i, c := range cands {
		cp, has := pointOf(c.AWSRegion)
		if !has {
			if c.Home {
				return "", false
			}
			continue
		}
		km := int(math.Round(DistanceKm(p, cp)))
		switch {
		case best < 0, km < bestKm:
		case km == bestKm && better(c, cands[best]):
		default:
			continue
		}
		best, bestKm = i, km
	}
	if best < 0 {
		return "", false
	}
	return cands[best].Code, true
}

// better breaks an exact tie: the home first, then the smaller code.
func better(a, b Candidate) bool {
	if a.Home != b.Home {
		return a.Home
	}
	return a.Code < b.Code
}
```

- [ ] **Step 5: Run the tests to verify they pass**

Run: `go test ./internal/geo -count=1`
Expected: PASS.

- [ ] **Step 6: Commit**

```bash
git add internal/geo/point.go internal/geo/nearest.go internal/geo/nearest_test.go
git commit -m "geo: haversine and the nearest-region pick with deterministic ties (nearest-region §6.3)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```

---

### Task 3: `internal/geo` timezones and country points

**Files:**
- Create: `internal/geo/data/zone.tab`, `internal/geo/data/links.txt`, `internal/geo/data/VERSION`
- Create: `internal/geo/tz.go`, `scripts/update-tzgeo.sh`
- Test: `internal/geo/tz_test.go`

**Interfaces:**
- Consumes: `Point` from Task 2.
- Produces:
  - `func ZonePoint(name string) (Point, bool)`;
  - `func CountryPoint(iso2 string) (Point, bool)`;
  - `func TZDataVersion() string`.

- [ ] **Step 1: Write the refresh script**

```bash
#!/usr/bin/env bash
# scripts/update-tzgeo.sh <version>, e.g. 2026b
#
# Refreshes internal/geo/data from one IANA tzdata release
# (nearest-region spec §6.2). zone.tab carries the coordinates; the Link
# lines resolve the old names browsers still report (Asia/Calcutta). Run it
# as a release-checklist step beside the Go toolchain bump; a stale table
# only misses zone names newer than it, and those fall through to IP.
set -euo pipefail
v="${1:?usage: update-tzgeo.sh <tzdata version, e.g. 2026b>}"
here="$(cd "$(dirname "$0")/.." && pwd)"
out="$here/internal/geo/data"
work="$(mktemp -d "${TMPDIR:-/tmp}/tzgeo.XXXXXX")"
trap 'rm -rf "$work"' EXIT
base="https://data.iana.org/time-zones/releases"
curl -fsSL -o "$work/tzdata.tar.gz" "$base/tzdata$v.tar.gz"
curl -fsSL -o "$work/tzdata.tar.gz.asc" "$base/tzdata$v.tar.gz.asc"
if command -v gpg >/dev/null; then
  gpg --verify "$work/tzdata.tar.gz.asc" "$work/tzdata.tar.gz"
else
  echo "gpg not found: signature NOT checked" >&2
  exit 1
fi
tar -xzf "$work/tzdata.tar.gz" -C "$work"
mkdir -p "$out"
cp "$work/zone.tab" "$out/zone.tab"
awk '$1 == "Link" { print $2, $3 }' \
  "$work"/africa "$work"/antarctica "$work"/asia "$work"/australasia \
  "$work"/backward "$work"/etcetera "$work"/europe "$work"/northamerica \
  "$work"/southamerica | sort -u > "$out/links.txt"
printf '%s\n' "$v" > "$out/VERSION"
echo "internal/geo/data refreshed to tzdata $v"
```

`chmod +x scripts/update-tzgeo.sh`. Its three output lines are S1 to S3 from the copy record.

- [ ] **Step 2: Generate the data**

Run: `scripts/update-tzgeo.sh 2026b`. This needs network access to
`data.iana.org` and `gpg` with the IANA key; outside the sandbox, use
`dangerouslyDisableSandbox` for this one command.

Expected: three files in `internal/geo/data/`. `links.txt` has lines like
`Asia/Kolkata Asia/Calcutta`, and `VERSION` contains `2026b`.

- [ ] **Step 3: Write the failing tests**

```go
// internal/geo/tz_test.go
package geo

import (
	"strings"
	"testing"
)

func TestZonePoint(t *testing.T) {
	mapped := []string{
		"Europe/London", "Europe/Dublin", "Europe/Amsterdam",
		"Asia/Kolkata", "Asia/Calcutta",
		"America/Argentina/Buenos_Aires", "europe/london",
	}
	for _, z := range mapped {
		if _, ok := ZonePoint(z); !ok {
			t.Errorf("ZonePoint(%q) unmapped, want a point", z)
		}
	}
	a, _ := ZonePoint("Asia/Calcutta")
	b, _ := ZonePoint("Asia/Kolkata")
	if a != b {
		t.Errorf("Asia/Calcutta %v != Asia/Kolkata %v; the link must resolve to one point", a, b)
	}
	unmapped := []string{
		"", "UTC", "Etc/UTC", "Etc/GMT+5", "GMT", "UCT", "Universal", "Zulu",
		"Factory", "Local", "localtime", "posixrules", "../etc/passwd",
		"Europe/London/Extra/Deep", strings.Repeat("A", 65), "Nowhere/Atlantis",
	}
	for _, z := range unmapped {
		if p, ok := ZonePoint(z); ok {
			t.Errorf("ZonePoint(%q) = %v, want unmappable", z, p)
		}
	}
	l, _ := ZonePoint("Europe/London")
	if l.Lat < 51 || l.Lat > 52 || l.Lon > 0.5 || l.Lon < -0.5 {
		t.Errorf("Europe/London = %v, want about 51.5, -0.12", l)
	}
}

// A link cycle must terminate (at most 4 hops) rather than hang.
func TestZonePointLinkLoopTerminates(t *testing.T) {
	links := map[string]string{"a/x": "a/y", "a/y": "a/x"}
	if _, ok := resolveLinks("a/x", links, map[string]Point{}); ok {
		t.Fatal("a link loop resolved to a point")
	}
}

// Every country with more than one zone.tab row needs an override naming
// its principal zone: a tzdata refresh that gives a country a second row
// fails here instead of silently averaging a continent (spec §6.2).
func TestCountryOverridesCoverEveryMultiZoneCountry(t *testing.T) {
	for cc, n := range zoneRowsPerCountry() {
		if n < 2 {
			continue
		}
		zone, ok := countryOverride[cc]
		if !ok {
			t.Errorf("country %s has %d zone.tab rows and no override", cc, n)
			continue
		}
		if _, ok := ZonePoint(zone); !ok {
			t.Errorf("override for %s names %q, which is not in zone.tab", cc, zone)
		}
	}
	for cc := range countryOverride {
		if zoneRowsPerCountry()[cc] < 2 {
			t.Errorf("override for %s is unused: it has fewer than two rows", cc)
		}
	}
}

func TestCountryPoint(t *testing.T) {
	for _, cc := range []string{"GB", "IE", "US", "AU", "IN", "gb"} {
		if _, ok := CountryPoint(cc); !ok {
			t.Errorf("CountryPoint(%q) unmapped", cc)
		}
	}
	us, _ := CountryPoint("US")
	ny, _ := ZonePoint("America/New_York")
	if us != ny {
		t.Errorf("US = %v, want its principal zone New York %v", us, ny)
	}
	if _, ok := CountryPoint("ZZ"); ok {
		t.Error("CountryPoint(ZZ) mapped")
	}
}

func TestTZDataVersion(t *testing.T) {
	if v := TZDataVersion(); v == "" || strings.ContainsAny(v, " \n") {
		t.Fatalf("TZDataVersion = %q", v)
	}
}
```

- [ ] **Step 4: Run them to verify they fail**

Run: `go test ./internal/geo -run 'TestZonePoint|TestCountry|TestTZDataVersion' -count=1`
Expected: FAIL, undefined `ZonePoint`.

- [ ] **Step 5: Implement `tz.go`**

```go
package geo

import (
	_ "embed"
	"regexp"
	"strconv"
	"strings"
	"sync"
)

//go:embed data/zone.tab
var zoneTab string

//go:embed data/links.txt
var linksTxt string

//go:embed data/VERSION
var tzVersion string

// TZDataVersion is the IANA release the embedded tables came from.
func TZDataVersion() string { return strings.TrimSpace(tzVersion) }

var (
	tzOnce     sync.Once
	zonePoints map[string]Point // lowercased zone name -> point
	zoneLinks  map[string]string
	countryOne map[string]Point // countries with exactly one row
	rowsPerCC  map[string]int
)

// zoneShape is spec §6.2 step 1: at most 64 bytes, one to three
// "/"-separated segments of [A-Za-z0-9_+-].
var zoneShape = regexp.MustCompile(`^[A-Za-z0-9_+\-]+(/[A-Za-z0-9_+\-]+){0,2}$`)

// notAPlace names an offset or a machine, not a place (R3).
var notAPlace = map[string]bool{
	"": true, "local": true, "localtime": true, "posixrules": true,
	"utc": true, "uct": true, "gmt": true, "universal": true, "zulu": true,
	"factory": true,
}

func loadTZ() {
	zonePoints = map[string]Point{}
	countryOne = map[string]Point{}
	rowsPerCC = map[string]int{}
	for _, line := range strings.Split(zoneTab, "\n") {
		if line == "" || strings.HasPrefix(line, "#") {
			continue
		}
		f := strings.Split(line, "\t")
		if len(f) < 3 {
			continue
		}
		p, ok := parseISO6709(f[1])
		if !ok {
			continue
		}
		zonePoints[strings.ToLower(f[2])] = p
		cc := strings.ToUpper(f[0])
		rowsPerCC[cc]++
		countryOne[cc] = p
	}
	zoneLinks = map[string]string{}
	for _, line := range strings.Split(linksTxt, "\n") {
		f := strings.Fields(line)
		if len(f) == 2 {
			zoneLinks[strings.ToLower(f[1])] = strings.ToLower(f[0])
		}
	}
}

// ZonePoint maps an IANA zone name to its principal location, following
// tzdata links (at most 4 hops). It refuses anything that is not a place.
func ZonePoint(name string) (Point, bool) {
	tzOnce.Do(loadTZ)
	name = strings.TrimSpace(name)
	if len(name) > 64 || !zoneShape.MatchString(name) {
		return Point{}, false
	}
	low := strings.ToLower(name)
	if notAPlace[low] || strings.HasPrefix(low, "etc/") {
		return Point{}, false
	}
	return resolveLinks(low, zoneLinks, zonePoints)
}

func resolveLinks(name string, links map[string]string, points map[string]Point) (Point, bool) {
	for hop := 0; hop <= 4; hop++ {
		if p, ok := points[name]; ok {
			return p, true
		}
		next, ok := links[name]
		if !ok {
			return Point{}, false
		}
		name = next
	}
	return Point{}, false
}

// countryOverride names the principal zone of every country with more
// than one zone.tab row (tzdata 2026b). TestCountryOverridesCoverEvery
// MultiZoneCountry fails when a refresh adds a multi-row country.
var countryOverride = map[string]string{
	"AQ": "Antarctica/McMurdo", "AR": "America/Argentina/Buenos_Aires",
	"AU": "Australia/Sydney", "BR": "America/Sao_Paulo", "CA": "America/Toronto",
	"CD": "Africa/Kinshasa", "CL": "America/Santiago", "CN": "Asia/Shanghai",
	"CY": "Asia/Nicosia", "DE": "Europe/Berlin", "EC": "America/Guayaquil",
	"ES": "Europe/Madrid", "FM": "Pacific/Pohnpei", "GL": "America/Nuuk",
	"ID": "Asia/Jakarta", "KI": "Pacific/Tarawa", "KZ": "Asia/Almaty",
	"MH": "Pacific/Majuro", "MN": "Asia/Ulaanbaatar", "MX": "America/Mexico_City",
	"MY": "Asia/Kuala_Lumpur", "NZ": "Pacific/Auckland", "PF": "Pacific/Tahiti",
	"PG": "Pacific/Port_Moresby", "PS": "Asia/Gaza", "PT": "Europe/Lisbon",
	"RU": "Europe/Moscow", "UA": "Europe/Kyiv", "UM": "Pacific/Wake",
	"US": "America/New_York", "UZ": "Asia/Tashkent",
}

// CountryPoint maps an ISO 3166-1 alpha-2 code to a point: its only
// zone.tab row, or its override's zone.
func CountryPoint(iso2 string) (Point, bool) {
	tzOnce.Do(loadTZ)
	cc := strings.ToUpper(strings.TrimSpace(iso2))
	if zone, ok := countryOverride[cc]; ok {
		return ZonePoint(zone)
	}
	if rowsPerCC[cc] == 1 {
		return countryOne[cc], true
	}
	return Point{}, false
}

func zoneRowsPerCountry() map[string]int { tzOnce.Do(loadTZ); return rowsPerCC }

// parseISO6709 reads zone.tab's ±DDMM±DDDMM or ±DDMMSS±DDDMMSS.
func parseISO6709(s string) (Point, bool) {
	i := strings.IndexAny(s[1:], "+-") + 1
	if i <= 0 {
		return Point{}, false
	}
	lat, ok1 := dms(s[:i], 2)
	lon, ok2 := dms(s[i:], 3)
	return Point{lat, lon}, ok1 && ok2
}

func dms(s string, degDigits int) (float64, bool) {
	sign := 1.0
	if s[0] == '-' {
		sign = -1
	}
	d := s[1:]
	if len(d) != degDigits+2 && len(d) != degDigits+4 {
		return 0, false
	}
	deg, err1 := strconv.Atoi(d[:degDigits])
	min, err2 := strconv.Atoi(d[degDigits : degDigits+2])
	sec := 0
	var err3 error
	if len(d) == degDigits+4 {
		sec, err3 = strconv.Atoi(d[degDigits+2:])
	}
	if err1 != nil || err2 != nil || err3 != nil {
		return 0, false
	}
	return sign * (float64(deg) + float64(min)/60 + float64(sec)/3600), true
}
```

- [ ] **Step 6: Run the tests to verify they pass**

Run: `go test ./internal/geo -count=1`
Expected: PASS. If `TestCountryOverridesCoverEveryMultiZoneCountry` names a
country, the downloaded tzdata differs from 2026b. Add or remove that
override using the principal zone, which is the country's capital or largest
city.

- [ ] **Step 7: Commit**

```bash
git add internal/geo/tz.go internal/geo/tz_test.go internal/geo/data scripts/update-tzgeo.sh
git commit -m "geo: embedded IANA zone.tab and links, zone and country points (nearest-region §6.2)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```

---

### Task 4: AWS region table and place names (C23, from the approved record)

**Files:**
- Create: `internal/geo/awsregions.go`
- Test: `internal/geo/awsregions_test.go`

**Interfaces:**
- Consumes:
  - `Point` and `NearestWith` (Task 2);
  - the record's `C23-*` entries (Task 0).
- Produces:
  - `type AWSRegion struct{ Point; Place string }`;
  - `func LookupAWSRegion(code string) (AWSRegion, bool)`;
  - `func Nearest(p Point, cands []Candidate) (string, bool)`;
  - `func PlaceName(awsRegion, fallback string) string`.

Tests elsewhere name a place through `geo.PlaceName("eu-west-2", "uk")`,
never a literal, so the approved text can differ from the draft without
breaking them.

- [ ] **Step 1: Write the failing tests**

```go
// internal/geo/awsregions_test.go
package geo

import (
	"regexp"
	"strings"
	"testing"
)

func TestAWSRegionTable(t *testing.T) {
	shape := regexp.MustCompile(`^[a-z]{2}(-[a-z]+)+-\d$`)
	for code, r := range awsRegions {
		if !shape.MatchString(code) {
			t.Errorf("%s is not an AWS region code", code)
		}
		if r.Lat < -90 || r.Lat > 90 || r.Lon < -180 || r.Lon > 180 {
			t.Errorf("%s: %v out of range", code, r.Point)
		}
		if r.Place == "" || strings.Contains(r.Place, "—") {
			t.Errorf("%s: place %q empty or contains an em dash", code, r.Place)
		}
	}
	if len(awsRegions) != 34 {
		t.Errorf("%d regions, want the 34 of spec C23", len(awsRegions))
	}
	if PlaceName("xx-nowhere-1", "zz") != "zz" || PlaceName("", "ie") != "ie" {
		t.Error("PlaceName must fall back to the given code")
	}
	if r, ok := LookupAWSRegion("eu-west-2"); !ok || PlaceName("eu-west-2", "uk") != r.Place {
		t.Error("PlaceName must read the table")
	}
}

func TestNearestUsesTheTable(t *testing.T) {
	london, _ := ZonePoint("Europe/London")
	got, ok := Nearest(london, []Candidate{
		{Code: "", Home: true, AWSRegion: "eu-west-1"},
		{Code: "uk", AWSRegion: "eu-west-2"},
	})
	if !ok || got != "uk" {
		t.Fatalf("Nearest from London = (%q, %v), want uk", got, ok)
	}
}
```

- [ ] **Step 2: Run them to verify they fail**

Run: `go test ./internal/geo -run 'TestAWSRegionTable|TestNearestUsesTheTable' -count=1`
Expected: FAIL, undefined `awsRegions`.

- [ ] **Step 3: Implement the table**

The `Place` strings below are the drafts Task 0 submitted. Write the
record's `C23-<code>` text for each in their place.

```go
package geo

// AWSRegion is one AWS commercial region's metro-area point and the place
// name every surface shows for it (spec §14, copy C23 as approved in the
// nearest-region copy record).
//
// Source: AWS's official region list
// (https://docs.aws.amazon.com/global-infrastructure/latest/regions/aws-regions.html)
// as of 2026-10-01. Coordinates are each region's metro area, checked by
// hand; AWS publishes no facility locations, and tens of kilometres never
// change a choice between regions.
type AWSRegion struct {
	Point
	Place string
}

var awsRegions = map[string]AWSRegion{
	"us-east-1":      {Point{39.04, -77.49}, "US East · N. Virginia"},
	"us-east-2":      {Point{39.96, -83.00}, "US East · Ohio"},
	"us-west-1":      {Point{37.77, -122.42}, "US West · N. California"},
	"us-west-2":      {Point{45.84, -119.70}, "US West · Oregon"},
	"ca-central-1":   {Point{45.50, -73.57}, "Canada · Montréal"},
	"ca-west-1":      {Point{51.05, -114.07}, "Canada · Calgary"},
	"mx-central-1":   {Point{20.59, -100.39}, "Mexico · Querétaro"},
	"sa-east-1":      {Point{-23.55, -46.63}, "Brazil · São Paulo"},
	"eu-west-1":      {Point{53.35, -6.26}, "Ireland · Dublin"},
	"eu-west-2":      {Point{51.51, -0.13}, "UK · London"},
	"eu-west-3":      {Point{48.86, 2.35}, "France · Paris"},
	"eu-central-1":   {Point{50.11, 8.68}, "Germany · Frankfurt"},
	"eu-central-2":   {Point{47.37, 8.54}, "Switzerland · Zurich"},
	"eu-south-1":     {Point{45.46, 9.19}, "Italy · Milan"},
	"eu-south-2":     {Point{41.65, -0.89}, "Spain · Aragón"},
	"eu-north-1":     {Point{59.33, 18.07}, "Sweden · Stockholm"},
	"il-central-1":   {Point{32.09, 34.78}, "Israel · Tel Aviv"},
	"me-south-1":     {Point{26.23, 50.59}, "Bahrain"},
	"me-central-1":   {Point{25.20, 55.27}, "UAE"},
	"af-south-1":     {Point{-33.92, 18.42}, "South Africa · Cape Town"},
	"ap-south-1":     {Point{19.08, 72.88}, "India · Mumbai"},
	"ap-south-2":     {Point{17.39, 78.49}, "India · Hyderabad"},
	"ap-east-1":      {Point{22.32, 114.17}, "Hong Kong"},
	"ap-east-2":      {Point{25.03, 121.57}, "Taiwan · Taipei"},
	"ap-southeast-1": {Point{1.35, 103.82}, "Singapore"},
	"ap-southeast-2": {Point{-33.87, 151.21}, "Australia · Sydney"},
	"ap-southeast-3": {Point{-6.21, 106.85}, "Indonesia · Jakarta"},
	"ap-southeast-4": {Point{-37.81, 144.96}, "Australia · Melbourne"},
	"ap-southeast-5": {Point{3.14, 101.69}, "Malaysia"},
	"ap-southeast-6": {Point{-36.85, 174.76}, "New Zealand · Auckland"},
	"ap-southeast-7": {Point{13.76, 100.50}, "Thailand · Bangkok"},
	"ap-northeast-1": {Point{35.68, 139.69}, "Japan · Tokyo"},
	"ap-northeast-2": {Point{37.57, 126.98}, "South Korea · Seoul"},
	"ap-northeast-3": {Point{34.69, 135.50}, "Japan · Osaka"},
}

// LookupAWSRegion finds a region in the table.
func LookupAWSRegion(code string) (AWSRegion, bool) {
	r, ok := awsRegions[code]
	return r, ok
}

func tablePoint(code string) (Point, bool) {
	r, ok := awsRegions[code]
	return r.Point, ok
}

// Nearest is NearestWith over the shipped table.
func Nearest(p Point, cands []Candidate) (string, bool) {
	return NearestWith(p, cands, tablePoint)
}

// PlaceName is the table's place for awsRegion, or fallback (the bare
// placement code) for a region the table does not know.
func PlaceName(awsRegion, fallback string) string {
	if r, ok := awsRegions[awsRegion]; ok {
		return r.Place
	}
	return fallback
}
```

- [ ] **Step 4: Run the tests to verify they pass**

Run: `go test ./internal/geo -count=1`
Expected: PASS.

- [ ] **Step 5: Check the coordinates by hand**

For each row, confirm the coordinates fall within the named metro area, and
that the 34 codes match AWS's current list. Correct any row that does not,
then re-run step 4.

- [ ] **Step 6: Commit**

```bash
git add internal/geo/awsregions.go internal/geo/awsregions_test.go
git commit -m "geo: AWS region table with approved place names (nearest-region §14)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```

---

### Task 5: Optional GeoIP locator

**Files:**
- Create: `internal/geo/mmdb.go`
- Create, under `internal/geo/testdata/`:
  - `GeoIP2-City-Test.mmdb`, `GeoIP2-Country-Test.mmdb`, `MaxMind-DB-test-ipv4-24.mmdb`;
  - `GeoIP2-Country-Test.json`;
  - `LICENSE-MIT`, `LICENSE-APACHE`;
  - `README`.
- Modify: `go.mod`, `go.sum`
- Test: `internal/geo/mmdb_test.go`

**Interfaces:**
- Consumes: `Point` and `CountryPoint` (Task 3), and O5 from the copy
  record.
- Produces:
  - `type Locator interface{ Locate(ip net.IP) (Point, bool) }`;
  - `func OpenMMDB(path string) (*MMDB, error)`;
  - `func (m *MMDB) Locate(ip net.IP) (Point, bool)`;
  - `func (m *MMDB) Close() error`;
  - `func Locatable(ip net.IP) bool`.

- [ ] **Step 1: Add the dependency and the pinned fixtures (network)**

Fetch every fixture and licence file at the commit the approved F1 text
names (Task 0 Step 0 resolved it before review), so the files and their
recorded provenance cannot disagree (codex plan-R3 3). Run this outside the
sandbox if needed:

```bash
go get github.com/oschwald/maxminddb-golang@v1.13.1
rec=docs/superpowers/plans/2026-10-02-nearest-region-copy.json
sha=$(jq -r '.entries[] | select(.id == "F1") | .text' "$rec" | grep -oE '[0-9a-f]{40}')
[ ${#sha} = 40 ] && [ -z "${sha//[0-9a-f]/}" ] || { echo "F1 must name exactly one commit"; exit 1; }
base="https://raw.githubusercontent.com/maxmind/MaxMind-DB/$sha"
d=internal/geo/testdata
mkdir -p "$d"
for f in test-data/GeoIP2-City-Test.mmdb test-data/GeoIP2-Country-Test.mmdb \
         test-data/MaxMind-DB-test-ipv4-24.mmdb source-data/GeoIP2-Country-Test.json \
         LICENSE-MIT LICENSE-APACHE; do
  curl -fsSL -o "$d/$(basename "$f")" "$base/$f" || { echo "missing $f at $sha"; exit 1; }
done
echo "$sha"
```

If a licence file has a different name at that commit, list the repository
root at `$sha` on GitHub and fetch the MIT and Apache licence files under
their real names. Write `internal/geo/testdata/README` as the record's F1
text, byte for byte, with nothing substituted:

```bash
jq -j '.entries[] | select(.id == "F1") | .text' "$rec" > internal/geo/testdata/README
```

- [ ] **Step 2: Write the failing tests**

```go
// internal/geo/mmdb_test.go
package geo

import (
	"bytes"
	"errors"
	"log"
	"net"
	"os"
	"path/filepath"
	"testing"
)

func TestOpenMMDBRefusesNonDatabases(t *testing.T) {
	dir := t.TempDir()
	missing := filepath.Join(dir, "missing.mmdb")
	junk := filepath.Join(dir, "junk.mmdb")
	if err := os.WriteFile(junk, []byte("not a maxmind database"), 0o600); err != nil {
		t.Fatal(err)
	}
	city, err := os.ReadFile("testdata/GeoIP2-City-Test.mmdb")
	if err != nil {
		t.Fatal(err)
	}
	truncated := filepath.Join(dir, "truncated.mmdb")
	if err := os.WriteFile(truncated, city[:len(city)/3], 0o600); err != nil {
		t.Fatal(err)
	}
	for _, p := range []string{missing, junk, truncated} {
		if m, err := OpenMMDB(p); err == nil {
			m.Close()
			t.Errorf("OpenMMDB(%s) succeeded, want an error", filepath.Base(p))
		}
	}
}

func TestMMDBCityRecordGivesItsLocation(t *testing.T) {
	m, err := OpenMMDB("testdata/GeoIP2-City-Test.mmdb")
	if err != nil {
		t.Fatal(err)
	}
	defer m.Close()
	p, ok := m.Locate(net.ParseIP("81.2.69.142"))
	if !ok || p.Lat < 51 || p.Lat > 52 {
		t.Fatalf("81.2.69.142 = (%v, %v), want a London point", p, ok)
	}
	// Review Focus 2: an IPv4-mapped IPv6 address is the same client.
	if mapped, ok := m.Locate(net.ParseIP("::ffff:81.2.69.142")); !ok || mapped != p {
		t.Fatalf("::ffff:81.2.69.142 = (%v, %v), want %v", mapped, ok, p)
	}
}

func TestMMDBCountryOnlyRecordGivesTheCountryPoint(t *testing.T) {
	m, err := OpenMMDB("testdata/GeoIP2-Country-Test.mmdb")
	if err != nil {
		t.Fatal(err)
	}
	defer m.Close()
	p, ok := m.Locate(net.ParseIP("81.2.69.142"))
	gb, _ := CountryPoint("GB")
	if !ok || p != gb {
		t.Fatalf("country-only 81.2.69.142 = (%v, %v), want GB's point %v", p, ok, gb)
	}
}

// Review Focus 2 and spec §6.5: a public IPv6 address looked up in an
// IPv4-only database is a miss, and the reader's error — which contains
// the address — never reaches the log.
func TestMMDBLookupErrorsNeverLogTheAddress(t *testing.T) {
	m, err := OpenMMDB("testdata/MaxMind-DB-test-ipv4-24.mmdb")
	if err != nil {
		t.Fatal(err)
	}
	defer m.Close()
	var buf bytes.Buffer
	prev := log.Writer()
	log.SetOutput(&buf)
	defer log.SetOutput(prev)
	lookupErrLast = lookupErrLast.AddDate(-1, 0, 0) // un-throttle
	if _, ok := m.Locate(net.ParseIP("2001:4860::1")); ok {
		t.Fatal("an IPv6 address located in an IPv4-only database")
	}
	if bytes.Contains(buf.Bytes(), []byte("2001:4860")) {
		t.Fatalf("the address leaked into the log: %s", buf.String())
	}
	if buf.Len() == 0 {
		t.Fatal("a lookup failure must be logged (as a fixed category)")
	}
}

// Review Focus 2: non-public addresses never reach the reader — proved
// through Locate itself, with a counting lookup in place of the reader.
func TestLocateNeverLooksUpNonPublicAddresses(t *testing.T) {
	calls := 0
	m := &MMDB{lookup: func(net.IP, any) error { calls++; return errors.New("unused") }}
	for _, s := range []string{
		"127.0.0.1", "::1", "10.1.2.3", "192.168.0.1", "172.16.0.1",
		"100.64.0.1", "169.254.1.1", "fe80::1", "fd00::1", "0.0.0.0", "::",
	} {
		if _, ok := m.Locate(net.ParseIP(s)); ok {
			t.Errorf("Locate(%s) answered", s)
		}
	}
	if calls != 0 {
		t.Fatalf("the reader was asked %d time(s) about non-public addresses", calls)
	}
	m.Locate(net.ParseIP("81.2.69.142"))
	if calls != 1 {
		t.Fatalf("a public address must reach the reader, calls = %d", calls)
	}
	var nilM *MMDB
	if _, ok := nilM.Locate(net.ParseIP("81.2.69.142")); ok {
		t.Error("a nil *MMDB located an address")
	}
}
```

If 81.2.69.142 is absent from `GeoIP2-Country-Test.mmdb`, choose an address
that `testdata/GeoIP2-Country-Test.json` maps to a country, and assert that
country's `CountryPoint`.

- [ ] **Step 3: Run them to verify they fail**

Run: `go test ./internal/geo -run 'MMDB|Locate' -count=1`
Expected: FAIL, undefined `OpenMMDB`.

- [ ] **Step 4: Implement `mmdb.go`**

```go
package geo

import (
	"log"
	"net"
	"sync"
	"time"

	"github.com/oschwald/maxminddb-golang"
)

// Locator turns a client address into a point. A nil Locator, or a nil
// *MMDB, locates nothing: the IP step is optional (spec §6.2).
type Locator interface {
	Locate(ip net.IP) (Point, bool)
}

// MMDB is an operator-supplied MaxMind-format database
// (CONSOLE_GEOIP_DB). lookup is the reader's Lookup, a field so a test can
// prove which addresses ever reach it.
type MMDB struct {
	r      *maxminddb.Reader
	lookup func(ip net.IP, result any) error
}

// OpenMMDB opens and verifies the file. An error means "IP step off"; the
// console logs it once and starts anyway.
func OpenMMDB(path string) (*MMDB, error) {
	r, err := maxminddb.Open(path)
	if err != nil {
		return nil, err
	}
	if err := r.Verify(); err != nil {
		r.Close()
		return nil, err
	}
	return &MMDB{r: r, lookup: r.Lookup}, nil
}

// Close releases the mapping.
func (m *MMDB) Close() error {
	if m == nil || m.r == nil {
		return nil
	}
	return m.r.Close()
}

type mmdbRecord struct {
	Location struct {
		Latitude  *float64 `maxminddb:"latitude"`
		Longitude *float64 `maxminddb:"longitude"`
	} `maxminddb:"location"`
	Country struct {
		ISOCode string `maxminddb:"iso_code"`
	} `maxminddb:"country"`
	RegisteredCountry struct {
		ISOCode string `maxminddb:"iso_code"`
	} `maxminddb:"registered_country"`
}

var (
	lookupErrMu   sync.Mutex
	lookupErrLast time.Time
)

// Locate returns the record's location (City databases), else its
// country's point, else its registered country's. A lookup error is a
// miss, logged as a FIXED category at most once a minute: the reader's
// own error text can contain the address (spec §6.5).
func (m *MMDB) Locate(ip net.IP) (Point, bool) {
	if m == nil || m.lookup == nil || !Locatable(ip) {
		return Point{}, false
	}
	if v4 := ip.To4(); v4 != nil {
		ip = v4
	}
	var rec mmdbRecord
	if err := m.lookup(ip, &rec); err != nil {
		lookupErrMu.Lock()
		if time.Since(lookupErrLast) > time.Minute {
			lookupErrLast = time.Now()
			log.Print("geo: a GeoIP lookup failed and was treated as a miss")
		}
		lookupErrMu.Unlock()
		return Point{}, false
	}
	if rec.Location.Latitude != nil && rec.Location.Longitude != nil {
		return Point{*rec.Location.Latitude, *rec.Location.Longitude}, true
	}
	if p, ok := CountryPoint(rec.Country.ISOCode); ok {
		return p, true
	}
	return CountryPoint(rec.RegisteredCountry.ISOCode)
}

var cgnat = &net.IPNet{IP: net.IPv4(100, 64, 0, 0), Mask: net.CIDRMask(10, 32)}

// Locatable is false for addresses that say nothing about where a person
// is: loopback, private, link-local, CGNAT and unspecified.
func Locatable(ip net.IP) bool {
	if ip == nil || ip.IsLoopback() || ip.IsPrivate() || ip.IsUnspecified() ||
		ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() || cgnat.Contains(ip) {
		return false
	}
	return true
}
```

The log text is O5 from the copy record.

- [ ] **Step 5: Run the tests to verify they pass**

Run: `go test ./internal/geo -count=1 && go mod tidy && git diff --stat go.mod go.sum`
Expected: PASS. `go.mod` gains `github.com/oschwald/maxminddb-golang v1.13.1`
and its `golang.org/x/sys` requirement only.

- [ ] **Step 6: Cross-compile, then commit**

```bash
fail=0
for t in darwin/arm64 darwin/amd64 linux/arm64 windows/amd64 windows/arm64; do
  CGO_ENABLED=0 GOOS=${t%/*} GOARCH=${t#*/} go build ./... || { echo "FAILED $t"; fail=1; }
done
[ "$fail" = 0 ] || exit 1  # stops this block before any commit below it
git add go.mod go.sum internal/geo/mmdb.go internal/geo/mmdb_test.go internal/geo/testdata
git commit -m "geo: optional GeoIP locator; lookup errors logged as a fixed category (nearest-region §6.2, §6.5)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```

---

### Task 6: Console page copy (C1, C2, C3, C5, C7, C8, C21, C24, from the record)

**Files:**
- Create: `internal/console/regioncopy.go`
- Test: `internal/console/regioncopy_test.go`

**Interfaces:**
- Consumes: the record's C1, C2, C3, C5, C7, C8, C21 and C24 (Task 0).
- Produces:
  - `var regionCopy regionCopyStrings`, with fields `PickerLabel`,
    `PickerHelp`, `AutoOption`, `Unavailable`, `StartsIn`, `CantStart`,
    `AuditCreated` and `Attribution`;
  - `func (p page) RegionCopy() regionCopyStrings`.

- [ ] **Step 1: Write the failing test**

```go
// internal/console/regioncopy_test.go
package console

import (
	"fmt"
	"reflect"
	"strings"
	"testing"
)

func TestRegionCopyHasNoEmDashesAndRendersItsVerbs(t *testing.T) {
	v := reflect.ValueOf(regionCopy)
	for i := 0; i < v.NumField(); i++ {
		s := v.Field(i).String()
		if s == "" || strings.Contains(s, "—") {
			t.Errorf("%s = %q: empty or has an em dash", v.Type().Field(i).Name, s)
		}
	}
	for _, s := range []string{
		fmt.Sprintf(regionCopy.AutoOption, "P"),
		fmt.Sprintf(regionCopy.StartsIn, "P"),
		fmt.Sprintf(regionCopy.CantStart, "P"),
		fmt.Sprintf(regionCopy.AuditCreated, "blog", "P"),
	} {
		if strings.Contains(s, "%!") {
			t.Errorf("bad verb: %q", s)
		}
	}
}
```

- [ ] **Step 2: Run it to verify it fails**

Run: `go test ./internal/console -run TestRegionCopy -count=1`
Expected: FAIL, undefined `regionCopy`.

- [ ] **Step 3: Write the strings from the record**

The values below are the drafts Task 0 submitted. Write each field from its
record entry verbatim.

```go
package console

// regionCopy is every console-page string the nearest-region feature
// shows (spec §19), as approved in the nearest-region copy record. Verbs
// are %s in the order each comment names. Change none of them without a
// new copy review.
type regionCopyStrings struct {
	PickerLabel  string // C1
	PickerHelp   string // C2
	AutoOption   string // C3: place
	Unavailable  string // C5
	StartsIn     string // C7: place
	CantStart    string // C8: place
	AuditCreated string // C21: app name, place
	Attribution  string // C24 (linked to https://db-ip.com)
}

var regionCopy = regionCopyStrings{
	PickerLabel:  "Region",
	PickerHelp:   "Where this app's instances run. We picked the region nearest to you. Choose another if most of your users are somewhere else.",
	AutoOption:   "%s (nearest to you)",
	Unavailable:  "That region can't take new apps right now. Choose another.",
	StartsIn:     "New instances start in %s, this app's region.",
	CantStart:    "New instances can't start in %s, this app's region, right now. Pass a region to place one elsewhere.",
	AuditCreated: "created app %s in %s",
	Attribution:  "IP location by DB-IP.",
}

func (p page) RegionCopy() regionCopyStrings { return regionCopy }
```

- [ ] **Step 4: Run it to verify it passes**

Run: `go test ./internal/console -run TestRegionCopy -count=1`
Expected: PASS.

- [ ] **Step 5: Commit**

```bash
git add internal/console/regioncopy.go internal/console/regioncopy_test.go
git commit -m "console: region page copy, as approved

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```

---

### Task 7: Console API messages (C16 to C20, C16b to C19b, C26 to C29, C31, reasons; from the record)

**Files:**
- Create: `internal/console/regionmsgs.go`
- Test: `internal/console/regionmsgs_test.go`

**Interfaces:**
- Consumes: the record's C16 to C20, C16b to C19b, C26 to C29, C31, and
  the four `R-*` entries (Task 0).
- Produces:

```go
func reasonText(reason string) string // absent | not-hosting | no-secrets-key | not-eligible
func msgAppRegionUnavailable(code, reason string, can []string) string // C16; C16b when can is empty
func msgAppRegionNotAllowed(code string, allowed []string) string      // C17; C17b when empty
func msgRegionUnknown(code string, choices []string) string            // C18; C18b when empty
func msgRegionNotArmed(code string, choices []string) string           // C19; C19b when empty
const msgAppRegionUnreadable                                           // C20
func msgReplicaOverlap(code string) string                             // C26
const msgPolicyUnreadable                                              // C27
func msgAppRegionUnconfirmed(code string) string                       // C28
func msgPlacementDiscrepancy(host, app, code string) string            // C29
func msgNoSecretsKey(code string) string                               // C31
```

Each list a refusal carries names only what the door would accept (codex
plan-R4 2), so it can be empty. An empty list never renders as a list: the
`b` variant drops the list sentence.

- [ ] **Step 1: Write the failing test**

```go
// internal/console/regionmsgs_test.go
package console

import (
	"strings"
	"testing"
)

func TestRegionMessagesRender(t *testing.T) {
	all := []string{
		msgAppRegionUnavailable("uk", "absent", []string{"ie", "au"}),
		msgAppRegionNotAllowed("uk", []string{"ie"}),
		msgRegionUnknown("zz", []string{"ie", "uk"}),
		msgRegionNotArmed("uk", []string{"ie"}),
		msgAppRegionUnreadable,
		msgReplicaOverlap("uk"),
		msgPolicyUnreadable,
		msgAppRegionUnconfirmed("uk"),
		msgPlacementDiscrepancy("a.go.tito.io", "blog", "uk"),
		msgNoSecretsKey("uk"),
		// codex plan-R4 2: the empty-list variants, C16b to C19b.
		msgAppRegionUnavailable("uk", "absent", nil),
		msgAppRegionNotAllowed("uk", nil),
		msgRegionUnknown("zz", nil),
		msgRegionNotArmed("uk", nil),
	}
	for _, s := range all {
		if s == "" || strings.Contains(s, "—") || strings.Contains(s, "%!") {
			t.Errorf("bad message %q", s)
		}
	}
	if !strings.Contains(all[0], reasonText("absent")) || !strings.Contains(all[0], "ie, au") {
		t.Errorf("C16 = %q, want the readable reason and the list", all[0])
	}
	for _, s := range all[len(all)-4:] {
		if strings.Contains(s, ": .") || strings.Contains(s, ": ,") {
			t.Errorf("an empty list rendered as a list: %q", s)
		}
	}
	seen := map[string]bool{}
	for _, r := range []string{"absent", "not-hosting", "no-secrets-key", "not-eligible"} {
		txt := reasonText(r)
		if txt == r || seen[txt] {
			t.Errorf("reasonText(%s) = %q: untranslated or duplicated", r, txt)
		}
		seen[txt] = true
	}
}
```

- [ ] **Step 2: Run it to verify it fails**

Run: `go test ./internal/console -run TestRegionMessagesRender -count=1`
Expected: FAIL, undefined.

- [ ] **Step 3: Write the strings from the record**

The drafts are shown. Write each from its record entry verbatim.

```go
package console

import (
	"fmt"
	"strings"
)

// The nearest-region feature's API and operator messages (spec §19), as
// approved in the nearest-region copy record. Change none of them without
// a new copy review.

func reasonText(reason string) string {
	switch reason {
	case "absent":
		return "no box runs there now"
	case "not-hosting":
		return "boxes there can't start instances"
	case "no-secrets-key":
		return "boxes there can't open this deployment's secrets"
	case "not-eligible":
		return "boxes there don't take ordinary instances"
	}
	return reason
}

// C16, or C16b when no region would pass (codex plan-R4 2: a list names
// only what the door takes, so it can be empty).
func msgAppRegionUnavailable(code, reason string, can []string) string {
	if len(can) == 0 {
		return fmt.Sprintf("this app's region %s can't take new instances right now (%s), and no other region this app may use can either. Try again later.",
			code, reasonText(reason))
	}
	return fmt.Sprintf("this app's region %s can't take new instances right now (%s). Pass a region to place this instance elsewhere. Regions that can: %s.",
		code, reasonText(reason), strings.Join(can, ", "))
}

// C17, or C17b when no allowed region would pass.
func msgAppRegionNotAllowed(code string, allowed []string) string {
	if len(allowed) == 0 {
		return fmt.Sprintf("this app's region %s is not one its instances may use, and no region they may use can take new instances right now. Try again later.", code)
	}
	return fmt.Sprintf("this app's region %s is not one its instances may use. Allowed: %s. Pass a region to choose one.",
		code, strings.Join(allowed, ", "))
}

// C18, or C18b when no region would pass.
func msgRegionUnknown(code string, choices []string) string {
	if len(choices) == 0 {
		return fmt.Sprintf("region %s is not one this platform runs in. Run the command without --region.", code)
	}
	return fmt.Sprintf("region %s is not one this platform runs in. Choose from: %s.", code, strings.Join(choices, ", "))
}

// C19, or C19b when no region would pass.
func msgRegionNotArmed(code string, choices []string) string {
	if len(choices) == 0 {
		return fmt.Sprintf("region %s can't take new apps right now. Run the command without --region.", code)
	}
	return fmt.Sprintf("region %s can't take new apps right now. Choose from: %s.", code, strings.Join(choices, ", "))
}

// C20
const msgAppRegionUnreadable = "could not read this app's region. Try again."

// C26
func msgReplicaOverlap(code string) string {
	return fmt.Sprintf("this app's region %s is also one of this instance's replica placements. Pass a region for the instance, or drop %s from replicas.", code, code)
}

// C27
const msgPolicyUnreadable = "could not read this app's instance policy, so there is no region suggestion. Try again."

// C28
func msgAppRegionUnconfirmed(code string) string {
	return fmt.Sprintf("could not confirm that this app's region %s can take new instances. Try again, or pass a region.", code)
}

// C29: an operator log line.
func msgPlacementDiscrepancy(host, app, code string) string {
	return fmt.Sprintf("possible placement discrepancy: %s of %s has no region, but %s's region is %s. It may be a deliberate home override; review it.", host, app, app, code)
}

// C31 (R9)
func msgNoSecretsKey(code string) string {
	return fmt.Sprintf("region %s can't run instances right now: its boxes don't hold this deployment's secrets key, so an instance there would start without its config or secrets. Choose another region.", code)
}
```

- [ ] **Step 4: Run it to verify it passes**

Run: `go test ./internal/console -run TestRegionMessagesRender -count=1`
Expected: PASS.

- [ ] **Step 5: Commit**

```bash
git add internal/console/regionmsgs.go internal/console/regionmsgs_test.go
git commit -m "console: region API messages, as approved

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```

---

### Task 8: Store columns, rebuild list and accessors

**Files:**
- Modify: `internal/console/store.go`
  - the ALTER list, ending at `:386`;
  - `migrateAppsKey`, at `:487-505`;
  - `CreateAppIn`, at `:1349`.
- Test: `internal/console/store_region_test.go` (create)

**Interfaces:**
- Produces:

```go
func (s *Store) CreateAppInRegion(accountID int64, app, createdBy, region, source string, now time.Time) (created bool, err error)
func (s *Store) AppRegion(accountID int64, app string) (region, source string, err error)
type nonHomeAppRegion struct{ Sqid, App, Region string; CreatedAt time.Time }
func (s *Store) NonHomeAppRegions() ([]nonHomeAppRegion, error)
```

- [ ] **Step 1: Write the failing tests**

```go
// internal/console/store_region_test.go
package console

import (
	"database/sql"
	"path/filepath"
	"testing"
	"time"

	_ "modernc.org/sqlite"
)

func TestAppRegionRoundTripAndDefaults(t *testing.T) {
	s := openTestStore(t)
	now := time.Now()
	if ok, err := s.CreateAppInRegion(OpsAccountID, "blog", "p@k.dev", "uk", "timezone", now); err != nil || !ok {
		t.Fatalf("CreateAppInRegion = %v %v", ok, err)
	}
	if r, src, err := s.AppRegion(OpsAccountID, "blog"); err != nil || r != "uk" || src != "timezone" {
		t.Fatalf("AppRegion = %q %q %v", r, src, err)
	}
	if ok, err := s.CreateAppIn(OpsAccountID, "old", "p@k.dev", now); err != nil || !ok {
		t.Fatalf("CreateAppIn = %v %v", ok, err)
	}
	if r, src, _ := s.AppRegion(OpsAccountID, "old"); r != "" || src != "" {
		t.Fatalf("CreateAppIn row = %q %q, want the home and no source", r, src)
	}
	if r, src, err := s.AppRegion(OpsAccountID, "missing"); err != nil || r != "" || src != "" {
		t.Fatalf("missing row = %q %q %v", r, src, err)
	}
	// A lost claim race writes nothing.
	if ok, _ := s.CreateAppInRegion(OpsAccountID, "blog", "x@k.dev", "au", "ip", now); ok {
		t.Fatal("a second claim of the same name succeeded")
	}
	if r, _, _ := s.AppRegion(OpsAccountID, "blog"); r != "uk" {
		t.Fatalf("a lost claim changed the region to %q", r)
	}
}

// The fixture runs NewServer, whose backfill creates the ops account row
// (sqid "bab") that NonHomeAppRegions joins on.
func TestNonHomeAppRegions(t *testing.T) {
	fx := newConsoleFixture(t)
	now := time.Now()
	if _, err := fx.st.CreateAppInRegion(OpsAccountID, "blog", "p@k.dev", "uk", "chosen", now); err != nil {
		t.Fatal(err)
	}
	if _, err := fx.st.CreateAppInRegion(OpsAccountID, "home", "p@k.dev", "", "default", now); err != nil {
		t.Fatal(err)
	}
	rows, err := fx.st.NonHomeAppRegions()
	if err != nil || len(rows) != 1 || rows[0].App != "blog" || rows[0].Region != "uk" || rows[0].Sqid != "bab" {
		t.Fatalf("NonHomeAppRegions = %+v %v", rows, err)
	}
}

// spec §5.2: a database from BEFORE migrateAppsKey whose rows already
// carry an app region keeps it through the rebuild, a reopen, a rename and
// a move to another account. An empty value would not prove the copy.
func TestRegionValuesSurviveRebuildRenameAndMove(t *testing.T) {
	path := filepath.Join(t.TempDir(), "console.db")
	db, err := sql.Open("sqlite", path)
	if err != nil {
		t.Fatal(err)
	}
	if _, err := db.Exec(`CREATE TABLE apps (
  app TEXT PRIMARY KEY, created_by TEXT NOT NULL, created_at INTEGER NOT NULL,
  account_id INTEGER NOT NULL DEFAULT 1, deleted_at INTEGER NOT NULL DEFAULT 0,
  placement TEXT NOT NULL DEFAULT '', control_plane INTEGER NOT NULL DEFAULT 0,
  build_probe TEXT NOT NULL DEFAULT '', region TEXT NOT NULL DEFAULT '',
  region_source TEXT NOT NULL DEFAULT '');
INSERT INTO apps (app, created_by, created_at, region, region_source) VALUES ('legacy', 'p@k.dev', 1, 'uk', 'timezone');`); err != nil {
		t.Fatal(err)
	}
	db.Close()
	for i := 0; i < 2; i++ { // the rebuild, then a reopen of the rebuilt table
		s, err := Open(path)
		if err != nil {
			t.Fatalf("open %d: %v", i, err)
		}
		if r, src, err := s.AppRegion(OpsAccountID, "legacy"); err != nil || r != "uk" || src != "timezone" {
			t.Fatalf("open %d: legacy = %q %q %v, want uk/timezone carried", i, r, src, err)
		}
		s.Close()
	}
	s, err := Open(path)
	if err != nil {
		t.Fatal(err)
	}
	defer s.Close()
	// Open migrates but does not backfill accounts (NewServer does), and
	// rename and move both resolve the app's account row: seed the ops
	// account the legacy row belongs to, and the move's destination.
	if _, err := s.db.Exec(`INSERT OR IGNORE INTO accounts (id, sqid, name, created_at) VALUES (?, 'bab', 'Ops', 0)`, OpsAccountID); err != nil {
		t.Fatal(err)
	}
	if err := s.RenameAppIn(OpsAccountID, "legacy", "renamed"); err != nil {
		t.Fatal(err)
	}
	if r, src, _ := s.AppRegion(OpsAccountID, "renamed"); r != "uk" || src != "timezone" {
		t.Fatalf("after rename = %q %q", r, src)
	}
	const other = 2001
	if _, err := s.db.Exec(`INSERT INTO accounts (id, sqid, name, created_at) VALUES (?, 'zzz', 'Other', 0)`, other); err != nil {
		t.Fatal(err)
	}
	if err := s.MoveApp("renamed", OpsAccountID, other); err != nil {
		t.Fatal(err)
	}
	if r, src, _ := s.AppRegion(other, "renamed"); r != "uk" || src != "timezone" {
		t.Fatalf("after move = %q %q", r, src)
	}
}
```

- [ ] **Step 2: Run them to verify they fail**

Run: `go test ./internal/console -run 'TestAppRegion|TestNonHomeAppRegions|TestRegionValuesSurvive' -count=1`
Expected: FAIL, undefined `CreateAppInRegion`.

- [ ] **Step 3: Add the columns to the ALTER list**

Directly after the `build_probe` ALTER:

```go
		// App region (nearest-region spec §5.1): the placement code new
		// instances default to, in the RECORD vocabulary — "" is the
		// deployment's home, which is also what every existing row reads, so
		// the column default IS the backfill (§5.4). region_source is a label
		// for people and logs: chosen | timezone | ip | default | "" (the row
		// predates the feature). No timezone, IP or coordinate is stored.
		`ALTER TABLE apps ADD COLUMN region TEXT NOT NULL DEFAULT '';`,
		`ALTER TABLE apps ADD COLUMN region_source TEXT NOT NULL DEFAULT '';`,
```

- [ ] **Step 4: Carry them through the rebuild**

In `migrateAppsKey`, replace the DDL and the copy:

```go
		`CREATE TABLE apps_new (
  app           TEXT NOT NULL,
  account_id    INTEGER NOT NULL DEFAULT 1,
  created_by    TEXT NOT NULL,
  created_at    INTEGER NOT NULL,
  deleted_at    INTEGER NOT NULL DEFAULT 0,
  placement     TEXT NOT NULL DEFAULT '',
  control_plane INTEGER NOT NULL DEFAULT 0,
  build_probe   TEXT NOT NULL DEFAULT '',
  region        TEXT NOT NULL DEFAULT '',
  region_source TEXT NOT NULL DEFAULT '',
  PRIMARY KEY (account_id, app)
);`,
		// The ALTERs above run before migrateAppsKey in migrate(), so every
		// column below already exists on the old table when this copies it;
		// a row already carrying an app region must not lose it
		// (nearest-region spec §5.2).
		`INSERT INTO apps_new (app, account_id, created_by, created_at, deleted_at, placement, control_plane, build_probe, region, region_source)
   SELECT app, account_id, created_by, created_at, deleted_at, placement, control_plane, build_probe, region, region_source FROM apps;`,
```

- [ ] **Step 5: Add the accessors**

```go
func (s *Store) CreateAppIn(accountID int64, app, createdBy string, now time.Time) (created bool, err error) {
	return s.CreateAppInRegion(accountID, app, createdBy, "", "", now)
}

// CreateAppInRegion claims an app name with its region in the SAME insert:
// an app region has no repair verb, so a separate best-effort write that
// failed would leave the app at the home for good, silently (spec §5.3).
func (s *Store) CreateAppInRegion(accountID int64, app, createdBy, region, source string, now time.Time) (created bool, err error) {
	res, err := s.db.Exec(`INSERT INTO apps (app, account_id, created_by, created_at, region, region_source)
VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT(account_id, app) DO NOTHING`,
		app, accountID, createdBy, now.Unix(), region, source)
	if err != nil {
		return false, err
	}
	n, err := res.RowsAffected()
	return n > 0, err
}

// AppRegion reads an app's region and its source label. A missing row
// reads as the home with no source.
func (s *Store) AppRegion(accountID int64, app string) (region, source string, err error) {
	err = s.db.QueryRow(`SELECT region, region_source FROM apps WHERE account_id = ? AND app = ?`, accountID, app).
		Scan(&region, &source)
	if errors.Is(err, sql.ErrNoRows) {
		return "", "", nil
	}
	return region, source, err
}

// nonHomeAppRegion is one live app holding a non-home region, for the
// roll-forward discrepancy log (spec §5.7).
type nonHomeAppRegion struct {
	Sqid, App, Region string
	CreatedAt         time.Time
}

func (s *Store) NonHomeAppRegions() ([]nonHomeAppRegion, error) {
	rows, err := s.db.Query(`SELECT a.sqid, p.app, p.region, p.created_at FROM apps p
JOIN accounts a ON a.id = p.account_id
WHERE p.region != '' AND p.deleted_at = 0 ORDER BY a.sqid, p.app`)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var out []nonHomeAppRegion
	for rows.Next() {
		var r nonHomeAppRegion
		var created int64
		if err := rows.Scan(&r.Sqid, &r.App, &r.Region, &created); err != nil {
			return nil, err
		}
		r.CreatedAt = time.Unix(created, 0)
		out = append(out, r)
	}
	return out, rows.Err()
}
```

- [ ] **Step 6: Run the tests to verify they pass**

Run: `go test ./internal/console -count=1`
Expected: PASS, the whole package.

- [ ] **Step 7: Commit**

```bash
git add internal/console/store.go internal/console/store_region_test.go
git commit -m "console store: app region columns carried through the apps-key rebuild (nearest-region §5)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```

---

### Task 9: Console config, the switch, the expected secrets fingerprint and the test seams

**Files:**
- Create: `internal/console/regionconfig.go`, `internal/console/regionconfig_test.go`
- Modify: `internal/console/server.go`
  - `Config`, near `HomeRegion` at `:477-484`;
  - the `Server` struct;
  - `NewServer`, whose last line before `return` calls `logRegionConfig`.
- Modify: `cmd/console/main.go:278`
- Test: `cmd/console/region_test.go` (create, `package main`)

**Interfaces:**
- Consumes: `geo.Locator` and `geo.OpenMMDB` (Task 5),
  `appconfig.PublicFingerprint` (Task 1b), and the record's O1 to O4.
- Produces:
  - `Config.AppRegions bool` and `Config.GeoIP geo.Locator`;
  - `Server.appRegionOf func(accountID int64, app string) (string, string, error)`,
    a test seam, nil in production;
  - `Server.beforeClaimInsert func()`, a test seam, nil in production;
  - the test helper `mustTestSecretsKey() (pubB64, fp string)`, in
    `regionconfig_test.go`, used by every later R9 test;
  - these methods and helpers:

```go
func (s *Server) appRegionsOn() bool
func (s *Server) expectedSecretsFP() string // "" = cannot judge (R9)
func (s *Server) appRegion(accountID int64, app string) (string, string, error)
func logRegionConfig(cfg Config)
func openGeoIP(path string) geo.Locator // cmd/console
func appRegionsSwitch(v string) bool    // cmd/console
```

- [ ] **Step 1: Write the failing tests**

```go
// internal/console/regionconfig_test.go
package console

import (
	"bytes"
	"crypto/ecdh"
	"encoding/base64"
	"log"
	"strings"
	"testing"

	"github.com/carlosframework/platform/internal/appconfig"
)

// mustTestSecretsKey is the fixed P-256 key every R9 test in this package
// uses: its public half as CARLOS_SECRETS_PUBKEY carries it, and the
// fingerprint a box holding it publishes.
func mustTestSecretsKey() (pubB64, fp string) {
	priv, err := ecdh.P256().NewPrivateKey(bytes.Repeat([]byte{1}, 32))
	if err != nil {
		panic(err)
	}
	return base64.StdEncoding.EncodeToString(priv.PublicKey().Bytes()), appconfig.KeyFingerprint(priv.PublicKey())
}

func TestAppRegionsOnNeedsANamedHome(t *testing.T) {
	fx := newConsoleFixture(t)
	for _, c := range []struct {
		on   bool
		home string
		want bool
	}{{false, "ie", false}, {true, "", false}, {true, "ie", true}} {
		fx.srv.cfg.AppRegions, fx.srv.cfg.HomeRegion = c.on, c.home
		if got := fx.srv.appRegionsOn(); got != c.want {
			t.Errorf("AppRegions=%v HomeRegion=%q: appRegionsOn = %v, want %v", c.on, c.home, got, c.want)
		}
	}
}

func TestExpectedSecretsFP(t *testing.T) {
	fx := newConsoleFixture(t)
	pub, fp := mustTestSecretsKey()
	fx.srv.cfg.SecretsPub = pub
	if got := fx.srv.expectedSecretsFP(); got != fp {
		t.Fatalf("expectedSecretsFP = %q, want %q", got, fp)
	}
	fx.srv.cfg.SecretsPub = ""
	if got := fx.srv.expectedSecretsFP(); got != "" {
		t.Fatalf("no CARLOS_SECRETS_PUBKEY = %q, want empty (cannot judge)", got)
	}
}

func TestLogRegionConfig(t *testing.T) {
	var buf bytes.Buffer
	prev := log.Writer()
	log.SetOutput(&buf)
	defer log.SetOutput(prev)
	logRegionConfig(Config{AppRegions: true})
	out := buf.String()
	if !strings.Contains(out, "CONSOLE_HOME_REGION is unset") || !strings.Contains(out, "CARLOS_SECRETS_PUBKEY is unset") {
		t.Fatalf("missing startup lines: %s", out)
	}
	// Switch off: the guess endpoint still answers, so a missing secrets
	// key is still worth saying (codex plan-R2 8); an unnamed home is not.
	buf.Reset()
	logRegionConfig(Config{})
	if out := buf.String(); !strings.Contains(out, "CARLOS_SECRETS_PUBKEY is unset") || strings.Contains(out, "CONSOLE_HOME_REGION") {
		t.Fatalf("switch off logged %q, want only the secrets-key line", out)
	}
	buf.Reset()
	pub, _ := mustTestSecretsKey()
	logRegionConfig(Config{AppRegions: true, HomeRegion: "ie", SecretsPub: pub})
	if buf.Len() != 0 {
		t.Fatalf("a complete configuration logged: %s", buf.String())
	}
}
```

The two `strings.Contains` fragments are substrings of O1 and O2. Use
fragments of the approved text if review changed them.

Create `cmd/console/region_test.go`. It must be `package main`:
`main_test.go` is the external `package main_test` and cannot reach the
unexported helpers.

```go
package main

import (
	"path/filepath"
	"testing"
)

func TestOpenGeoIPIsOptionalAndNeverFatal(t *testing.T) {
	if openGeoIP("") != nil {
		t.Fatal("unset CONSOLE_GEOIP_DB must mean no locator")
	}
	if openGeoIP(filepath.Join(t.TempDir(), "missing.mmdb")) != nil {
		t.Fatal("an unopenable file must mean no locator, not a crash")
	}
}

func TestAppRegionsSwitchParses(t *testing.T) {
	for v, want := range map[string]bool{"on": true, "ON": true, " on ": true, "": false, "off": false, "true": false} {
		if got := appRegionsSwitch(v); got != want {
			t.Errorf("appRegionsSwitch(%q) = %v, want %v", v, got, want)
		}
	}
}
```

- [ ] **Step 2: Run them to verify they fail**

Run: `go test ./internal/console -run 'TestAppRegionsOn|TestExpectedSecretsFP|TestLogRegionConfig' -count=1; go test ./cmd/console -run 'OpenGeoIP|AppRegionsSwitch' -count=1`
Expected: FAIL, undefined.

- [ ] **Step 3: Add the Config fields and the Server seams**

In `Config`, after `HomeRegion`:

```go
	// AppRegions is CONSOLE_APP_REGIONS=on (nearest-region spec §5.6). Off
	// never stops the create door's fill for rows already holding a region.
	AppRegions bool
	// GeoIP is the optional CONSOLE_GEOIP_DB locator; nil = IP step off.
	GeoIP geo.Locator
```

In the `Server` struct:

```go
	// appRegionOf and beforeClaimInsert are TEST SEAMS (nil in
	// production): a store failure on the app-region read (C20), and a
	// competing insert landing after the region was resolved.
	appRegionOf       func(accountID int64, app string) (string, string, error)
	beforeClaimInsert func()
```

Import `"github.com/carlosframework/platform/internal/geo"`.

- [ ] **Step 4: Implement `regionconfig.go`**

```go
package console

import (
	"log"

	"github.com/carlosframework/platform/internal/appconfig"
)

// appRegionsOn is spec §5.6: the switch AND a named home.
func (s *Server) appRegionsOn() bool {
	return s.cfg.AppRegions && s.cfg.HomeRegion != ""
}

// expectedSecretsFP is the fingerprint a box must publish to receive
// placed instances (spec R9): the console's own CARLOS_SECRETS_PUBKEY,
// the key the CLI seals config to. "" means the console cannot judge, and
// then no coded region is eligible for automatic placement.
func (s *Server) expectedSecretsFP() string {
	if s.cfg.SecretsPub == "" {
		return ""
	}
	fp, err := appconfig.PublicFingerprint(s.cfg.SecretsPub)
	if err != nil {
		return ""
	}
	return fp
}

// appRegion reads the app's stored region through the test seam when one
// is set.
func (s *Server) appRegion(accountID int64, app string) (string, string, error) {
	if s.appRegionOf != nil {
		return s.appRegionOf(accountID, app)
	}
	return s.store.AppRegion(accountID, app)
}

// logRegionConfig names, once at startup, each configuration that leaves
// part of the feature off. The secrets-key line does not depend on the
// switch: the guess endpoint answers with the switch off too (R9).
func logRegionConfig(cfg Config) {
	if cfg.AppRegions && cfg.HomeRegion == "" {
		log.Print("console: CONSOLE_APP_REGIONS=on but CONSOLE_HOME_REGION is unset, so app regions stay off until the home is named")
	}
	if cfg.SecretsPub == "" {
		log.Print("console: CARLOS_SECRETS_PUBKEY is unset, so no region outside the home can be chosen automatically")
	}
}
```

The two log texts are O1 and O2 from the record. In `NewServer`, call
`logRegionConfig(cfg)` immediately before its successful `return`.

- [ ] **Step 5: Wire the environment in `cmd/console/main.go`**

Beside `HomeRegion:`:

```go
		AppRegions:                appRegionsSwitch(os.Getenv("CONSOLE_APP_REGIONS")),
		GeoIP:                     openGeoIP(strings.TrimSpace(os.Getenv("CONSOLE_GEOIP_DB"))),
```

Near `envOr`:

```go
// appRegionsSwitch reads CONSOLE_APP_REGIONS: exactly "on" (any case,
// trimmed) turns app regions on.
func appRegionsSwitch(v string) bool { return strings.EqualFold(strings.TrimSpace(v), "on") }

// openGeoIP opens the optional CONSOLE_GEOIP_DB. Unset, unopenable or not
// an mmdb means the IP step is off — logged once, never fatal. Returns an
// untyped nil on every failure, never a nil *geo.MMDB in the interface.
func openGeoIP(path string) geo.Locator {
	if path == "" {
		return nil
	}
	m, err := geo.OpenMMDB(path)
	if err != nil {
		log.Printf("console: CONSOLE_GEOIP_DB %s is unusable, so the IP step of region guesses is off: %v", path, err)
		return nil
	}
	log.Printf("console: GeoIP database loaded from %s", path)
	return m
}
```

The two log texts are O3 and O4. The open error describes the file, never a
client.

- [ ] **Step 6: Run the tests to verify they pass**

Run: `go test ./internal/console -run 'TestAppRegionsOn|TestExpectedSecretsFP|TestLogRegionConfig' -count=1 && go test ./cmd/console -count=1`
Expected: PASS.

- [ ] **Step 7: Commit**

```bash
git add internal/console/server.go internal/console/regionconfig.go internal/console/regionconfig_test.go cmd/console/main.go cmd/console/region_test.go
git commit -m "console: app-regions switch, expected secrets fingerprint, GeoIP, test seams (nearest-region §5.6, §6.2, R9)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```

---

### Task 10: Strict edges read, candidates, eligibility (incl. R9) and policy

**Files:**
- Modify: `internal/console/instancesplacement.go`
  - `edgesFilePlacementView`, at `:35-55`;
  - beside `readEdgesFiles`, at `:83`.
- Create: `internal/console/regioncandidates.go`
- Test: `internal/console/regioncandidates_test.go`

**Interfaces:**
- Consumes:
  - `provision.ValidRegion` and `provision.GetManifest`;
  - `s.appPlacement` (`fleets.go:479`) and `openRegistry`;
  - `instanceRegionAllowed` (`apiinstances.go:1797`);
  - `geo.LookupAWSRegion` (Task 4);
  - `s.expectedSecretsFP` (Task 9);
  - the record's O6 and O7.
- Produces:

```go
type edgesSnapshot struct {
	Files    []edgesFilePlacementView
	Complete bool // false when any listed file could not be read or parsed
}
func readEdgesSnapshot(ctx context.Context, s release.Store) (edgesSnapshot, error) // error only when the LISTING fails
type regionCandidate struct {
	Code      string // record vocabulary; "" is the home
	Home      bool
	AWSRegion string
	Reachable, Hosting, KeyOK, Eligible bool
	NoKeyProven bool // R9: current writers only, and none holds the key
}
func regionCandidates(snap edgesSnapshot, homeName, expectedFP string) []regionCandidate
func (s *Server) regionCands(snap edgesSnapshot) []regionCandidate
func (s *Server) candsOrHome(ctx context.Context) []regionCandidate // a fresh snapshot, or the home alone
func homeOnlyCandidates() []regionCandidate
func (c regionCandidate) unavailableReason() string // "", absent, not-hosting, no-secrets-key, not-eligible
func findCandidate(cands []regionCandidate, code string) (regionCandidate, bool)
func eligibleCount(cands []regionCandidate) int
type policyState int // policyAbsent, policyKnown, policyUnreadable
type regionPolicy struct{ State policyState; M *provision.Manifest; Placed string }
func (s *Server) appPolicy(ctx context.Context, acct *Account, app string) regionPolicy
func (p regionPolicy) pooled() bool
func (p regionPolicy) allows(code string) bool
```

Test helpers produced for later console tasks, in
`regioncandidates_test.go`:

- `testKeyPub` and `testKeyFP`, from Task 9's `mustTestSecretsKey`;
- `edgeJSON`;
- the fixture edges `homeBox`, `ukEligible`, `ukNoKey`, `ukWrongKey`,
  `ukOldWriter`, `auPoolOnly`, `usNoInbound`, `nzNoHosting`, `jpNoEIP` and
  `fleetBoxFile`;
- `splitCapability`, the same-region box pairs no single box of which is
  eligible;
- `setEdges`, which creates or replaces, unlike `putEdges`;
- `failingList` and `captureLog`.

- [ ] **Step 1: Write the failing tests and the shared helpers**

```go
// internal/console/regioncandidates_test.go
package console

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"io"
	"log"
	"os"
	"strings"
	"testing"

	"github.com/carlosframework/platform/internal/provision"
	"github.com/carlosframework/platform/internal/release"
)

// mustTestSecretsKey is Task 9's (regionconfig_test.go).
var testKeyPub, testKeyFP = mustTestSecretsKey()

func bp(b bool) *bool { return &b }

type edgeOpts struct {
	hib, shared, inbound *bool // nil = the writer predates the field
	fp                   string
}

var eligibleBox = edgeOpts{hib: bp(true), shared: bp(true), inbound: bp(true), fp: testKeyFP}

// edgeJSON writes one box's edges file the way internal/host does.
func edgeJSON(box, eip, awsRegion, placement string, o edgeOpts) string {
	m := map[string]any{"v": 1, "box": box, "eip": eip, "region": awsRegion, "hosts": map[string]any{}}
	if placement != "" {
		m["placement"] = placement
	}
	if o.hib != nil {
		m["hibernation_configured"] = *o.hib
	}
	if o.shared != nil {
		m["serves_shared_pool"] = *o.shared
	}
	if o.inbound != nil {
		m["accepts_inbound"] = *o.inbound
	}
	if o.fp != "" {
		m["secrets_key_fp"] = o.fp
	}
	b, _ := json.Marshal(m)
	return string(b)
}

var (
	homeBox      = edgeJSON("ie-box", "5.6.7.8", "eu-west-1", "", eligibleBox)
	ukEligible   = edgeJSON("uk-box", "1.2.3.4", "eu-west-2", "uk", eligibleBox)
	ukNoKey      = edgeJSON("uk-box", "1.2.3.4", "eu-west-2", "uk", edgeOpts{hib: bp(true), shared: bp(true), inbound: bp(true)})
	ukWrongKey   = edgeJSON("uk-box", "1.2.3.4", "eu-west-2", "uk", edgeOpts{hib: bp(true), shared: bp(true), inbound: bp(true), fp: "sha256:" + strings.Repeat("0", 64)})
	ukOldWriter  = edgeJSON("uk-box", "1.2.3.4", "eu-west-2", "uk", edgeOpts{})
	auPoolOnly   = edgeJSON("au-box", "9.9.9.9", "ap-southeast-2", "au", edgeOpts{hib: bp(true), shared: bp(false), inbound: bp(true), fp: testKeyFP})
	usNoInbound  = edgeJSON("us-box", "8.8.4.4", "us-east-1", "us", edgeOpts{hib: bp(true), shared: bp(true), inbound: bp(false), fp: testKeyFP})
	nzNoHosting  = edgeJSON("nz-box", "6.6.6.6", "ap-southeast-6", "nz", edgeOpts{hib: bp(false), shared: bp(true), inbound: bp(true), fp: testKeyFP})
	jpNoEIP      = edgeJSON("jp-box", "", "ap-northeast-1", "jp", eligibleBox)
	fleetBoxFile = edgeJSON("fl-box", "4.4.4.4", "eu-west-2", "fleet/acme", eligibleBox)
)

// setEdges creates OR replaces edges files. putEdges is create-only (an
// empty ifMatch on DirStore and S3), so it cannot change a box's file.
func setEdges(t *testing.T, fx *consoleFixture, files map[string]string) {
	t.Helper()
	ctx := context.Background()
	for name, body := range files {
		key := "control/edges/" + name + ".json"
		etag := ""
		if _, cur, err := fx.rel.Get(ctx, key); err == nil {
			etag = cur
		} else if !errors.Is(err, os.ErrNotExist) {
			t.Fatal(err)
		}
		if _, err := fx.rel.Put(ctx, key, []byte(body), etag); err != nil {
			t.Fatalf("set %s: %v", key, err)
		}
	}
}

type failingList struct{ release.Store }

func (failingList) List(context.Context, string) ([]string, error) {
	return nil, errors.New("bucket unreachable")
}

func captureLog(w io.Writer) (restore func()) {
	prev, flags := log.Writer(), log.Flags()
	log.SetOutput(w)
	return func() { log.SetOutput(prev); log.SetFlags(flags) }
}

// views is a COMPLETE snapshot of the given edges-file bodies.
func views(t *testing.T, bodies ...string) edgesSnapshot {
	t.Helper()
	snap := edgesSnapshot{Complete: true}
	for _, b := range bodies {
		var f edgesFilePlacementView
		if err := json.Unmarshal([]byte(b), &f); err != nil {
			t.Fatal(err)
		}
		snap.Files = append(snap.Files, f)
	}
	return snap
}

func TestRegionCandidatesNeedPositiveEligibility(t *testing.T) {
	ca := edgeJSON("ca-box", "7.7.7.7", "ca-central-1", "ca", edgeOpts{})
	cands := regionCandidates(views(t, homeBox, ukEligible, auPoolOnly, usNoInbound, ca, nzNoHosting, jpNoEIP, fleetBoxFile), "ie", testKeyFP)
	want := map[string]string{"": "", "uk": "", "au": "not-eligible", "us": "not-eligible",
		"ca": "no-secrets-key", "nz": "not-hosting", "jp": "absent"}
	if len(cands) != len(want) {
		t.Fatalf("candidates = %+v, want exactly %v (no fleet code)", cands, want)
	}
	for code, reason := range want {
		c, ok := findCandidate(cands, code)
		if !ok {
			t.Fatalf("no candidate %q", code)
		}
		if got := c.unavailableReason(); got != reason {
			t.Errorf("%q: reason %q, want %q", code, got, reason)
		}
	}
	if h, _ := findCandidate(cands, ""); !h.Home || h.AWSRegion != "eu-west-1" || !h.Eligible {
		t.Errorf("home = %+v", h)
	}
}

// R9: a box must hold the deployment's secrets key to receive placed
// instances, and only a CURRENT writer can prove it does not.
func TestRegionCandidatesSecretsKey(t *testing.T) {
	cases := []struct {
		name        string
		body        string
		expected    string
		reason      string
		noKeyProven bool
	}{
		{"matching key", ukEligible, testKeyFP, "", false},
		{"no key, current writer", ukNoKey, testKeyFP, "no-secrets-key", true},
		{"wrong key, current writer", ukWrongKey, testKeyFP, "no-secrets-key", true},
		{"old writer: unknown", ukOldWriter, testKeyFP, "no-secrets-key", false},
		{"console cannot judge", ukEligible, "", "no-secrets-key", false},
	}
	for _, c := range cases {
		cand, _ := findCandidate(regionCandidates(views(t, homeBox, c.body), "ie", c.expected), "uk")
		if got := cand.unavailableReason(); got != c.reason || cand.NoKeyProven != c.noKeyProven {
			t.Errorf("%s: reason %q proven %v, want %q %v", c.name, got, cand.NoKeyProven, c.reason, c.noKeyProven)
		}
	}
	// One box with the key and one without: the region is not PROVEN
	// keyless, and is eligible through the box that holds it.
	ukKeyless2 := edgeJSON("uk-box-2", "1.2.3.5", "eu-west-2", "uk", edgeOpts{hib: bp(true), shared: bp(true), inbound: bp(true)})
	cand, _ := findCandidate(regionCandidates(views(t, homeBox, ukEligible, ukKeyless2), "ie", testKeyFP), "uk")
	if !cand.Eligible || cand.NoKeyProven {
		t.Errorf("mixed region = %+v", cand)
	}
}

// codex plan-R2 1: a negative proof needs EVERY box. With one file
// unreadable or malformed, a readable keyless box proves nothing.
func TestNoKeyProvenNeedsACompleteRead(t *testing.T) {
	incomplete := views(t, homeBox, ukNoKey)
	incomplete.Complete = false
	if c, _ := findCandidate(regionCandidates(incomplete, "ie", testKeyFP), "uk"); c.NoKeyProven {
		t.Fatal("an incomplete read proved a region keyless")
	}
	ctx := context.Background()
	for name, extra := range map[string]string{"malformed": "{not json", "unversioned": `{"box":"uk-box-2"}`} {
		s := release.NewDirStore(t.TempDir())
		for key, body := range map[string]string{"ie-box": homeBox, "uk-box": ukNoKey, "uk-box-2": extra} {
			if _, err := s.Put(ctx, "control/edges/"+key+".json", []byte(body), ""); err != nil {
				t.Fatal(err)
			}
		}
		snap, err := readEdgesSnapshot(ctx, s)
		if err != nil || snap.Complete {
			t.Fatalf("%s: snapshot complete=%v err=%v, want incomplete", name, snap.Complete, err)
		}
		if c, _ := findCandidate(regionCandidates(snap, "ie", testKeyFP), "uk"); c.NoKeyProven {
			t.Fatalf("%s: proved keyless with a box unassessed", name)
		}
	}
	s := release.NewDirStore(t.TempDir())
	for key, body := range map[string]string{"ie-box": homeBox, "uk-box": ukNoKey, "uk-box-2": ukEligible} {
		if _, err := s.Put(ctx, "control/edges/"+key+".json", []byte(body), ""); err != nil {
			t.Fatal(err)
		}
	}
	snap, err := readEdgesSnapshot(ctx, failingGet{Store: s, key: "control/edges/uk-box-2.json"})
	if err != nil || snap.Complete {
		t.Fatalf("failed GET: complete=%v err=%v, want incomplete", snap.Complete, err)
	}
	if c, _ := findCandidate(regionCandidates(snap, "ie", testKeyFP), "uk"); c.NoKeyProven {
		t.Fatal("a failed GET let the readable keyless box prove the region keyless")
	}
}

type failingGet struct {
	release.Store
	key string
}

func (f failingGet) Get(ctx context.Context, key string) ([]byte, string, error) {
	if key == f.key {
		return nil, "", errors.New("read timeout")
	}
	return f.Store.Get(ctx, key)
}

// codex plan-R2 2: a candidate's coordinates come from its ELIGIBLE boxes
// only; ties among them go to the lexically smallest AWS region.
func TestCandidateLocationComesFromEligibleBoxes(t *testing.T) {
	syd1 := edgeJSON("uk-syd-1", "2.2.2.1", "ap-southeast-2", "uk", edgeOpts{hib: bp(true), shared: bp(true), inbound: bp(true)})
	syd2 := edgeJSON("uk-syd-2", "2.2.2.2", "ap-southeast-2", "uk", edgeOpts{hib: bp(true), shared: bp(true), inbound: bp(false), fp: testKeyFP})
	c, _ := findCandidate(regionCandidates(views(t, homeBox, ukEligible, syd1, syd2), "ie", testKeyFP), "uk")
	if !c.Eligible || c.AWSRegion != "eu-west-2" {
		t.Fatalf("mixed eligibility located uk at %q, want the eligible box's eu-west-2", c.AWSRegion)
	}
	dub := edgeJSON("uk-dub", "2.2.2.3", "eu-west-1", "uk", eligibleBox)
	if c, _ := findCandidate(regionCandidates(views(t, homeBox, ukEligible, dub), "ie", testKeyFP), "uk"); c.AWSRegion != "eu-west-1" {
		t.Fatalf("eligible tie = %q, want the lexically smallest eu-west-1", c.AWSRegion)
	}
}

func TestRegionCandidatesFoldTheHomesName(t *testing.T) {
	coded := edgeJSON("ie2", "5.6.7.9", "eu-west-1", "ie", eligibleBox)
	cands := regionCandidates(views(t, homeBox, coded), "ie", testKeyFP)
	if len(cands) != 1 || !cands[0].Home {
		t.Fatalf("candidates = %+v, want the home alone", cands)
	}
}

// spec §6.3, §6.5: a candidate with no point is logged once per code, and
// the line carries no caller coordinates (there are none to carry).
func TestRegionCandidatesLogAMissingPointOnce(t *testing.T) {
	var buf bytes.Buffer
	restore := captureLog(&buf)
	defer restore()
	odd := edgeJSON("odd-box", "3.3.3.3", "xx-nowhere-1", "odd", eligibleBox)
	regionCandidates(views(t, homeBox, odd), "ie", testKeyFP)
	regionCandidates(views(t, homeBox, odd), "ie", testKeyFP)
	if n := strings.Count(buf.String(), `region "odd"`); n != 1 {
		t.Fatalf("missing-point line logged %d times, want once: %s", n, buf.String())
	}
}

func TestReadEdgesSnapshotReportsListFailure(t *testing.T) {
	if _, err := readEdgesSnapshot(context.Background(), failingList{}); err == nil {
		t.Fatal("a failed listing must be an error, not an empty answer (codex R2-2)")
	}
	snap, err := readEdgesSnapshot(context.Background(), release.NewDirStore(t.TempDir()))
	if err != nil || len(snap.Files) != 0 || !snap.Complete {
		t.Fatalf("an empty prefix = %+v %v, want complete and empty", snap, err)
	}
}

func TestAppPolicyStates(t *testing.T) {
	fx, _ := instanceFixture(t, "code-policy")
	acct := &Account{ID: OpsAccountID, Sqid: "bab"}
	ctx := context.Background()
	if p := fx.srv.appPolicy(ctx, acct, "titogo"); p.State != policyAbsent || !p.allows("uk") {
		t.Fatalf("no manifest = %+v", p)
	}
	m := titoManifest()
	m.Pool, m.Regions = "", []string{"uk"}
	enableInstances(t, fx, "titogo", m)
	if p := fx.srv.appPolicy(ctx, acct, "titogo"); p.State != policyKnown || p.allows("") || !p.allows("uk") || p.pooled() {
		t.Fatalf("known = %+v", p)
	}
	m.Pool = "tito"
	enableInstances(t, fx, "titogo", m)
	if !fx.srv.appPolicy(ctx, acct, "titogo").pooled() {
		t.Fatal("a pooled manifest must read as pooled")
	}
	if err := fx.st.SetAppPlacement(OpsAccountID, "titogo", "fleet/acme"); err != nil {
		t.Fatal(err)
	}
	if p := fx.srv.appPolicy(ctx, acct, "titogo"); p.Placed != "fleet/acme" {
		t.Fatalf("placed = %q", p.Placed)
	}
	_, etag, err := fx.rel.Get(ctx, provision.ManifestKey("bab", "titogo"))
	if err != nil {
		t.Fatal(err)
	}
	if _, err := fx.rel.Put(ctx, provision.ManifestKey("bab", "titogo"), []byte("{not json"), etag); err != nil {
		t.Fatal(err)
	}
	if p := fx.srv.appPolicy(ctx, acct, "titogo"); p.State != policyUnreadable || p.allows("") {
		t.Fatalf("unreadable = %+v, want unreadable and nothing allowed", p)
	}
}

// splitCapability pairs two boxes in one region that between them publish
// every eligibility fact while neither publishes them all (codex
// plan-R3 4). Task 11 reuses it for the guess.
var splitCapability = map[string][2]string{
	"keyless ordinary box beside a keyed named-pool-only box": {
		edgeJSON("uk-a", "1.2.3.4", "eu-west-2", "uk", edgeOpts{hib: bp(true), shared: bp(true), inbound: bp(true)}),
		edgeJSON("uk-b", "1.2.3.5", "eu-west-2", "uk", edgeOpts{hib: bp(true), shared: bp(false), inbound: bp(true), fp: testKeyFP}),
	},
	"keyed no-inbound box beside a keyless inbound box": {
		edgeJSON("uk-a", "1.2.3.4", "eu-west-2", "uk", edgeOpts{hib: bp(true), shared: bp(true), inbound: bp(false), fp: testKeyFP}),
		edgeJSON("uk-b", "1.2.3.5", "eu-west-2", "uk", edgeOpts{hib: bp(true), shared: bp(true), inbound: bp(true)}),
	},
	"keyed non-hosting box beside a keyless hosting box": {
		edgeJSON("uk-a", "1.2.3.4", "eu-west-2", "uk", edgeOpts{hib: bp(false), shared: bp(true), inbound: bp(true), fp: testKeyFP}),
		edgeJSON("uk-b", "1.2.3.5", "eu-west-2", "uk", edgeOpts{hib: bp(true), shared: bp(true), inbound: bp(true)}),
	},
}

// Eligibility is ONE box's facts, never the region's union of them.
func TestEligibilityNeverCombinesBoxes(t *testing.T) {
	for name, boxes := range splitCapability {
		cands := regionCandidates(views(t, homeBox, boxes[0], boxes[1]), "ie", testKeyFP)
		if c, _ := findCandidate(cands, "uk"); c.Eligible || c.unavailableReason() == "" {
			t.Errorf("%s: uk = %+v, want ineligible", name, c)
		}
		if n := eligibleCount(cands); n != 1 {
			t.Errorf("%s: %d eligible candidates, want the home alone", name, n)
		}
	}
}
```

Task 1's `TestEligibilityPairsWithTheReconcileRefusal` pairs these with the
reconciler: the facts a box publishes are exactly what its reconcile pass
mints, so a region is offered only through one box that would mint the
record.

`enableInstances` writes through `provision.PutManifest`, which replaces with
the current etag (`provision.go:775`), so calling it twice is safe.

- [ ] **Step 2: Run them to verify they fail**

Run: `go test ./internal/console -run 'TestRegionCandidates|TestReadEdgesSnapshot|TestAppPolicyStates|TestNoKeyProven|TestCandidateLocation|TestEligibilityNeverCombinesBoxes' -count=1`
Expected: FAIL, undefined.

- [ ] **Step 3: Extend the view and add the strict read**

In `edgesFilePlacementView`, after `HibernationConfigured`:

```go
	// ServesSharedPool and AcceptsInbound: POINTERS, absent = a writer
	// predating them (unknown). SecretsKeyFP: absent = no key, or a writer
	// predating it; AcceptsInbound's presence tells the two apart
	// (nearest-region spec §6.1, R9).
	ServesSharedPool *bool `json:"serves_shared_pool"`
	AcceptsInbound   *bool `json:"accepts_inbound"`
	SecretsKeyFP     string `json:"secrets_key_fp"`
```

Make `readEdgesFiles` a wrapper over the strict read:

```go
func readEdgesFiles(ctx context.Context, s release.Store) []edgesFilePlacementView {
	if s == nil {
		return nil
	}
	snap, err := readEdgesSnapshot(ctx, s)
	if err != nil {
		log.Printf("console: api instances: could not list %s for the placement join: %v", edgesFilePrefix, err)
		return nil
	}
	return snap.Files
}

// edgesSnapshot is one read of control/edges/. Complete is false when any
// listed file could not be read or parsed: positive facts about the boxes
// that WERE read still hold, but nothing may be concluded about "every
// box" (R9's NoKeyProven, codex plan-R2 1).
type edgesSnapshot struct {
	Files    []edgesFilePlacementView
	Complete bool
}

// readEdgesSnapshot REPORTS a failed listing instead of answering empty:
// the door must tell "could not read" (503, C28) from "no box publishes
// this code" (codex R2-2). A single unreadable or malformed file is
// skipped, as before, and marks the snapshot incomplete.
func readEdgesSnapshot(ctx context.Context, s release.Store) (edgesSnapshot, error) {
	keys, err := s.List(ctx, edgesFilePrefix)
	if err != nil {
		return edgesSnapshot{}, err
	}
	snap := edgesSnapshot{Complete: true}
	for _, key := range keys {
		name, ok := strings.CutPrefix(key, edgesFilePrefix)
		if !ok || !strings.HasSuffix(name, ".json") || strings.Contains(name, "/") {
			continue
		}
		body, _, err := s.Get(ctx, key)
		if err != nil {
			log.Printf("console: api instances: could not read %s for the placement join: %v", key, err)
			snap.Complete = false
			continue
		}
		var f edgesFilePlacementView
		if err := json.Unmarshal(body, &f); err != nil || f.V < 1 || f.Box == "" {
			log.Printf("console: api instances: skipping %s in the placement join: not a versioned edges file", key)
			snap.Complete = false
			continue
		}
		snap.Files = append(snap.Files, f)
	}
	return snap, nil
}
```

- [ ] **Step 4: Implement `regioncandidates.go`**

```go
package console

import (
	"context"
	"errors"
	"log"
	"os"
	"sort"
	"sync"

	"github.com/carlosframework/platform/internal/geo"
	"github.com/carlosframework/platform/internal/provision"
)

// disagreeSaid and noPointSaid throttle two diagnostics to once per code.
var disagreeSaid, noPointSaid sync.Map

// regionCandidate is one region an automatic choice may land in (spec
// §6.1). Code is in the record vocabulary: "" is the home.
type regionCandidate struct {
	Code        string
	Home        bool
	AWSRegion   string
	Reachable   bool // some box publishing the code has a dial address
	Hosting     bool // ... and its activator configured (unknown counts, as in placements)
	KeyOK       bool // ... and holds the deployment's secrets key (R9)
	Eligible    bool // ONE box has all of it, every fact POSITIVELY true
	NoKeyProven bool // R9: every reachable hosting box is a current writer, and none holds the key
}

// unavailableReason is "" for an eligible candidate, else the first bar it
// fails (the C16 vocabulary).
func (c regionCandidate) unavailableReason() string {
	switch {
	case c.Eligible:
		return ""
	case !c.Reachable:
		return "absent"
	case !c.Hosting:
		return "not-hosting"
	case !c.KeyOK:
		return "no-secrets-key"
	}
	return "not-eligible"
}

func isTrue(b *bool) bool { return b != nil && *b }

// regionCandidates folds an edges snapshot into candidates. expectedFP is
// the console's own secrets fingerprint; "" means it cannot judge, and then
// no coded region is KeyOK. The home is exempt from every eligibility fact.
// A coded candidate's AWS region comes only from its ELIGIBLE boxes (codex
// plan-R2 2); NoKeyProven needs a complete snapshot (codex plan-R2 1).
func regionCandidates(snap edgesSnapshot, homeName, expectedFP string) []regionCandidate {
	type acc struct {
		c                       regionCandidate
		regions                 map[string]int
		hostingBoxes            int
		unknownWriter, keyFound bool
	}
	by := map[string]*acc{}
	get := func(code string, home bool) *acc {
		a, ok := by[code]
		if !ok {
			a = &acc{c: regionCandidate{Code: code, Home: home}, regions: map[string]int{}}
			by[code] = a
		}
		return a
	}
	h := get("", true)
	h.c.Reachable, h.c.Hosting, h.c.KeyOK, h.c.Eligible = true, true, true, true
	for _, f := range snap.Files {
		home := f.Placement == "" || (homeName != "" && f.Placement == homeName)
		if !home && !provision.ValidRegion(f.Placement) {
			continue // fleet/<name> and malformed codes are never candidates
		}
		code := f.Placement
		if home {
			code = ""
		}
		a := get(code, home)
		if f.EIP == "" {
			continue
		}
		if home {
			if f.Region != "" {
				a.regions[f.Region]++
			}
			continue
		}
		hosting := isTrue(f.HibernationConfigured) || f.HibernationConfigured == nil
		keyOK := expectedFP != "" && f.SecretsKeyFP == expectedFP
		eligible := isTrue(f.HibernationConfigured) && isTrue(f.ServesSharedPool) && isTrue(f.AcceptsInbound) && keyOK
		a.c.Reachable = true
		a.c.Hosting = a.c.Hosting || hosting
		if hosting {
			a.hostingBoxes++
			a.c.KeyOK = a.c.KeyOK || keyOK
			a.keyFound = a.keyFound || keyOK
			a.unknownWriter = a.unknownWriter || f.AcceptsInbound == nil
		}
		a.c.Eligible = a.c.Eligible || eligible
		if eligible && f.Region != "" {
			a.regions[f.Region]++
		}
	}
	out := make([]regionCandidate, 0, len(by))
	for _, a := range by {
		a.c.AWSRegion = majority(a.c.Code, a.regions)
		a.c.NoKeyProven = !a.c.Home && snap.Complete && expectedFP != "" && a.hostingBoxes > 0 && !a.unknownWriter && !a.keyFound
		if a.c.Eligible {
			if _, ok := geo.LookupAWSRegion(a.c.AWSRegion); !ok {
				if _, said := noPointSaid.LoadOrStore(a.c.Code, true); !said {
					log.Printf("console: region %q has no point in the shipped AWS region table (AWS region %q), so region guesses leave it out", a.c.Code, a.c.AWSRegion)
				}
			}
		}
		out = append(out, a.c)
	}
	sort.Slice(out, func(i, j int) bool { return out[i].Code < out[j].Code })
	return out
}

// regionCands is regionCandidates with this console's home and expected
// secrets fingerprint.
func (s *Server) regionCands(snap edgesSnapshot) []regionCandidate {
	return regionCandidates(snap, s.cfg.HomeRegion, s.expectedSecretsFP())
}

// candsOrHome is the candidates from a fresh snapshot, or the home alone
// when the listing fails: for displays and guesses that must not fail.
func (s *Server) candsOrHome(ctx context.Context) []regionCandidate {
	snap, err := readEdgesSnapshot(ctx, s.cfg.Releases)
	if err != nil {
		return homeOnlyCandidates()
	}
	return s.regionCands(snap)
}

// majority picks the most common AWS region, ties to the lexically
// smallest, and logs a disagreement once per code.
func majority(code string, regions map[string]int) string {
	best, n := "", 0
	for r, c := range regions {
		if c > n || (c == n && r < best) {
			best, n = r, c
		}
	}
	if len(regions) > 1 {
		if _, said := disagreeSaid.LoadOrStore(code, true); !said {
			log.Printf("console: boxes for region %q disagree on their AWS region %v; using %s", code, regions, best)
		}
	}
	return best
}

func homeOnlyCandidates() []regionCandidate {
	return []regionCandidate{{Code: "", Home: true, Reachable: true, Hosting: true, KeyOK: true, Eligible: true}}
}

func findCandidate(cands []regionCandidate, code string) (regionCandidate, bool) {
	for _, c := range cands {
		if c.Code == code {
			return c, true
		}
	}
	return regionCandidate{}, false
}

func eligibleCount(cands []regionCandidate) int {
	n := 0
	for _, c := range cands {
		if c.Eligible {
			n++
		}
	}
	return n
}

type policyState int

const (
	policyAbsent policyState = iota
	policyKnown
	policyUnreadable
)

// regionPolicy is what the manifest and the fleet placement say about
// where an instance may land (spec §6.1).
type regionPolicy struct {
	State  policyState
	M      *provision.Manifest
	Placed string
}

// appPolicy reads the manifest directly (never instanceManifest, which
// answers the caller itself) and the fleet placement. Any failure is
// policyUnreadable: an unrestricted policy is never invented.
func (s *Server) appPolicy(ctx context.Context, acct *Account, app string) regionPolicy {
	var p regionPolicy
	m, err := provision.GetManifest(ctx, s.cfg.Releases, acct.Sqid, app)
	switch {
	case err == nil:
		p.State, p.M = policyKnown, m
	case errors.Is(err, os.ErrNotExist):
		p.State = policyAbsent
	default:
		log.Printf("console: region policy %s/%s: read manifest: %v", acct.Sqid, app, err)
		return regionPolicy{State: policyUnreadable}
	}
	reg, err := openRegistry(s.cfg.Registry)
	if err != nil {
		log.Printf("console: region policy %s/%s: open registry: %v", acct.Sqid, app, err)
		return regionPolicy{State: policyUnreadable}
	}
	placed, err := s.appPlacement(reg, acct, app)
	if err != nil {
		log.Printf("console: region policy %s/%s: placement: %v", acct.Sqid, app, err)
		return regionPolicy{State: policyUnreadable}
	}
	p.Placed = placed
	return p
}

func (p regionPolicy) pooled() bool { return p.State == policyKnown && p.M.Pool != "" }

// allows is the door's own allow-list predicate on a record-vocabulary code.
func (p regionPolicy) allows(code string) bool {
	switch p.State {
	case policyKnown:
		return instanceRegionAllowed(p.M.Regions, code)
	case policyAbsent:
		return true
	}
	return false
}
```

The two diagnostics are O6 and O7 from the record. Neither carries anything
about a caller.

- [ ] **Step 5: Run the tests to verify they pass**

Run: `go test ./internal/console -run 'TestRegionCandidates|TestReadEdgesSnapshot|TestAppPolicyStates|TestNoKeyProven|TestCandidateLocation|TestEligibilityNeverCombinesBoxes|TestPlacements' -count=1`
Expected: PASS. `TestPlacements*` still passes after the `readEdgesFiles`
refactor.

Then a mutation check (codex plan-R3 4): make eligibility the region's
union of facts. Give the accumulator four flags (any box hosting, serving
the shared pool, accepting inbound, holding the key), and set
`a.c.Eligible` from all four instead of `a.c.Eligible || eligible`. Run
`go test ./internal/console -run TestEligibilityNeverCombinesBoxes -count=1`.
It must FAIL on all three pairs. Revert, and re-run: PASS.

- [ ] **Step 6: Commit**

```bash
git add internal/console/instancesplacement.go internal/console/regioncandidates.go internal/console/regioncandidates_test.go
git commit -m "console: region candidates with positive per-box eligibility incl. the secrets key (nearest-region §6.1, R9)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```

---

### Task 11: The guess and its invariant

**Files:**
- Create: `internal/console/regionguess.go`
- Test: `internal/console/regionguess_test.go`

**Interfaces:**
- Consumes:
  - `regionCandidate`, `findCandidate` and `homeOnlyCandidates` (Task 10);
  - `instanceRegionAllowed`;
  - `geo.ZonePoint`, `geo.Nearest`, `geo.Candidate`, `geo.LookupAWSRegion`
    and `geo.Locator`.
- Produces:

```go
type guessInput struct{ TZ string; IP net.IP }
type guessResult struct{ Code, Source string } // Code: record vocabulary
func guessRegion(cands []regionCandidate, allows func(code string) bool, in guessInput, loc geo.Locator) (guessResult, bool)
```

- [ ] **Step 1: Write the failing tests**

```go
// internal/console/regionguess_test.go
package console

import (
	"math/rand/v2"
	"net"
	"strings"
	"testing"

	"github.com/carlosframework/platform/internal/geo"
)

type fakeLocator map[string]geo.Point

func (f fakeLocator) Locate(ip net.IP) (geo.Point, bool) {
	p, ok := f[ip.String()]
	return p, ok
}

func allowAll(string) bool { return true }

func cand(code, aws string, eligible bool) regionCandidate {
	return regionCandidate{Code: code, Home: code == "", AWSRegion: aws,
		Reachable: true, Hosting: true, KeyOK: eligible, Eligible: eligible}
}

var (
	cHome = cand("", "eu-west-1", true)
	cUK   = cand("uk", "eu-west-2", true)
	cAU   = cand("au", "ap-southeast-2", true)
)

func TestGuessRegionOrder(t *testing.T) {
	loc := fakeLocator{"81.2.69.142": {Lat: -33.9, Lon: 151.2}}
	cands := []regionCandidate{cHome, cUK, cAU}
	cases := []struct {
		name string
		in   guessInput
		want guessResult
	}{
		{"timezone first", guessInput{TZ: "Europe/London", IP: net.ParseIP("81.2.69.142")}, guessResult{"uk", "timezone"}},
		{"UTC falls to IP", guessInput{TZ: "Etc/UTC", IP: net.ParseIP("81.2.69.142")}, guessResult{"au", "ip"}},
		{"nothing maps", guessInput{TZ: "Etc/UTC"}, guessResult{"", "default"}},
	}
	for _, c := range cases {
		if got, ok := guessRegion(cands, allowAll, c.in, loc); !ok || got != c.want {
			t.Errorf("%s: got (%+v, %v), want %+v", c.name, got, ok, c.want)
		}
	}
	got, _ := guessRegion([]regionCandidate{cHome, cUK, cand("au", "ap-southeast-2", false)}, allowAll, guessInput{TZ: "Australia/Sydney"}, nil)
	if got.Code == "au" {
		t.Fatal("picked an ineligible region")
	}
}

// spec §6.4, codex R3-4.
func TestGuessRegionNeverReturnsADisallowedHome(t *testing.T) {
	noPointHome := cand("", "", true)
	onlyUK := func(code string) bool { return instanceRegionAllowed([]string{"uk"}, code) }
	cands := []regionCandidate{noPointHome, cUK}
	if got, ok := guessRegion(cands, onlyUK, guessInput{TZ: "Europe/London"}, nil); !ok || got.Code != "uk" {
		t.Fatalf("mapped hint = (%+v, %v), want uk", got, ok)
	}
	if got, ok := guessRegion(cands, onlyUK, guessInput{TZ: "Etc/UTC"}, nil); ok {
		t.Fatalf("unmappable hint = %+v, want no guess", got)
	}
	if got, ok := guessRegion(homeOnlyCandidates(), onlyUK, guessInput{TZ: "Europe/London"}, nil); ok {
		t.Fatalf("edges-read failure = %+v, want no guess", got)
	}
}

// The invariant over generated MANIFESTS, using the door's own predicate
// on the normalized code (spec §16, codex plan-R1 10):
//
//  1. every non-null guess passes instanceRegionAllowed and is an eligible
//     candidate;
//  2. a guess exists EXACTLY when the order of §6.4 can reach one: the home
//     is a filtered candidate, or the timezone maps and some filtered
//     candidate has a point.
func TestGuessRegionInvariantProperty(t *testing.T) {
	codes := []string{"", "uk", "au", "us", "jp"}
	listable := []string{"", "ie", "uk", "au", "us", "jp"}
	regions := []string{"eu-west-1", "eu-west-2", "ap-southeast-2", "us-east-1", "", "xx-nowhere-1"}
	zones := []string{"Europe/London", "Australia/Sydney", "America/New_York", "Etc/UTC", "", "Asia/Tokyo"}
	r := rand.New(rand.NewPCG(1, 2))
	for i := 0; i < 20000; i++ {
		var cands []regionCandidate
		for _, code := range codes {
			if code != "" && r.IntN(3) == 0 {
				continue
			}
			cands = append(cands, cand(code, regions[r.IntN(len(regions))], code == "" || r.IntN(4) != 0))
		}
		var list []string
		for _, c := range listable {
			if r.IntN(3) == 0 {
				list = append(list, c)
			}
		}
		allows := func(code string) bool { return instanceRegionAllowed(list, code) }
		tz := zones[r.IntN(len(zones))]
		got, ok := guessRegion(cands, allows, guessInput{TZ: tz}, nil)

		homeIn, anyPoint := false, false
		for _, c := range cands {
			if !c.Eligible || !allows(c.Code) {
				continue
			}
			homeIn = homeIn || c.Home
			if _, has := geo.LookupAWSRegion(c.AWSRegion); has {
				anyPoint = true
			}
		}
		_, tzMaps := geo.ZonePoint(tz)
		want := homeIn || (tzMaps && anyPoint)
		if ok != want {
			t.Fatalf("case %d: guess existence %v, want %v (list %v, tz %q, cands %+v)", i, ok, want, list, tz, cands)
		}
		if !ok {
			continue
		}
		if !allows(got.Code) {
			t.Fatalf("case %d: guess %+v fails the manifest %v", i, got, list)
		}
		if c, found := findCandidate(cands, got.Code); !found || !c.Eligible {
			t.Fatalf("case %d: guess %+v is not an eligible candidate", i, got)
		}
	}
}

// Review Focus 4's core: a malformed hint is absent.
func TestGuessRegionToleratesGarbageHints(t *testing.T) {
	got, ok := guessRegion([]regionCandidate{cHome, cUK}, allowAll, guessInput{TZ: strings.Repeat("a", 10<<10)}, fakeLocator{})
	if !ok || got != (guessResult{"", "default"}) {
		t.Fatalf("garbage tz = (%+v, %v), want the home default", got, ok)
	}
}

// codex plan-R3 4: a region whose boxes only collectively have every fact
// is never guessed, however near the caller is (Task 10's splitCapability).
func TestGuessRegionSkipsSplitCapabilityRegions(t *testing.T) {
	for name, boxes := range splitCapability {
		cands := regionCandidates(views(t, homeBox, boxes[0], boxes[1]), "ie", testKeyFP)
		got, ok := guessRegion(cands, allowAll, guessInput{TZ: "Europe/London"}, nil)
		if !ok || got.Code != "" {
			t.Errorf("%s: guess = %+v %v, want the home", name, got, ok)
		}
	}
}
```

Under Task 10's union-of-facts mutation this test still passes, and that is
expected: a candidate's location comes only from an eligible box (codex
plan-R2 2), so a split region has no point to be guessed at. Task 10's
`TestEligibilityNeverCombinesBoxes` is the test that mutation must fail.

The existence rule is exact. When the home is filtered, every path ends in a
guess: the nearest pick, or the home default. Otherwise a guess needs a
mapped timezone and a filtered candidate with a point, because there is no
IP in this property. An IP-only variant is covered by
`TestGuessRegionOrder`.

- [ ] **Step 2: Run them to verify they fail**

Run: `go test ./internal/console -run TestGuessRegion -count=1`
Expected: FAIL, undefined `guessRegion`.

- [ ] **Step 3: Implement `regionguess.go`**

```go
package console

import (
	"net"

	"github.com/carlosframework/platform/internal/geo"
)

// guessInput carries a request's location hints for ONE call. Neither
// field is ever stored, logged, audited or echoed (spec §6.5).
type guessInput struct {
	TZ string
	IP net.IP
}

// guessResult is a guess in the record vocabulary and its source word.
type guessResult struct{ Code, Source string }

// guessRegion is spec §6.4:
//
//  1. filter candidates to eligible ones the policy allows;
//  2. nearest among those with a point, from the timezone, else (only
//     when the timezone is unmappable) the IP;
//  3. else the home, ONLY if the home is itself a filtered candidate;
//  4. else no guess.
func guessRegion(cands []regionCandidate, allows func(code string) bool, in guessInput, loc geo.Locator) (guessResult, bool) {
	var pick []geo.Candidate
	homeIn := false
	for _, c := range cands {
		if !c.Eligible || !allows(c.Code) {
			continue
		}
		pick = append(pick, geo.Candidate{Code: c.Code, Home: c.Home, AWSRegion: c.AWSRegion})
		homeIn = homeIn || c.Home
	}
	if len(pick) == 0 {
		return guessResult{}, false
	}
	if p, ok := geo.ZonePoint(in.TZ); ok {
		if code, ok := geo.Nearest(p, pick); ok {
			return guessResult{code, "timezone"}, true
		}
	} else if loc != nil && in.IP != nil {
		if p, ok := loc.Locate(in.IP); ok {
			if code, ok := geo.Nearest(p, pick); ok {
				return guessResult{code, "ip"}, true
			}
		}
	}
	if homeIn {
		return guessResult{"", "default"}, true
	}
	return guessResult{}, false
}
```

- [ ] **Step 4: Run the tests to verify they pass**

Run: `go test ./internal/console -run TestGuessRegion -count=1`
Expected: PASS.

- [ ] **Step 5: Mutation checks, one at a time (codex plan-R2 9)**

Each mutation must fail the property test on its own, against a passing
baseline. So restore and re-run between them:

1. Run `go test ./internal/console -run TestGuessRegionInvariantProperty -count=1`.
   It must PASS; this is the baseline.
2. **Mutation A:** make `guessRegion`'s first line `return guessResult{}, false`.
   Run the same command. It must FAIL with "guess existence false, want
   true". Revert, and re-run: PASS.
3. **Mutation B:** change the final `if homeIn {` to `if true {`. This is a
   home fallback the policy does not allow. Run the same command. It must
   FAIL with "guess existence true, want false". Revert, and re-run: PASS.
4. `git diff --stat` shows only this task's two files.

- [ ] **Step 6: Commit**

```bash
git add internal/console/regionguess.go internal/console/regionguess_test.go
git commit -m "console: the region guess, with existence and policy invariants property-tested (nearest-region §6.4)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```

---

### Task 12: `claimApp` resolution and the API create door

**Files:**
- Create: `internal/console/regionclaim.go`
- Modify: `internal/console/server.go`
  - `claimApp`, at `:3466-3567`;
  - the browser call site at `:3626`, which passes `regionReq{}` until
    Task 13.
- Modify: `internal/console/apicli.go:330-470` (`handleAPICreateApp`)
- Modify, adding the `regionReq{}` argument:
  - `internal/console/apps_claim_test.go:118,134,150,165`;
  - `internal/console/apicli_test.go:2504`.
- Test: `internal/console/regionclaim_test.go`

**Interfaces:**
- Consumes:
  - Tasks 8 to 11;
  - `s.clientIP` (`authcli.go:876`), `principalFrom`, `placements` and
    `hostingSet`;
  - `homeRegionIn` and `homeRegionOut`;
  - the record's O8.
- Produces:

```go
type regionReq struct {
	explicit    string // a person's choice, record vocabulary
	hasExplicit bool
	tz          string
	ip          net.IP // request-local, person requests only
}
func (s *Server) validateExplicitRegion(ctx context.Context, raw string) (code string, ref *doorRefusal)
type explicitGate struct{ caps map[string]placementCapability; published bool; cands []regionCandidate }
func (s *Server) readExplicitGate(ctx context.Context) explicitGate
func (g explicitGate) refusal(code string) string // "", unknown, not-armed, no-key
func (s *Server) explicitChoices(g explicitGate) []string
func (s *Server) resolveNewAppRegion(ctx context.Context, rq regionReq, adopting bool) (region, source string)
func (s *Server) placeOf(code string, cands []regionCandidate) string
func (s *Server) personIP(r *http.Request) net.IP
func (s *Server) claimApp(ctx context.Context, acct *Account, name, addr, placement string, ownerClaim bool, rq regionReq) (claimOutcome, string, *apexHold)
```

`handleAPICreateApp`'s response gains `region`, `region_name`,
`region_source` and `regions_available`.

C18 and C19 list exactly the codes `validateExplicitRegion` would accept
now, through one predicate, `explicitGate.refusal`: the home's name, plus
each published code that is shape-valid, armed and hosting, and not proved
keyless (R9). They never list a code the same door then refuses (codex
plan-R4 2), and the test submits every listed code and expects it to pass.

- [ ] **Step 1: Write the failing tests**

```go
// internal/console/regionclaim_test.go
package console

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"strings"
	"testing"
	"time"

	"github.com/carlosframework/platform/internal/geo"
	"github.com/carlosframework/platform/internal/registry"
	"github.com/carlosframework/platform/internal/svcrole"
)

func regionFixture(t *testing.T) (*consoleFixture, string) {
	t.Helper()
	fx := newConsoleFixture(t)
	fx.srv.cfg.AppRegions, fx.srv.cfg.HomeRegion, fx.srv.cfg.SecretsPub = true, "ie", testKeyPub
	setEdges(t, fx, map[string]string{"ie-box": homeBox, "uk-box": ukEligible})
	return fx, cliToken(t, fx, "paul@keymail.dev", "code-region-claim")
}

func newAppURL(fx *consoleFixture) string { return fx.ts.URL + "/api/cli/apps/bab/new" }

// postJSONFrom is postJSON with an X-Forwarded-For, which the console
// trusts from the loopback edge (authcli.go clientIP).
func postJSONFrom(t *testing.T, u string, body any, bearer, xff string) (int, map[string]any) {
	t.Helper()
	raw, _ := json.Marshal(body)
	req, err := http.NewRequest(http.MethodPost, u, bytes.NewReader(raw))
	if err != nil {
		t.Fatal(err)
	}
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Authorization", "Bearer "+bearer)
	req.Header.Set("X-Forwarded-For", xff)
	return doJSON(t, req)
}

func errOf(res map[string]any) string { s, _ := res["error"].(string); return s }

var ukPlace = geo.PlaceName("eu-west-2", "uk")

func TestAPICreateAppResolvesRegion(t *testing.T) {
	fx, token := regionFixture(t)
	status, res := postJSON(t, newAppURL(fx), map[string]string{"name": "blog", "timezone": "Europe/London"}, token)
	if status != http.StatusOK || res["region"] != "uk" || res["region_source"] != "timezone" ||
		res["region_name"] != ukPlace || res["regions_available"] != float64(2) {
		t.Fatalf("create = %d %v", status, res)
	}
	if r, src, _ := fx.st.AppRegion(OpsAccountID, "blog"); r != "uk" || src != "timezone" {
		t.Fatalf("stored %q %q", r, src)
	}
	// An old CLI's body, from loopback with no forwarded address.
	if _, res = postJSON(t, newAppURL(fx), map[string]string{"name": "old"}, token); res["region"] != "ie" || res["region_source"] != "default" {
		t.Fatalf("old body = %v", res)
	}
	// An explicit home name is a choice, stored as "".
	postJSON(t, newAppURL(fx), map[string]string{"name": "athome", "region": "ie"}, token)
	if r, src, _ := fx.st.AppRegion(OpsAccountID, "athome"); r != "" || src != "chosen" {
		t.Fatalf("explicit home = %q %q", r, src)
	}
}

// The old-body path through the real door: a person's forwarded address
// is located; a service principal's never is (spec §6.2).
func TestAPICreateAppLocatesOnlyAPerson(t *testing.T) {
	fx, token := regionFixture(t)
	fx.srv.cfg.GeoIP = fakeLocator{"81.2.69.142": {Lat: 51.5, Lon: -0.12}}
	if status, res := postJSONFrom(t, newAppURL(fx), map[string]string{"name": "person"}, token, "81.2.69.142"); status != http.StatusOK ||
		res["region"] != "uk" || res["region_source"] != "ip" {
		t.Fatalf("person = %d %v", status, res)
	}
	svc := serviceToken(t, fx.st, OpsAccountID, "hub", "", svcrole.Publish)
	if status, res := postJSONFrom(t, newAppURL(fx), map[string]string{"name": "hubapp"}, svc, "81.2.69.142"); status != http.StatusOK ||
		res["region"] != "ie" || res["region_source"] != "default" {
		t.Fatalf("service = %d %v, want no IP step", status, res)
	}
}

func TestAPICreateAppRefusesBadExplicitRegions(t *testing.T) {
	fx, token := regionFixture(t)
	// nz cannot host; au's current-writer box lacks the key (R9).
	auNoKey := edgeJSON("au-box", "9.9.9.8", "ap-southeast-2", "au", edgeOpts{hib: bp(true), shared: bp(true), inbound: bp(true)})
	setEdges(t, fx, map[string]string{"nz-box": nzNoHosting, "au-box": auNoKey})
	// codex plan-R4 2: each list names exactly what this door would take.
	choices := []string{"ie", "uk"}
	cases := []struct{ name, region, want string }{
		{"xa", "Bad/Code", msgRegionUnknown("Bad/Code", choices)},
		{"xb", "nz", msgRegionNotArmed("nz", choices)},
		{"xc", "au", msgNoSecretsKey("au")},
	}
	for _, c := range cases {
		status, res := postJSON(t, newAppURL(fx), map[string]string{"name": c.name, "region": c.region}, token)
		if status != http.StatusBadRequest || errOf(res) != c.want {
			t.Errorf("region %q = %d %q, want 400 %q", c.region, status, errOf(res), c.want)
		}
		if has, _ := fx.st.HasAppIn(OpsAccountID, c.name); has {
			t.Errorf("a refused region still claimed %s", c.name)
		}
	}
	// Every advertised choice is accepted by the same door, same fixture.
	for i, code := range choices {
		name := fmt.Sprintf("alt%d", i)
		if status, res := postJSON(t, newAppURL(fx), map[string]string{"name": name, "region": code}, token); status != http.StatusOK {
			t.Errorf("advertised %q refused: %d %v", code, status, res)
		}
	}
	// R9: a region whose current-writer boxes lack the key.
	setEdges(t, fx, map[string]string{"uk-box": ukNoKey})
	if status, res := postJSON(t, newAppURL(fx), map[string]string{"name": "xd", "region": "uk"}, token); status != http.StatusBadRequest || errOf(res) != msgNoSecretsKey("uk") {
		t.Fatalf("keyless uk = %d %v, want C31", status, res)
	}
}

func TestAPICreateAppSwitchOffStoresTheHome(t *testing.T) {
	fx, token := regionFixture(t)
	fx.srv.cfg.AppRegions = false
	_, res := postJSON(t, newAppURL(fx), map[string]string{"name": "off", "region": "uk", "timezone": "Europe/London"}, token)
	if r, src, _ := fx.st.AppRegion(OpsAccountID, "off"); r != "" || src != "default" || res["region"] != "ie" {
		t.Fatalf("switch off = %q %q %v", r, src, res)
	}
	fx.srv.cfg.AppRegions, fx.srv.cfg.HomeRegion = true, ""
	postJSON(t, newAppURL(fx), map[string]string{"name": "unnamed", "region": "uk"}, token)
	if r, _, _ := fx.st.AppRegion(OpsAccountID, "unnamed"); r != "" {
		t.Fatalf("unnamed home stored %q", r)
	}
}

func TestClaimAdoptionDiscardsTheGuess(t *testing.T) {
	fx, _ := regionFixture(t)
	acct := &Account{ID: OpsAccountID, Sqid: "bab"}
	seedBucketApp(t, fx, "bab", "oldsite")
	if out, _, _ := fx.srv.claimApp(context.Background(), acct, "oldsite", "paul@keymail.dev", "", true, regionReq{tz: "Europe/London"}); out != claimOK {
		t.Fatalf("adoption = %v", out)
	}
	if r, src, _ := fx.st.AppRegion(OpsAccountID, "oldsite"); r != "" || src != "default" {
		t.Fatalf("adoption with a guess = %q %q", r, src)
	}
	seedBucketApp(t, fx, "bab", "oldsite2")
	fx.srv.claimApp(context.Background(), acct, "oldsite2", "paul@keymail.dev", "", true, regionReq{explicit: "uk", hasExplicit: true})
	if r, src, _ := fx.st.AppRegion(OpsAccountID, "oldsite2"); r != "uk" || src != "chosen" {
		t.Fatalf("adoption with an explicit choice = %q %q", r, src)
	}
}

// codex plan-R6 1, spec §6.5: every successful creation logs exactly one
// region line, naming what was stored, whatever the switch says: on, off,
// and on with an unnamed home (which keeps assignment off).
func TestClaimLogsOneRegionLineInEverySwitchState(t *testing.T) {
	for _, c := range []struct {
		name, app, home string
		on              bool
		stored, source  string
		line            string
	}{
		{"assignment on", "on1", "ie", true, "uk", "chosen", "console: app region bab/on1: uk (chosen)"},
		{"assignment off", "off1", "ie", false, "", "default", "console: app region bab/off1: ie (default)"},
		{"on, home unnamed", "unnamed1", "", true, "", "default", "console: app region bab/unnamed1:  (default)"},
	} {
		fx, token := regionFixture(t)
		fx.srv.cfg.AppRegions, fx.srv.cfg.HomeRegion = c.on, c.home
		var logs bytes.Buffer
		restore := captureLog(&logs)
		status, res := postJSON(t, newAppURL(fx), map[string]string{"name": c.app, "region": "uk"}, token)
		restore()
		if status != http.StatusOK {
			t.Fatalf("%s: create = %d %v", c.name, status, res)
		}
		if r, src, _ := fx.st.AppRegion(OpsAccountID, c.app); r != c.stored || src != c.source {
			t.Fatalf("%s: stored %q %q, want %q %q", c.name, r, src, c.stored, c.source)
		}
		if n := strings.Count(logs.String(), "console: app region bab/"+c.app+":"); n != 1 || !strings.Contains(logs.String(), c.line+"\n") {
			t.Errorf("%s: %d region line(s) in %q, want exactly %q", c.name, n, logs.String(), c.line)
		}
	}
}

// Review Focus 5: a competing insert that lands AFTER the region was
// resolved loses the claim: no row of ours, no region log line.
func TestClaimLostAfterResolutionWritesNothing(t *testing.T) {
	fx, token := regionFixture(t)
	fx.srv.beforeClaimInsert = func() {
		if _, err := fx.st.CreateAppInRegion(OpsAccountID, "racy", "rival@k.dev", "", "chosen", time.Now()); err != nil {
			t.Error(err)
		}
	}
	var logs bytes.Buffer
	restore := captureLog(&logs)
	status, _ := postJSON(t, newAppURL(fx), map[string]string{"name": "racy", "region": "uk"}, token)
	restore()
	if status != http.StatusConflict {
		t.Fatalf("lost race = %d, want 409", status)
	}
	if r, src, _ := fx.st.AppRegion(OpsAccountID, "racy"); r != "" || src != "chosen" {
		t.Fatalf("the rival's row changed: %q %q", r, src)
	}
	if strings.Contains(logs.String(), "app region bab/racy") {
		t.Fatalf("a lost claim logged a region line: %s", logs.String())
	}
}

// Review Focus 5: a held apex refuses before resolution, so nothing is
// written or logged.
func TestClaimHeldApexWritesNothing(t *testing.T) {
	fx, token := regionFixture(t)
	fx.srv.cfg.AppsDomain = "oncarlos.test"
	fx.addRoute(t, registry.Route{Host: "beta." + fx.sqid + ".oncarlos.test", App: "kass", Account: fx.sqid, Kind: "instance"})
	var logs bytes.Buffer
	restore := captureLog(&logs)
	status, _ := postJSON(t, newAppURL(fx), map[string]string{"name": "beta", "timezone": "Europe/London"}, token)
	restore()
	if status != http.StatusConflict {
		t.Fatalf("held apex = %d, want 409", status)
	}
	if has, _ := fx.st.HasAppIn(OpsAccountID, "beta"); has {
		t.Fatal("beta was created")
	}
	if strings.Contains(logs.String(), "app region bab/beta") {
		t.Fatalf("a refused claim logged a region line: %s", logs.String())
	}
}
```

`fx.sqid` is the fixture account's sqid, `bab`. `fx.addRoute` is
`apex_test.go`'s helper. `doJSON` is `authcli_test.go`'s. `serviceToken` is
`svcfixture_test.go:14`.

- [ ] **Step 2: Run them to verify they fail**

Run: `go test ./internal/console -run 'TestAPICreateApp|TestClaim' -count=1`
Expected: FAIL to compile: `claimApp` has the wrong arity, and `regionReq` is
undefined.

- [ ] **Step 3: Implement `regionclaim.go`**

```go
package console

import (
	"context"
	"net"
	"net/http"
	"sort"
	"strings"

	"github.com/carlosframework/platform/internal/geo"
	"github.com/carlosframework/platform/internal/provision"
)

// regionReq is what a create door hands claimApp: never a resolved region.
// The guess is resolved INSIDE claimApp, after it knows whether this is an
// adoption (spec §5.5). ip is request-local and only set for a person.
type regionReq struct {
	explicit    string
	hasExplicit bool
	tz          string
	ip          net.IP
}

// validateExplicitRegion checks a person's explicit region as the instance
// door checks one (spec §8.1): ValidRegion; armed and hosting unless the
// gate is dormant; not provably without the secrets key (R9). The home's
// name normalizes to "". A refusal lists exactly the codes this same check
// would accept now (codex plan-R4 2).
func (s *Server) validateExplicitRegion(ctx context.Context, raw string) (string, *doorRefusal) {
	code := s.homeRegionIn(strings.TrimSpace(raw))
	if code == "" {
		return "", nil
	}
	gate := s.readExplicitGate(ctx)
	switch gate.refusal(code) {
	case "":
		return code, nil
	case "unknown":
		return "", &doorRefusal{http.StatusBadRequest, msgRegionUnknown(code, s.explicitChoices(gate))}
	case "not-armed":
		return "", &doorRefusal{http.StatusBadRequest, msgRegionNotArmed(code, s.explicitChoices(gate))}
	}
	return "", &doorRefusal{http.StatusBadRequest, msgNoSecretsKey(code)}
}

// explicitGate is one read of what an explicit app region must pass.
type explicitGate struct {
	caps      map[string]placementCapability
	published bool
	cands     []regionCandidate // nil when the edges read failed: R9 cannot refuse
}

func (s *Server) readExplicitGate(ctx context.Context) explicitGate {
	g := explicitGate{}
	g.caps, g.published = placements(ctx, s.cfg.Releases)
	if snap, err := readEdgesSnapshot(ctx, s.cfg.Releases); err == nil {
		g.cands = s.regionCands(snap)
	}
	return g
}

// refusal is "" when an explicit, record-vocabulary code passes, else
// which bar it fails: "unknown", "not-armed" or "no-key".
func (g explicitGate) refusal(code string) string {
	switch {
	case code == "":
		return ""
	case !provision.ValidRegion(code):
		return "unknown"
	case g.published && !(g.caps[code].Reachable && g.caps[code].Hosting):
		return "not-armed"
	}
	if c, ok := findCandidate(g.cands, code); ok && c.NoKeyProven {
		return "no-key"
	}
	return ""
}

// explicitChoices is the home's name plus every published code that
// refusal passes, sorted: never a code this door would refuse.
func (s *Server) explicitChoices(g explicitGate) []string {
	set := map[string]bool{}
	if s.cfg.HomeRegion != "" {
		set[s.cfg.HomeRegion] = true
	}
	for code := range g.caps {
		if c := s.homeRegionIn(code); c != "" && g.refusal(c) == "" {
			set[c] = true
		}
	}
	out := make([]string, 0, len(set))
	for c := range set {
		out = append(out, c)
	}
	sort.Strings(out)
	return out
}

// resolveNewAppRegion is spec §5.5 and §5.6 for a NEW row.
func (s *Server) resolveNewAppRegion(ctx context.Context, rq regionReq, adopting bool) (region, source string) {
	if !s.appRegionsOn() {
		return "", "default"
	}
	if rq.hasExplicit {
		return rq.explicit, "chosen"
	}
	if adopting {
		return "", "default"
	}
	files, err := readEdgesSnapshot(ctx, s.cfg.Releases)
	if err != nil {
		return "", "default"
	}
	g, ok := guessRegion(s.regionCands(files), func(string) bool { return true },
		guessInput{TZ: rq.tz, IP: rq.ip}, s.cfg.GeoIP)
	if !ok {
		return "", "default"
	}
	return g.Code, g.Source
}

// placeOf names a record-vocabulary code: its AWS region's place, else the
// code under its name.
func (s *Server) placeOf(code string, cands []regionCandidate) string {
	c, _ := findCandidate(cands, code)
	return geo.PlaceName(c.AWSRegion, s.homeRegionOut(code))
}

// personIP is the request's own address, for a person only (spec §6.2).
// A service or app principal is a server; its address says nothing about
// where a person is.
func (s *Server) personIP(r *http.Request) net.IP {
	if p, ok := principalFrom(r.Context()); ok && p.Kind != PrincipalUser {
		return nil
	}
	return net.ParseIP(s.clientIP(r))
}
```

`personIP` treats "no principal in the context" as a person. That is the
browser door, where the session is a person by construction.

- [ ] **Step 4: Thread `regionReq` through `claimApp`**

Change the signature, and replace the insert at `server.go:3550`:

```go
func (s *Server) claimApp(ctx context.Context, acct *Account, name, addr, placement string, ownerClaim bool, rq regionReq) (outcome claimOutcome, placed string, held *apexHold) {
```

```go
	region, source := s.resolveNewAppRegion(ctx, rq, adoptingExistingApp)
	if s.beforeClaimInsert != nil {
		s.beforeClaimInsert()
	}
	created, err := s.store.CreateAppInRegion(acct.ID, name, addr, region, source, time.Now())
	if err != nil {
		return claimCreateFailed, "", nil
	}
	if !created { // lost a claim race after the checks
		return claimTaken, "", nil
	}
	// Spec §6.5: one line per creation, in every switch state (codex
	// plan-R6 1). Only reached once the row is ours.
	log.Printf("console: app region %s/%s: %s (%s)", acct.Sqid, name, s.homeRegionOut(region), source)
```

The log text is O8. It is logged for every successful creation, with the
switch on or off and with the home named or not, exactly as spec §6.5
says: one line per creation, naming what was stored. With an unnamed home
the code is the record's `""`, so the line reads `bab/app:  (default)`.
A claim that loses its race or is refused logs nothing, because the line
comes after both checks. `TestClaimLogsOneRegionLineInEverySwitchState`
pins all three states. Add `, regionReq{}` to every existing call:
`server.go:3626`, `apps_claim_test.go:118,134,150,165` and
`apicli_test.go:2504`.

- [ ] **Step 5: Extend `handleAPICreateApp`**

Add the request fields:

```go
		Region   string `json:"region"`
		Timezone string `json:"timezone"`
```

After the `place` validation, before `ownerClaim`:

```go
	rq := regionReq{tz: req.Timezone, ip: s.personIP(r)}
	if strings.TrimSpace(req.Region) != "" {
		code, ref := s.validateExplicitRegion(r.Context(), req.Region)
		if ref != nil {
			jsonError(w, ref.Status, ref.Msg)
			return
		}
		rq.explicit, rq.hasExplicit = code, true
	}
```

Pass `rq` to `claimApp`. In `claimOK`:

```go
		gotRegion, gotSource, err := s.store.AppRegion(acct.ID, name)
		if err != nil {
			log.Printf("console: claim %s/%s: read back region: %v", acct.Sqid, name, err)
		}
		files, ferr := readEdgesSnapshot(r.Context(), s.cfg.Releases)
		cands := homeOnlyCandidates()
		if ferr == nil {
			cands = s.regionCands(files)
		}
```

Add to the response map:

```go
			"region":            s.homeRegionOut(gotRegion),
			"region_name":       s.placeOf(gotRegion, cands),
			"region_source":     gotSource,
			"regions_available": eligibleCount(cands),
```

- [ ] **Step 6: Run the tests to verify they pass**

Run: `go test ./internal/console -count=1`
Expected: PASS, including every existing claim test.

Then the log check (codex plan-R6 1): wrap the `log.Printf` back in
`if s.appRegionsOn() { … }`, and run
`go test ./internal/console -run TestClaimLogsOneRegionLineInEverySwitchState -count=1`.
It must FAIL on `assignment off` and `on, home unnamed` (0 lines). Revert,
and re-run: PASS.

Then a mutation check (codex plan-R4 2): in `explicitChoices`, also accept
codes whose refusal is `no-key`. Run
`go test ./internal/console -run TestAPICreateAppRefusesBadExplicitRegions -count=1`.
It must FAIL (`au` is listed). Revert, and re-run: PASS.

- [ ] **Step 7: Commit**

```bash
git add internal/console/regionclaim.go internal/console/regionclaim_test.go internal/console/server.go internal/console/apicli.go internal/console/apps_claim_test.go internal/console/apicli_test.go
git commit -m "console: resolve the app region inside claimApp; API create takes region and timezone (nearest-region §5.5, §8.1, §9.1, R9)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```

---

### Task 13: The new-app page picker, POST and browser guess route

**Files:**
- Create: `internal/console/regionpicker.go`
- Modify: `internal/console/server.go`
  - the `page` struct, at `:1350`;
  - `handleNewAppForm`, at `:3586`;
  - `handleNewApp`, at `:3607`;
  - the routes, at `:1152-1153`.
- Modify: `internal/console/templates/app_new.html`
- Test: `internal/console/regionpicker_test.go`

**Interfaces:**
- Consumes: Tasks 6, 10, 11 and 12.
- Produces:

```go
const autoRegion = "@auto"
type regionOption struct{ Value, Label string }
type regionPickerView struct{ Auto string; Options []regionOption; Attribution bool }
func (s *Server) newAppPicker(ctx context.Context, ip net.IP) *regionPickerView // nil = no picker
func (s *Server) pickedRegion(ctx context.Context, r *http.Request) (regionReq, bool)
func (s *Server) handleNewAppRegion(w http.ResponseWriter, r *http.Request)
```

The `page` struct gains the field `RegionPicker *regionPickerView`.

- [ ] **Step 1: Write the failing tests**

`html/template` writes `'` as `&#39;`, so every assertion on rendered copy
compares the escaped form, both positive and negative (codex plan-R1 8).

```go
// internal/console/regionpicker_test.go
package console

import (
	"encoding/json"
	"html"
	"net/http"
	"net/url"
	"regexp"
	"strings"
	"testing"
)

func has(body, plain string) bool { return strings.Contains(body, html.EscapeString(plain)) }

// newAppFormGolden is the base tree's own rendering of the create-app form
// (the branch base, switch off), with the session's CSRF value blanked.
const newAppFormGolden = `<form class="signin" method="post" action="/a/bab/apps/new">
  <input type="hidden" name="csrf" value="CSRF">
  <label class="visually-hidden" for="app-name">App name</label>
  <input id="app-name" type="text" name="name" placeholder="my-app" autofocus
    required pattern="[A-Za-z][A-Za-z0-9-]{0,31}" maxlength="32">
  <button type="submit">Create app</button>
</form>`

// newAppForm is the rendered <form>…</form>, CSRF value blanked.
func newAppForm(t *testing.T, body string) string {
	t.Helper()
	i := strings.Index(body, `<form class="signin"`)
	if i < 0 {
		t.Fatalf("no create-app form in %s", body)
	}
	j := strings.Index(body[i:], "</form>") + i + len("</form>")
	return regexp.MustCompile(`name="csrf" value="[^"]*"`).ReplaceAllString(body[i:j], `name="csrf" value="CSRF"`)
}

// codex plan-R4 3, spec §10 and §16: whenever there is no picker, the form
// is today's form byte for byte: with the switch off, and with the switch
// on but only one eligible region.
func TestNewAppPageWithoutPickerIsUnchanged(t *testing.T) {
	off := newConsoleFixture(t)
	one, _ := regionFixture(t)
	setEdges(t, one, map[string]string{"uk-box": ukNoKey}) // the home alone is eligible
	for name, fx := range map[string]*consoleFixture{"switch off": off, "one eligible region": one} {
		_, _, body := get(t, signInPaul(t, fx), fx.u("/apps/new"))
		if got := newAppForm(t, body); got != newAppFormGolden {
			t.Errorf("%s: form =\n%q\nwant\n%q", name, got, newAppFormGolden)
		}
	}
}

func TestNewAppPickerAndAutoChoice(t *testing.T) {
	fx, _ := regionFixture(t)
	c, csrf := signInAs(t, fx, "paul@keymail.dev", "code-picker")
	_, _, body := get(t, c, fx.u("/apps/new"))
	for _, want := range []string{`name="region"`, `value="@auto"`, `value="uk"`, `value="ie"`} {
		if !strings.Contains(body, want) {
			t.Fatalf("picker missing %q", want)
		}
	}
	if !has(body, ukPlace) || !has(body, regionCopy.PickerLabel) || !has(body, regionCopy.PickerHelp) {
		t.Fatal("picker missing its copy")
	}
	if status, _, _ := postForm(t, c, fx.u("/apps/new"), url.Values{"csrf": {csrf}, "name": {"auto1"}, "region": {"@auto"}, "tz": {"Europe/London"}}); status != http.StatusSeeOther {
		t.Fatalf("@auto post = %d", status)
	}
	if r, src, _ := fx.st.AppRegion(OpsAccountID, "auto1"); r != "uk" || src != "timezone" {
		t.Fatalf("@auto = %q %q", r, src)
	}
	postForm(t, c, fx.u("/apps/new"), url.Values{"csrf": {csrf}, "name": {"chose"}, "region": {"ie"}, "tz": {"Europe/London"}})
	if r, src, _ := fx.st.AppRegion(OpsAccountID, "chose"); r != "" || src != "chosen" {
		t.Fatalf("explicit ie = %q %q", r, src)
	}
}

// codex R2-3: a home NAMED "auto" is a region, not the sentinel.
func TestRegionNamedAutoIsNotTheSentinel(t *testing.T) {
	fx, _ := regionFixture(t)
	fx.srv.cfg.HomeRegion = "auto"
	c, csrf := signInAs(t, fx, "paul@keymail.dev", "code-auto-name")
	postForm(t, c, fx.u("/apps/new"), url.Values{"csrf": {csrf}, "name": {"homeauto"}, "region": {"auto"}})
	if r, src, _ := fx.st.AppRegion(OpsAccountID, "homeauto"); r != "" || src != "chosen" {
		t.Fatalf("explicit home named auto = %q %q", r, src)
	}
}

// Review Focus 3: uk's box loses the secrets key between render and POST.
func TestNewAppPostWithANoLongerEligibleRegion(t *testing.T) {
	fx, _ := regionFixture(t)
	c, csrf := signInAs(t, fx, "paul@keymail.dev", "code-stale")
	if _, _, body := get(t, c, fx.u("/apps/new")); !strings.Contains(body, `value="uk"`) {
		t.Fatal("precondition: uk offered")
	}
	setEdges(t, fx, map[string]string{"uk-box": ukNoKey})
	status, loc, _ := postForm(t, c, fx.u("/apps/new"), url.Values{"csrf": {csrf}, "name": {"stale"}, "region": {"uk"}})
	if status != http.StatusSeeOther || loc != fx.path("/apps/new") {
		t.Fatalf("stale region = %d %q, want back to the form", status, loc)
	}
	if _, _, body := get(t, c, fx.u("/apps/new")); !has(body, regionCopy.Unavailable) {
		t.Fatal("no C5 flash")
	}
	if exists, _ := fx.st.HasAppIn(OpsAccountID, "stale"); exists {
		t.Fatal("a refused region still created the app")
	}
}

func TestNewAppRegionRouteNeedsSessionAndCSRF(t *testing.T) {
	fx, _ := regionFixture(t)
	c, csrf := signInAs(t, fx, "paul@keymail.dev", "code-route")
	status, _, body := postForm(t, c, fx.u("/apps/new/region"), url.Values{"csrf": {csrf}, "tz": {"Europe/London"}})
	var res map[string]string
	if err := json.Unmarshal([]byte(body), &res); err != nil {
		t.Fatalf("guess route body %q: %v", body, err)
	}
	if status != http.StatusOK || res["code"] != "uk" || res["source"] != "timezone" || res["name"] != ukPlace {
		t.Fatalf("guess route = %d %v", status, res)
	}
	if status, _, _ := postForm(t, c, fx.u("/apps/new/region"), url.Values{"csrf": {"wrong"}, "tz": {"Europe/London"}}); status != http.StatusForbidden {
		t.Fatalf("bad csrf = %d, want 403", status)
	}
	if status, _, _ := postForm(t, newBrowser(t), fx.u("/apps/new/region"), url.Values{"tz": {"Europe/London"}}); status == http.StatusOK {
		t.Fatal("no session got a guess")
	}
}
```

`signInPaul` (`server_test.go:450`) returns just the client. `signInAs`
(`svcpage_test.go:59`) also returns the CSRF token.

- [ ] **Step 2: Run them to verify they fail**

Run: `go test ./internal/console -run 'TestNewApp|TestRegionNamedAuto' -count=1`
Expected: FAIL.

- [ ] **Step 3: Implement `regionpicker.go`**

```go
package console

import (
	"context"
	"crypto/subtle"
	"fmt"
	"net"
	"net/http"
	"strings"
)

// autoRegion is the picker's "let the server guess" value. "@" is outside
// the placement-code grammar, so no real region collides with it (codex
// R2-3).
const autoRegion = "@auto"

type regionOption struct{ Value, Label string }

type regionPickerView struct {
	Auto        string // the @auto option's label (C3)
	Options     []regionOption
	Attribution bool // C24, when the IP step is configured
}

// newAppPicker builds the picker, or nil: switch off, an edges read
// failure, or fewer than two eligible regions (§10). The server's own
// guess is IP, then home; the page script relabels @auto with the
// browser's timezone guess.
func (s *Server) newAppPicker(ctx context.Context, ip net.IP) *regionPickerView {
	if !s.appRegionsOn() {
		return nil
	}
	files, err := readEdgesSnapshot(ctx, s.cfg.Releases)
	if err != nil {
		return nil
	}
	cands := s.regionCands(files)
	if eligibleCount(cands) < 2 {
		return nil
	}
	g, _ := guessRegion(cands, func(string) bool { return true }, guessInput{IP: ip}, s.cfg.GeoIP)
	v := &regionPickerView{
		Auto:        fmt.Sprintf(regionCopy.AutoOption, s.placeOf(g.Code, cands)),
		Attribution: s.cfg.GeoIP != nil,
	}
	for _, c := range cands {
		if c.Eligible {
			v.Options = append(v.Options, regionOption{Value: s.homeRegionOut(c.Code), Label: s.placeOf(c.Code, cands)})
		}
	}
	return v
}

// pickedRegion turns the form's region field into a regionReq. ok=false
// means the submitted code is not an eligible region NOW (C5).
func (s *Server) pickedRegion(ctx context.Context, r *http.Request) (regionReq, bool) {
	rq := regionReq{tz: r.FormValue("tz"), ip: s.personIP(r)}
	v := strings.TrimSpace(r.FormValue("region"))
	if v == "" || v == autoRegion {
		return rq, true
	}
	files, err := readEdgesSnapshot(ctx, s.cfg.Releases)
	if err != nil {
		return rq, false
	}
	code := s.homeRegionIn(v)
	c, found := findCandidate(s.regionCands(files), code)
	if !found || !c.Eligible {
		return rq, false
	}
	rq.explicit, rq.hasExplicit = code, true
	return rq, true
}

// handleNewAppRegion is the page script's guess (spec §9.5): POST with csrf
// and tz, session-gated, writing nothing.
func (s *Server) handleNewAppRegion(w http.ResponseWriter, r *http.Request) {
	_, raw, _, _, ok := s.account(w, r)
	if !ok {
		return
	}
	if subtle.ConstantTimeCompare([]byte(r.FormValue("csrf")), []byte(raw)) != 1 {
		jsonError(w, http.StatusForbidden, "Cross-site request blocked.")
		return
	}
	files, err := readEdgesSnapshot(r.Context(), s.cfg.Releases)
	cands := homeOnlyCandidates()
	if err == nil {
		cands = s.regionCands(files)
	}
	g, ok := guessRegion(cands, func(string) bool { return true }, guessInput{TZ: r.FormValue("tz"), IP: s.personIP(r)}, s.cfg.GeoIP)
	if !ok {
		g = guessResult{"", "default"}
	}
	writeJSON(w, http.StatusOK, map[string]string{
		"code": s.homeRegionOut(g.Code), "name": s.placeOf(g.Code, cands), "source": g.Source,
	})
}
```

The CSRF refusal reuses `handleNewApp`'s existing text, which is existing
copy and is not new.

- [ ] **Step 4: Wire the routes, the GET and the POST**

Beside the `apps/new` routes:

```go
	s.mux.HandleFunc("POST /a/{sqid}/apps/new/region", s.handleNewAppRegion)
```

In `page`, add `RegionPicker *regionPickerView`. In `handleNewAppForm`,
before `render`:

```go
	p.RegionPicker = s.newAppPicker(r.Context(), s.personIP(r))
```

In `handleNewApp`, after the CSRF check and the `name` read:

```go
	rq, regionOK := s.pickedRegion(r.Context(), r)
	if !regionOK {
		fail(regionCopy.Unavailable)
		return
	}
	outcome, _, held := s.claimApp(r.Context(), acct, name, addr, "", role == RoleOwner, rq)
```

In `claimOK`, replace the audit line:

```go
		detail := "created app " + name
		if s.appRegionsOn() {
			region, _, _ := s.store.AppRegion(acct.ID, name)
			files, _ := readEdgesSnapshot(r.Context(), s.cfg.Releases)
			detail = fmt.Sprintf(regionCopy.AuditCreated, name, s.placeOf(region, s.regionCands(files)))
		}
		s.audit(acct, name, addr, "create", detail)
```

- [ ] **Step 5: Add the picker to `app_new.html`, leaving the no-picker form byte-identical**

Replace the last line of the name `<input>` and the button line (two lines,
`app_new.html:13-14`) with the block below. The `{{with}}` opens on the
input's own line and `{{end}}` closes on the script's, so with no picker the
template emits exactly today's bytes: no blank line, no extra spaces (codex
plan-R4 3; spec §10, §16). `TestNewAppPageWithoutPickerIsUnchanged` compares
the rendered form with the base tree's own rendering, with the switch off
and with the switch on but one eligible region.

```html
    required pattern="[A-Za-z][A-Za-z0-9-]{0,31}" maxlength="32">{{with .RegionPicker}}
  <label for="app-region">{{$.RegionCopy.PickerLabel}}</label>
  <select id="app-region" name="region">
    <option value="@auto" selected>{{.Auto}}</option>
    {{range .Options}}<option value="{{.Value}}">{{.Label}}</option>{{end}}
  </select>
  <input type="hidden" name="tz" value="">
  <p class="small">{{$.RegionCopy.PickerHelp}}</p>
  {{if .Attribution}}<p class="small"><a href="https://db-ip.com">{{$.RegionCopy.Attribution}}</a></p>{{end}}
  <script>
  // Enhancement only: with JS off the server's IP-or-home guess stands.
  // Fills the tz hint and relabels ONLY the @auto option; its value stays
  // "@auto" so the server still knows the person did not choose.
  (function () {
    var f = document.currentScript.closest("form"), sel = f.elements.region, tz = "";
    var label = {{printf $.RegionCopy.AutoOption "%PLACE%"}};
    try { tz = Intl.DateTimeFormat().resolvedOptions().timeZone || ""; } catch (e) {}
    f.elements.tz.value = tz;
    if (!tz) return;
    var body = new URLSearchParams({ csrf: f.elements.csrf.value, tz: tz });
    fetch("{{$.Base}}/apps/new/region", { method: "POST", body: body, credentials: "same-origin" })
      .then(function (r) { return r.ok ? r.json() : null; })
      .then(function (g) {
        if (!g || sel.value !== "@auto") return;
        sel.options[0].text = label.replace("%PLACE%", g.name);
      })
      .catch(function () {});
  })();
  </script>{{end}}
  <button type="submit">Create app</button>
```

`html/template` renders the `{{printf …}}` in script context as a JS string
literal, so the approved C3 shape is used whatever its wording.

- [ ] **Step 6: Run the tests to verify they pass**

Run: `go test ./internal/console -count=1`
Expected: PASS.

Then a mutation check: put the block on its own lines instead
(`  {{with .RegionPicker}}` after the input, `  {{end}}` before the
button). `TestNewAppPageWithoutPickerIsUnchanged` must FAIL in both cases
(an extra `"  \n"`). Restore, and re-run: PASS.

- [ ] **Step 7: Commit**

```bash
git add internal/console/regionpicker.go internal/console/regionpicker_test.go internal/console/server.go internal/console/templates/app_new.html
git commit -m "console: new-app region picker with @auto, browser guess route (nearest-region §10, §9.5)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```

---

### Task 14: The instance create door: fill, eligibility, the R9 refusal and the response

**Files:**
- Modify: `internal/console/apiinstances.go`
  - `apiRecordView`, at `:535`;
  - `:763-799`, where the fill goes;
  - the armed gate, at `:827-852`;
  - `:1066`, where the response is written.
- Create: `internal/console/regionchoices.go` (`instanceChoices`)
- Test: `internal/console/regionfill_test.go`

**Interfaces:**
- Consumes: Tasks 7, 9 (`s.appRegion`) and 10 (`s.regionCands`,
  `findCandidate`, `NoKeyProven`).
- Produces: `apiRecordView.RegionFrom string`
  (`json:"region_from,omitempty"`) and `apiRecordView.RegionName string`
  (`json:"region_name,omitempty"`), and
  `func (s *Server) instanceChoices(ctx context.Context, allowed []string, cands []regionCandidate, except string) []string`.

C16 and C17 list what an EXPLICIT request for the same socket-backed
instance would pass now, not what automatic eligibility would pick (codex
plan-R4 2): the manifest's allow-list as the door reads it (every code
normalized through `homeRegionIn` first, so a home NAME in the allow-list
counts as the home only if `""` is allowed), the armed gate's reachable and
hosting bars, and R9's refusal. The refused region itself is left out. When
nothing passes, the message is C16b or C17b.

- [ ] **Step 1: Write the failing tests**

Every test that inspects a record asserts the create returned 201 first, so a
refusal cannot pass as a non-fill (codex plan-R1 11).

```go
// internal/console/regionfill_test.go
package console

import (
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"regexp"
	"strings"
	"testing"

	"github.com/carlosframework/platform/internal/provision"
)

type fillRec struct {
	provision.Record
	RegionFrom string `json:"region_from"`
	RegionName string `json:"region_name"`
}

// fillFixture: titogo with an unrestricted shared-pool manifest, a stored
// app region, a named home, the deployment's secrets key, and two
// eligible boxes. fx.st.db is the Store's own *sql.DB (same package).
func fillFixture(t *testing.T, appRegion string) (*consoleFixture, string, string) {
	t.Helper()
	fx, token := instanceFixture(t, "code-fill-"+appRegion)
	fx.srv.cfg.HomeRegion, fx.srv.cfg.SecretsPub = "ie", testKeyPub
	m := titoManifest()
	m.Pool, m.Regions = "", nil
	enableInstances(t, fx, "titogo", m)
	if _, err := fx.st.db.Exec(`UPDATE apps SET region = ?, region_source = 'chosen' WHERE app = 'titogo'`, appRegion); err != nil {
		t.Fatal(err)
	}
	setEdges(t, fx, map[string]string{"ie-box": homeBox, "uk-box": ukEligible})
	return fx, token, instancesURL(fx, "bab", "titogo")
}

func create(t *testing.T, u, token, body string) (int, fillRec, string) {
	t.Helper()
	status, _, out := rawReq(t, http.MethodPost, u, token, body)
	var rec fillRec
	json.Unmarshal([]byte(out), &rec)
	return status, rec, out
}

func mustCreate(t *testing.T, u, token, body string) fillRec {
	t.Helper()
	status, rec, out := create(t, u, token, body)
	if status != http.StatusCreated {
		t.Fatalf("create %s = %d %s, want 201", body, status, out)
	}
	return rec
}

func TestFillFromTheAppRegion(t *testing.T) {
	_, token, u := fillFixture(t, "uk")
	if rec := mustCreate(t, u, token, `{"host":"a.go.tito.io"}`); rec.Region != "uk" || rec.RegionFrom != "app" || rec.RegionName != ukPlace {
		t.Fatalf("fill = %+v", rec)
	}
	if rec := mustCreate(t, u, token, `{"host":"b.go.tito.io","region":"ie"}`); rec.Region != "" || rec.RegionFrom != "request" {
		t.Fatalf("explicit home = %+v", rec)
	}
	for _, body := range []string{
		`{"host":"c.go.tito.io","kind":"static"}`,
		`{"host":"d.go.tito.io","kind":"service","addrs":["10.0.0.4:8080"]}`,
	} {
		if rec := mustCreate(t, u, token, body); rec.Region != "" {
			t.Errorf("%s was filled: %+v", body, rec)
		}
	}
	status, _, out := create(t, u, token, `{"host":"e.go.tito.io","role":"replica"}`)
	if status != http.StatusBadRequest || !strings.Contains(out, "role replica needs a placement") {
		t.Errorf("replica without a region = %d %s, want the existing refusal", status, out)
	}
}

func TestFillWithAHomeAppRegionChangesNothing(t *testing.T) {
	_, token, u := fillFixture(t, "")
	if rec := mustCreate(t, u, token, `{"host":"a.go.tito.io"}`); rec.Region != "" || rec.RegionFrom != "" {
		t.Fatalf("home app = %+v", rec)
	}
}

// Precedence with a NON-HOME app default in play: pool and fleet win.
func TestFillPrecedence(t *testing.T) {
	fx, token, u := fillFixture(t, "uk")
	m := titoManifest()
	m.Pool, m.Regions = "tito", nil
	enableInstances(t, fx, "titogo", m)
	if rec := mustCreate(t, u, token, `{"host":"p.go.tito.io"}`); rec.Region != "" || rec.RegionFrom != "" {
		t.Fatalf("pooled = %+v, want no fill", rec)
	}
	m.Pool = ""
	enableInstances(t, fx, "titogo", m)
	if err := fx.st.SetAppPlacement(OpsAccountID, "titogo", "fleet/acme"); err != nil {
		t.Fatal(err)
	}
	if rec := mustCreate(t, u, token, `{"host":"f.go.tito.io"}`); rec.Region != "fleet/acme" || rec.RegionFrom != "fleet" {
		t.Fatalf("fleet = %+v", rec)
	}
}

func TestFillStoreErrorIsC20(t *testing.T) {
	fx, token, u := fillFixture(t, "uk")
	fx.srv.appRegionOf = func(int64, string) (string, string, error) { return "", "", errors.New("db gone") }
	status, _, out := create(t, u, token, `{"host":"a.go.tito.io"}`)
	if status != http.StatusBadGateway || !strings.Contains(out, msgAppRegionUnreadable) {
		t.Fatalf("store error = %d %s, want 502 C20", status, out)
	}
}

// codex R2-2: dormancy keeps explicit requests, never app-derived ones.
func TestAppDerivedNeedsPositiveEligibility(t *testing.T) {
	fx, token, u := fillFixture(t, "uk")
	for _, k := range []string{"control/edges/uk-box.json", "control/edges/ie-box.json"} {
		if err := fx.rel.Delete(t.Context(), k); err != nil {
			t.Fatal(err)
		}
	}
	mustCreate(t, u, token, `{"host":"x.go.tito.io","region":"uk"}`)
	status, _, out := create(t, u, token, `{"host":"y.go.tito.io"}`)
	if status != http.StatusBadRequest || !strings.Contains(out, reasonText("absent")) {
		t.Fatalf("app-derived uk under dormancy = %d %s, want C16 absent", status, out)
	}
}

func TestAppDerivedEdgesReadFailureIs503(t *testing.T) {
	fx, token, u := fillFixture(t, "uk")
	fx.srv.cfg.Releases = failingList{fx.rel}
	status, _, out := create(t, u, token, `{"host":"y.go.tito.io"}`)
	if status != http.StatusServiceUnavailable || !strings.Contains(out, msgAppRegionUnconfirmed("uk")) {
		t.Fatalf("edges failure = %d %s, want 503 C28", status, out)
	}
}

func TestAppDerivedRefusals(t *testing.T) {
	fx, token, u := fillFixture(t, "uk")
	m := titoManifest()
	m.Pool, m.Regions = "", []string{"au"}
	enableInstances(t, fx, "titogo", m)
	// au publishes no box, so nothing the allow-list names can pass: C17b.
	if status, _, out := create(t, u, token, `{"host":"y.go.tito.io"}`); status != http.StatusBadRequest ||
		!strings.Contains(out, msgAppRegionNotAllowed("uk", nil)) {
		t.Fatalf("allow-list = %d %s, want C17b", status, out)
	}
	m.Regions = nil
	enableInstances(t, fx, "titogo", m)
	if status, _, out := create(t, u, token, `{"host":"z.go.tito.io","replicas":["uk"]}`); status != http.StatusBadRequest ||
		!strings.Contains(out, msgReplicaOverlap("uk")) {
		t.Fatalf("replica overlap = %d %s, want C26", status, out)
	}
	setEdges(t, fx, map[string]string{"uk-box": ukNoKey})
	if status, _, out := create(t, u, token, `{"host":"k.go.tito.io"}`); status != http.StatusBadRequest ||
		!strings.Contains(out, reasonText("no-secrets-key")) {
		t.Fatalf("app-derived keyless uk = %d %s, want C16 no-secrets-key", status, out)
	}
}

// R9's explicit half: refused only when a current writer PROVES the
// region keyless, and only for what needs config on the box.
func TestExplicitCreateIntoAKeylessRegion(t *testing.T) {
	fx, token, u := fillFixture(t, "")
	setEdges(t, fx, map[string]string{"uk-box": ukNoKey})
	if status, _, out := create(t, u, token, `{"host":"a.go.tito.io","region":"uk"}`); status != http.StatusBadRequest || !strings.Contains(out, msgNoSecretsKey("uk")) {
		t.Fatalf("keyless instance = %d %s, want C31", status, out)
	}
	if status, _, out := create(t, u, token, `{"host":"r.go.tito.io","region":"uk","role":"replica"}`); status != http.StatusBadRequest || !strings.Contains(out, msgNoSecretsKey("uk")) {
		t.Fatalf("keyless replica = %d %s, want C31", status, out)
	}
	mustCreate(t, u, token, `{"host":"s.go.tito.io","region":"uk","kind":"static"}`)
	// An addr-backed service proxies to a process the platform never starts,
	// so it needs no config on the box either (codex plan-R2 7).
	mustCreate(t, u, token, `{"host":"v.go.tito.io","region":"uk","kind":"service","addrs":["10.0.0.4:8080"]}`)
	setEdges(t, fx, map[string]string{"uk-box": ukOldWriter})
	mustCreate(t, u, token, `{"host":"o.go.tito.io","region":"uk"}`) // unknown keeps today's behaviour
	setEdges(t, fx, map[string]string{"uk-box": ukNoKey})
	fx.srv.cfg.SecretsPub = ""
	mustCreate(t, u, token, `{"host":"n.go.tito.io","region":"uk"}`) // the console cannot judge
}

func TestFillRunsWithTheSwitchOff(t *testing.T) {
	fx, token, u := fillFixture(t, "uk")
	fx.srv.cfg.AppRegions = false
	if rec := mustCreate(t, u, token, `{"host":"a.go.tito.io"}`); rec.Region != "uk" {
		t.Fatalf("switch off = %+v, want the stored uk", rec)
	}
}

// codex plan-R3 6: a successful replica create names its place too, so the
// CLI prints C14 for it as for any other placement.
func TestReplicaCreateNamesItsPlace(t *testing.T) {
	_, token, u := fillFixture(t, "")
	rec := mustCreate(t, u, token, `{"host":"r.go.tito.io","region":"uk","role":"replica"}`)
	if rec.Role != "replica" || rec.Region != "uk" || rec.RegionName != ukPlace {
		t.Fatalf("replica = %+v, want role replica in uk named %q", rec, ukPlace)
	}
}

// persistedRecord is the record's bytes as the bucket holds them, with the
// create time blanked (the one field a golden cannot pin).
func persistedRecord(t *testing.T, fx *consoleFixture, host string) string {
	t.Helper()
	raw, _, err := fx.rel.Get(t.Context(), provision.RecordKey("bab", "titogo", host))
	if err != nil {
		t.Fatal(err)
	}
	return regexp.MustCompile(`"created_at":"[^"]*"`).ReplaceAllString(string(raw), `"created_at":"T"`)
}

// codex plan-R3 5, spec §16: the persisted record, byte for byte. A legacy
// home create writes exactly what the unmodified door wrote (the golden was
// taken from the base tree), and an app-defaulted non-home create differs
// only by its region: no steering stamp, nothing else (R8).
func TestPersistedRecordsMatchTheirGoldens(t *testing.T) {
	const legacy = `{"schema":1,"host":"a.go.tito.io","slug":"a","app":"titogo","account":"bab","channel":"stable","created_at":"T","created_by":"alice@keymail.dev"}`
	const appUK = `{"schema":1,"host":"a.go.tito.io","slug":"a","app":"titogo","account":"bab","region":"uk","channel":"stable","created_at":"T","created_by":"alice@keymail.dev"}`
	for _, c := range []struct{ appRegion, want string }{{"", legacy}, {"uk", appUK}} {
		fx, token, u := fillFixture(t, c.appRegion)
		rec := mustCreate(t, u, token, `{"host":"a.go.tito.io"}`)
		if rec.Steering != "" {
			t.Fatalf("app region %q: the response carries a steering stamp %q", c.appRegion, rec.Steering)
		}
		if got := persistedRecord(t, fx, "a.go.tito.io"); got != c.want {
			t.Fatalf("app region %q: stored\n%s\nwant\n%s", c.appRegion, got, c.want)
		}
	}
}

// codex plan-R4 2: C16's and C17's lists name exactly the regions an
// explicit create of this same instance would pass: the allow-list, the
// armed gate and R9 (not automatic eligibility), never the region refused.
// Each advertised region is then submitted, and must be accepted.
func TestAppRegionRefusalsAdvertiseOnlyWhatPasses(t *testing.T) {
	auEligible := edgeJSON("au-box", "9.9.9.9", "ap-southeast-2", "au", eligibleBox)
	for _, c := range []struct {
		name    string
		ukBox   string
		regions []string
		want    func(choices []string) string
		choices []string
	}{
		// uk is the app's region, keyless; the home is outside the allow-list.
		{"C16", ukNoKey, []string{"uk", "au", "nz"}, func(ch []string) string { return msgAppRegionUnavailable("uk", "no-secrets-key", ch) }, []string{"au"}},
		// uk is eligible but outside the allow-list; "ie" there is the
		// home's NAME, which the door reads as "" and so refuses.
		{"C17", ukEligible, []string{"au", "nz", "ie"}, func(ch []string) string { return msgAppRegionNotAllowed("uk", ch) }, []string{"au"}},
	} {
		fx, token, u := fillFixture(t, "uk")
		setEdges(t, fx, map[string]string{"uk-box": c.ukBox, "au-box": auEligible, "nz-box": nzNoHosting})
		m := titoManifest()
		m.Pool, m.Regions = "", c.regions
		enableInstances(t, fx, "titogo", m)
		status, _, out := create(t, u, token, `{"host":"y.go.tito.io"}`)
		if want := c.want(c.choices); status != http.StatusBadRequest || !strings.Contains(out, want) {
			t.Errorf("%s = %d %s, want %q", c.name, status, out, want)
		}
		for i, code := range c.choices {
			mustCreate(t, u, token, fmt.Sprintf(`{"host":"alt%d.go.tito.io","region":%q}`, i, code))
		}
	}
}
```

The `legacy` golden is the base tree's own output: the same request (an
unrestricted shared-pool manifest, no region, alice's token) against the
unmodified door at the branch base, its stored bytes read back with
`created_at` blanked. Re-take it the same way if the base moves.

- [ ] **Step 2: Run them to verify they fail**

Run: `go test ./internal/console -run 'TestFill|TestAppDerived|TestExplicitCreateIntoAKeylessRegion|TestReplicaCreateNamesItsPlace|TestPersistedRecords|TestAppRegionRefusals' -count=1`
Expected: FAIL.

- [ ] **Step 3: Implement the fill, replacing `apiinstances.go:772`**

```go
	// The app's region is the default for a create that names none
	// (nearest-region spec §7, R2). It runs BEFORE every check an explicit
	// region meets, and never for a fleet-placed app, a pooled manifest, a
	// replica, or anything but a socket-backed instance.
	raw := strings.TrimSpace(req.Region)
	regionFrom := ""
	if raw != "" {
		regionFrom = "request"
	}
	if raw == "" && placed == "" && m.Pool == "" && strings.TrimSpace(req.Role) == "" &&
		(provision.Record{Kind: strings.TrimSpace(req.Kind), Addrs: req.Addrs}).DerivesPaths() {
		appRegion, _, err := s.appRegion(acct.ID, app)
		if err != nil {
			log.Printf("console: api instances create %s/%s: read app region: %v", acct.Sqid, app, err)
			jsonError(w, http.StatusBadGateway, msgAppRegionUnreadable)
			return
		}
		if appRegion != "" {
			raw, regionFrom = appRegion, "app"
		}
	}
	if placed != "" {
		regionFrom = "fleet"
	}
	region := s.homeRegionIn(raw)
	// An APP-DERIVED region needs positive eligibility — the secrets key
	// included (R9) — and never rides the dormant gate (codex R2-2).
	if regionFrom == "app" {
		files, err := readEdgesSnapshot(ctx, s.cfg.Releases)
		if err != nil {
			log.Printf("console: api instances create %s/%s: read edges: %v", acct.Sqid, app, err)
			jsonError(w, http.StatusServiceUnavailable, msgAppRegionUnconfirmed(s.homeRegionOut(region)))
			return
		}
		cands := s.regionCands(files)
		reason := "absent"
		if c, ok := findCandidate(cands, region); ok {
			reason = c.unavailableReason()
		}
		// Each list names only what an explicit request for this same
		// instance would pass (codex plan-R4 2): instanceChoices.
		if reason != "" {
			jsonError(w, http.StatusBadRequest, msgAppRegionUnavailable(s.homeRegionOut(region), reason, s.instanceChoices(ctx, m.Regions, cands, region)))
			return
		}
		if !instanceRegionAllowed(m.Regions, region) {
			jsonError(w, http.StatusBadRequest, msgAppRegionNotAllowed(s.homeRegionOut(region), s.instanceChoices(ctx, m.Regions, cands, region)))
			return
		}
		for _, rep := range req.Replicas {
			if s.homeRegionIn(strings.TrimSpace(rep)) == region {
				jsonError(w, http.StatusBadRequest, msgReplicaOverlap(s.homeRegionOut(region)))
				return
			}
		}
	}
```

Create `internal/console/regionchoices.go`:

```go
package console

import (
	"context"
	"sort"

	"github.com/carlosframework/platform/internal/provision"
)

// instanceChoices lists, under their names, the regions an EXPLICIT
// request for this same socket-backed instance would pass right now (codex
// plan-R4 2): the manifest's allow-list as the door reads it (each code
// normalized through homeRegionIn first), the armed gate's reachable and
// hosting bars, and R9's refusal. These are the explicit door's rules, not
// automatic eligibility. except, the region just refused, is left out.
func (s *Server) instanceChoices(ctx context.Context, allowed []string, cands []regionCandidate, except string) []string {
	caps, published := placements(ctx, s.cfg.Releases)
	codes := map[string]bool{"": true}
	for c := range caps {
		codes[s.homeRegionIn(c)] = true
	}
	for _, c := range allowed {
		codes[s.homeRegionIn(c)] = true
	}
	var out []string
	for code := range codes {
		if code == except || !instanceRegionAllowed(allowed, code) {
			continue
		}
		if code != "" {
			if !provision.ValidRegion(code) || (published && !(caps[code].Reachable && caps[code].Hosting)) {
				continue
			}
			if c, ok := findCandidate(cands, code); ok && c.NoKeyProven {
				continue
			}
		}
		out = append(out, s.homeRegionOut(code))
	}
	sort.Strings(out)
	return out
}
```

- [ ] **Step 4: Add the R9 explicit refusal to the armed gate**

Inside `if placed == "" && region != "" {` (`:827`), after the closing brace
of its final `switch` (`:852`):

```go
		// R9: a socket-backed instance or a replica needs its config,
		// secrets and store binding rendered on the box, which only a box
		// holding the deployment's secrets key can do. Refused only when a
		// CURRENT writer proves the region keyless; unknown keeps today's
		// behaviour, so roll order never refuses a working region.
		if probe.DerivesPaths() || probe.Role == "replica" {
			if files, err := readEdgesSnapshot(ctx, s.cfg.Releases); err == nil {
				if c, ok := findCandidate(s.regionCands(files), region); ok && c.NoKeyProven {
					jsonError(w, http.StatusBadRequest, msgNoSecretsKey(s.homeRegionOut(region)))
					return
				}
			}
		}
```

- [ ] **Step 5: Fill the response fields**

In `apiRecordView`:

```go
	// RegionFrom and RegionName say how this create chose its region and
	// what the place is called (spec §7): facts about this request, not
	// part of the record.
	RegionFrom string `json:"region_from,omitempty"`
	RegionName string `json:"region_name,omitempty"`
```

At `:1066`, after `out := recordView(rec)`:

```go
	out.RegionFrom = regionFrom
	// Every non-fleet placement is named, replicas included (codex
	// plan-R3 6); a fleet-placed record's Region is the fleet itself.
	if placed == "" {
		out.RegionName = s.placeOf(rec.Region, s.candsOrHome(ctx))
	}
```

- [ ] **Step 6: Run the tests to verify they pass**

Run: `go test ./internal/console -count=1`
Expected: PASS, including `TestAPIInstanceCreate*` and the
placement-door tests.

Then a mutation check on the R9 gate's scope. Change
`if probe.DerivesPaths() || probe.Role == "replica" {` to `if true {`, and
run `go test ./internal/console -run TestExplicitCreateIntoAKeylessRegion -count=1`.
It must FAIL on the static or addr-backed create. Revert, and re-run: PASS.

Then the list mutation (codex plan-R4 2): in `instanceChoices`, drop the
`!instanceRegionAllowed(allowed, code)` condition. Run
`go test ./internal/console -run TestAppRegionRefusalsAdvertiseOnlyWhatPasses -count=1`.
It must FAIL on both C16 and C17 (the home `ie` is listed). Revert, and
re-run: PASS.

Then the steering mutation (codex plan-R3 5): right after the
`rec := provision.Record{...}` literal, add
`if regionFrom == "app" { rec.Steering = "latency" }`, and run
`go test ./internal/console -run TestPersistedRecordsMatchTheirGoldens -count=1`.
It must FAIL on the app-defaulted record. Revert, and re-run: PASS.

- [ ] **Step 7: Commit**

```bash
git add internal/console/apiinstances.go internal/console/regionchoices.go internal/console/regionfill_test.go
git commit -m "console: instance door fills the app region, proves eligibility, refuses keyless regions (nearest-region §7, R9)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```

---

### Task 15: `app_default`, place names and the guess endpoint

**Files:**
- Create: `internal/console/regiondefault.go`
- Modify: `internal/console/apiregions.go`
- Modify: `internal/console/authcli.go`, beside the `/regions` route at `:586`
- Test: `internal/console/regiondefault_test.go`

**Interfaces:**
- Consumes: Tasks 7, 9, 10, 11 and 12.
- Produces:

```go
type appDefaultView struct {
	Region         string `json:"region"`    // the STORED region, under its name; "" when unknown
	Name           string `json:"name"`
	Source         string `json:"source"`
	Effective      string `json:"effective"` // where a no-region create lands
	Available      *bool  `json:"available,omitempty"`
	Reason         string `json:"reason,omitempty"`
	RegionKnown    bool   `json:"region_known"`    // the stored region was read
	PlacementKnown bool   `json:"placement_known"` // the manifest and fleet placement were read
	InstanceDomain string `json:"instance_domain,omitempty"` // the domain instance hosts take; "" when unknown
}
func (s *Server) appDefault(ctx context.Context, acct *Account, app string) appDefaultView
func (s *Server) platformInstanceDomain(acct *Account) string
func (s *Server) handleAPIRegionGuess(w http.ResponseWriter, r *http.Request)
```

`GET …/regions` entries gain `aws_region` and `name`. The response gains
`app_default`.

`region_known` and `placement_known` are additive fields this plan adds to
spec §9.3's block (codex plan-R2 3). A response is always HTTP 200, so the
CLI needs them to tell a confirmed fact from one the console could not read.
`deploy --region` needs the stored region, and `canary --region` needs the
fleet placement. Each refuses when its fact is unknown (Tasks 21, 22).
Spec rev 6.1 records both (R9a): `region_known` is true exactly when the
stored region was read, and `placement_known` exactly when the manifest and
fleet placement were read; absent knowledge fails closed.

`instance_domain` is one more additive field, not in the spec (codex
plan-R3 1): the domain instance hosts take. It is the manifest's `domain`
when a manifest exists, else the domain the instances-manifest PUT would
stamp (`<sqid>.<PLATFORM_APPS_DOMAIN>`), and omitted when the manifest is
unreadable or the console has no valid apps domain. `canary --region` needs
it to name the canary's host, and read that one record, before its first
write.

A pooled default's `available` is the allow-list's verdict on the home,
because a no-region pooled create lands there (codex plan-R3 2). The
`pooled` reason stays, so the Instances tab still shows no line for it.

R9a holds in the pooled and fleet branches too (codex plan-R4 1, spec rev
6.2): with the stored region unread, `effective` stays empty and
`available` omitted, and `reason` (`pooled` or `fleet`) is the only
placement fact stated. Task 22's canary check therefore needs BOTH flags.

- [ ] **Step 1: Write the failing tests**

```go
// internal/console/regiondefault_test.go
package console

import (
	"context"
	"encoding/json"
	"errors"
	"io"
	"net/http"
	"strings"
	"testing"

	"github.com/carlosframework/platform/internal/provision"
)

func regionsURL(fx *consoleFixture) string { return fx.ts.URL + "/api/cli/apps/bab/titogo/regions" }
func guessURL(fx *consoleFixture) string   { return regionsURL(fx) + "/guess" }

// corruptManifest replaces the manifest with unparsable bytes, under its
// current etag (Put with "" is create-only and would not replace it).
func corruptManifest(t *testing.T, fx *consoleFixture) {
	t.Helper()
	key := provision.ManifestKey("bab", "titogo")
	_, etag, err := fx.rel.Get(context.Background(), key)
	if err != nil {
		t.Fatal(err)
	}
	if _, err := fx.rel.Put(context.Background(), key, []byte("{not json"), etag); err != nil {
		t.Fatal(err)
	}
}

func TestRegionsKeepsLegacyFieldsAndAddsAppDefault(t *testing.T) {
	fx, token, _ := fillFixture(t, "uk")
	status, _, out := rawReq(t, http.MethodGet, regionsURL(fx), token, "")
	var res struct {
		Default    string         `json:"default"`
		AppDefault appDefaultView `json:"app_default"`
	}
	if err := json.Unmarshal([]byte(out), &res); err != nil || status != http.StatusOK || res.Default != "ie" {
		t.Fatalf("legacy default = %d %s", status, out)
	}
	if d := res.AppDefault; d.Region != "uk" || d.Effective != "uk" || d.Available == nil || !*d.Available || d.Name != ukPlace {
		t.Fatalf("app_default = %+v", d)
	}
	if !strings.Contains(out, `"aws_region":"eu-west-2"`) || !strings.Contains(out, ukPlace) {
		t.Fatalf("entries lack names: %s", out)
	}
}

// Every reason §9.3 names, each from its own condition (codex plan-R1 12).
func TestAppDefaultReasons(t *testing.T) {
	acct := &Account{ID: OpsAccountID, Sqid: "bab"}
	cases := []struct {
		name      string
		setup     func(t *testing.T, fx *consoleFixture)
		reason    string
		available *bool
		effective string
	}{
		{"available", func(*testing.T, *consoleFixture) {}, "", bp(true), "uk"},
		{"absent", func(t *testing.T, fx *consoleFixture) { fx.rel.Delete(t.Context(), "control/edges/uk-box.json") }, "absent", bp(false), "uk"},
		{"not-hosting", func(t *testing.T, fx *consoleFixture) {
			setEdges(t, fx, map[string]string{"uk-box": edgeJSON("uk-box", "1.2.3.4", "eu-west-2", "uk", edgeOpts{hib: bp(false), shared: bp(true), inbound: bp(true), fp: testKeyFP})})
		}, "not-hosting", bp(false), "uk"},
		{"no-secrets-key", func(t *testing.T, fx *consoleFixture) { setEdges(t, fx, map[string]string{"uk-box": ukNoKey}) }, "no-secrets-key", bp(false), "uk"},
		{"not-eligible", func(t *testing.T, fx *consoleFixture) {
			setEdges(t, fx, map[string]string{"uk-box": edgeJSON("uk-box", "1.2.3.4", "eu-west-2", "uk", edgeOpts{hib: bp(true), shared: bp(false), inbound: bp(true), fp: testKeyFP})})
		}, "not-eligible", bp(false), "uk"},
		{"not-allowed", func(t *testing.T, fx *consoleFixture) {
			m := titoManifest()
			m.Pool, m.Regions = "", []string{"au"}
			enableInstances(t, fx, "titogo", m)
		}, "not-allowed", bp(false), "uk"},
		{"pooled", func(t *testing.T, fx *consoleFixture) {
			m := titoManifest()
			m.Pool, m.Regions = "tito", nil
			enableInstances(t, fx, "titogo", m)
		}, "pooled", bp(true), "ie"},
		{"fleet", func(t *testing.T, fx *consoleFixture) { fx.st.SetAppPlacement(OpsAccountID, "titogo", "fleet/acme") }, "fleet", nil, "fleet/acme"},
		{"policy-unreadable", corruptManifest, "policy-unreadable", nil, "uk"},
		{"edges-unreadable", func(t *testing.T, fx *consoleFixture) { fx.srv.cfg.Releases = failingList{fx.rel} }, "edges-unreadable", nil, "uk"},
	}
	for _, c := range cases {
		fx, _, _ := fillFixture(t, "uk")
		c.setup(t, fx)
		d := fx.srv.appDefault(t.Context(), acct, "titogo")
		okAvail := (c.available == nil && d.Available == nil) || (c.available != nil && d.Available != nil && *c.available == *d.Available)
		if d.Reason != c.reason || !okAvail || d.Effective != c.effective || d.Region != "uk" {
			t.Errorf("%s: %+v, want reason %q effective %q available %v", c.name, d, c.reason, c.effective, c.available)
		}
	}
}

// codex plan-R2 3: a fact the console could not read is never presented as
// known: no home-valued region, no non-fleet effective.
func TestAppDefaultKnownFlags(t *testing.T) {
	acct := &Account{ID: OpsAccountID, Sqid: "bab"}
	fx, _, _ := fillFixture(t, "uk")
	if d := fx.srv.appDefault(t.Context(), acct, "titogo"); !d.RegionKnown || !d.PlacementKnown || d.Region != "uk" {
		t.Fatalf("readable = %+v", d)
	}
	fx.srv.appRegionOf = func(int64, string) (string, string, error) { return "", "", errors.New("db gone") }
	if d := fx.srv.appDefault(t.Context(), acct, "titogo"); d.RegionKnown || !d.PlacementKnown || d.Region != "" || d.Effective != "" || d.Available != nil {
		t.Fatalf("unreadable region = %+v, want region unknown and nothing presented as fact", d)
	}
	fx2, _, _ := fillFixture(t, "uk")
	corruptManifest(t, fx2)
	if d := fx2.srv.appDefault(t.Context(), acct, "titogo"); !d.RegionKnown || d.PlacementKnown || d.Reason != "policy-unreadable" {
		t.Fatalf("unreadable policy = %+v, want placement unknown", d)
	}
}

// codex plan-R3 2: a pooled default is available only when the create door
// would take it. A no-region pooled create lands on the home, so the home
// must pass the manifest's allow-list.
func TestPooledAvailabilityMatchesTheCreateDoor(t *testing.T) {
	acct := &Account{ID: OpsAccountID, Sqid: "bab"}
	for _, c := range []struct {
		name    string
		regions []string
		avail   bool
	}{
		{"no allow-list", nil, true},
		{"allow-list without the home", []string{"uk"}, false},
	} {
		fx, token, u := fillFixture(t, "uk")
		m := titoManifest()
		m.Pool, m.Regions = "tito", c.regions
		enableInstances(t, fx, "titogo", m)
		d := fx.srv.appDefault(t.Context(), acct, "titogo")
		if d.Reason != "pooled" || d.Effective != "ie" || d.Available == nil || *d.Available != c.avail {
			t.Fatalf("%s: app_default = %+v, want pooled, ie, available %v", c.name, d, c.avail)
		}
		status, _, out := create(t, u, token, `{"host":"p.go.tito.io"}`)
		if created := status == http.StatusCreated; created != c.avail {
			t.Fatalf("%s: create = %d %s, but app_default said available %v", c.name, status, out, c.avail)
		}
	}
}

// codex plan-R3 1: instance_domain is the domain the instances-manifest PUT
// stamps on hosts, so the CLI can name a canary's host before any write.
func TestAppDefaultInstanceDomain(t *testing.T) {
	acct := &Account{ID: OpsAccountID, Sqid: "bab"}
	fx, _, _ := fillFixture(t, "uk")
	if d := fx.srv.appDefault(t.Context(), acct, "titogo"); d.InstanceDomain != "go.tito.io" {
		t.Fatalf("with a manifest = %q, want the manifest's go.tito.io", d.InstanceDomain)
	}
	corruptManifest(t, fx)
	if d := fx.srv.appDefault(t.Context(), acct, "titogo"); d.InstanceDomain != "" {
		t.Fatalf("unreadable manifest = %q, want none", d.InstanceDomain)
	}
	fx2, token := instanceFixture(t, "code-inst-domain")
	fx2.srv.cfg.AppsDomain = ""
	if d := fx2.srv.appDefault(t.Context(), acct, "titogo"); d.InstanceDomain != "" {
		t.Fatalf("no apps domain = %q, want none (the PUT refuses)", d.InstanceDomain)
	}
	fx2.srv.cfg.AppsDomain = "oncarlos.test"
	before := fx2.srv.appDefault(t.Context(), acct, "titogo").InstanceDomain
	status, _, out := rawReq(t, http.MethodPut, fx2.ts.URL+"/api/cli/apps/bab/titogo/instances-manifest", token, `{}`)
	var m struct {
		Domain string `json:"domain"`
	}
	if err := json.Unmarshal([]byte(out), &m); err != nil || status != http.StatusOK {
		t.Fatalf("enable = %d %s", status, out)
	}
	if before == "" || before != m.Domain {
		t.Fatalf("before the PUT instance_domain = %q, the PUT stamped %q", before, m.Domain)
	}
}

// codex plan-R4 1, spec R9a: an unread stored region leaves region, name,
// source and effective empty and available omitted, even when a fleet or a
// pooled manifest would otherwise decide them. The placement the console
// did read stays visible in reason.
func TestAppDefaultUnreadRegionHidesEffective(t *testing.T) {
	acct := &Account{ID: OpsAccountID, Sqid: "bab"}
	for _, c := range []struct {
		name   string
		setup  func(t *testing.T, fx *consoleFixture)
		reason string
	}{
		{"pooled", func(t *testing.T, fx *consoleFixture) {
			m := titoManifest()
			m.Pool, m.Regions = "tito", nil
			enableInstances(t, fx, "titogo", m)
		}, "pooled"},
		{"fleet", func(t *testing.T, fx *consoleFixture) {
			if err := fx.st.SetAppPlacement(OpsAccountID, "titogo", "fleet/acme"); err != nil {
				t.Fatal(err)
			}
		}, "fleet"},
	} {
		fx, _, _ := fillFixture(t, "uk")
		c.setup(t, fx)
		fx.srv.appRegionOf = func(int64, string) (string, string, error) { return "", "", errors.New("db gone") }
		d := fx.srv.appDefault(t.Context(), acct, "titogo")
		if d.RegionKnown || !d.PlacementKnown || d.Region != "" || d.Name != "" || d.Source != "" ||
			d.Effective != "" || d.Available != nil || d.Reason != c.reason {
			t.Errorf("%s with an unread region = %+v, want only placement_known and reason %q", c.name, d, c.reason)
		}
	}
}

func TestRegionGuessEndpoint(t *testing.T) {
	fx, token, _ := fillFixture(t, "")
	status, _, out := rawReq(t, http.MethodPost, guessURL(fx), token, `{"tz":"Europe/London"}`)
	if status != http.StatusOK || !strings.Contains(out, `"code":"uk"`) || !strings.Contains(out, `"source":"timezone"`) {
		t.Fatalf("guess = %d %s", status, out)
	}
	// Review Focus 4: garbage hints are absent, never an error.
	status, _, out = rawReq(t, http.MethodPost, guessURL(fx), token, `{"ip":"not-an-ip","tz":"`+strings.Repeat("a", 10<<10)+`"}`)
	if status != http.StatusOK || !strings.Contains(out, `"source":"default"`) {
		t.Fatalf("garbage = %d %s", status, out)
	}
	status, _, out = rawReq(t, http.MethodPost, guessURL(fx), token, `{not json`)
	if status != http.StatusOK || !strings.Contains(out, `"source":"default"`) {
		t.Fatalf("malformed body = %d %s", status, out)
	}
	fx.st.SetAppPlacement(OpsAccountID, "titogo", "fleet/acme")
	if _, _, out := rawReq(t, http.MethodPost, guessURL(fx), token, `{"tz":"Europe/London"}`); !strings.Contains(out, `"guess":null`) {
		t.Fatalf("fleet guess = %s (codex R4-1)", out)
	}
}

func TestRegionGuessPooledNullAndUnreadable502(t *testing.T) {
	fx, token, _ := fillFixture(t, "")
	m := titoManifest()
	m.Pool = "tito"
	enableInstances(t, fx, "titogo", m)
	if _, _, out := rawReq(t, http.MethodPost, guessURL(fx), token, `{"tz":"Europe/London"}`); !strings.Contains(out, `"guess":null`) {
		t.Fatalf("pooled guess = %s", out)
	}
	corruptManifest(t, fx)
	if status, _, out := rawReq(t, http.MethodPost, guessURL(fx), token, `{"tz":"Europe/London"}`); status != http.StatusBadGateway || !strings.Contains(out, msgPolicyUnreadable) {
		t.Fatalf("unreadable manifest = %d %s, want 502 C27", status, out)
	}
	fx2, token2, _ := fillFixture(t, "")
	fx2.srv.cfg.Registry = t.TempDir() // a directory: the registry will not open
	if status, _, out := rawReq(t, http.MethodPost, guessURL(fx2), token2, `{"tz":"Europe/London"}`); status != http.StatusBadGateway || !strings.Contains(out, msgPolicyUnreadable) {
		t.Fatalf("unreadable placement = %d %s, want 502 C27", status, out)
	}
}

// codex 8: with a canonical-host redirect configured, a guess POST's hints
// appear in no Location, no body, no log and no audit row.
func TestRegionGuessHintsNeverLeak(t *testing.T) {
	fx, token, _ := fillFixture(t, "")
	fx.srv.redirects = map[string]bool{"old.example": true}
	var logs strings.Builder
	restore := captureLog(&logs)
	defer restore()
	req, _ := http.NewRequest(http.MethodPost, guessURL(fx), strings.NewReader(`{"tz":"Pacific/Chatham","ip":"203.0.113.77"}`))
	req.Host = "old.example"
	req.Header.Set("Authorization", "Bearer "+token)
	res, err := (&http.Client{CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }}).Do(req)
	if err != nil {
		t.Fatal(err)
	}
	body, _ := io.ReadAll(res.Body)
	res.Body.Close()
	everything := res.Header.Get("Location") + string(body)
	_, _, out := rawReq(t, http.MethodPost, guessURL(fx), token, `{"tz":"Pacific/Chatham","ip":"203.0.113.77"}`)
	everything += out + logs.String()
	events, err := fx.st.Events(OpsAccountID, "titogo", 50)
	if err != nil {
		t.Fatal(err)
	}
	for _, e := range events {
		everything += e.Detail + e.Message
	}
	for _, secret := range []string{"Chatham", "203.0.113.77"} {
		if strings.Contains(everything, secret) {
			t.Fatalf("hint %q leaked: %s", secret, everything)
		}
	}
}
```

`fx.srv.redirects` is the normalized host set `ServeHTTP` reads
(`server.go:723`, `:1306`). `Event` carries both `Detail` and `Message`
(`store.go:2066`).

- [ ] **Step 2: Run them to verify they fail**

Run: `go test ./internal/console -run 'TestRegionsKeeps|TestAppDefault|TestRegionGuess|TestPooledAvailability' -count=1`
Expected: FAIL. `TestAppDefaultUnreadRegionHidesEffective` is the R9a case
for pooled and fleet apps.

- [ ] **Step 3: Implement `regiondefault.go`**

```go
package console

import (
	"context"
	"encoding/json"
	"net"
	"net/http"
	"strings"

	"github.com/carlosframework/platform/internal/provision"
)

// appDefaultView is §9.3's app_default. region is the STORED region (what
// deploy --region compares against); effective is where a no-region create
// lands — the fleet itself for a placed app, never the platform home
// (codex R2-4). RegionKnown and PlacementKnown say which facts were READ:
// an unread fact is left empty, never filled with the home (codex
// plan-R2 3; spec R9a). InstanceDomain is the domain instance hosts take,
// so canary --region can name its host before any write (codex plan-R3 1).
type appDefaultView struct {
	Region         string `json:"region"`
	Name           string `json:"name"`
	Source         string `json:"source"`
	Effective      string `json:"effective"`
	Available      *bool  `json:"available,omitempty"`
	Reason         string `json:"reason,omitempty"`
	RegionKnown    bool   `json:"region_known"`
	PlacementKnown bool   `json:"placement_known"`
	InstanceDomain string `json:"instance_domain,omitempty"`
}

func boolp(b bool) *bool { return &b }

func (s *Server) appDefault(ctx context.Context, acct *Account, app string) appDefaultView {
	stored, source, rerr := s.appRegion(acct.ID, app)
	snap, ferr := readEdgesSnapshot(ctx, s.cfg.Releases)
	cands := homeOnlyCandidates()
	if ferr == nil {
		cands = s.regionCands(snap)
	}
	pol := s.appPolicy(ctx, acct, app)
	v := appDefaultView{RegionKnown: rerr == nil, PlacementKnown: pol.State != policyUnreadable}
	switch pol.State {
	case policyKnown:
		v.InstanceDomain = pol.M.Domain
	case policyAbsent:
		v.InstanceDomain = s.platformInstanceDomain(acct)
	}
	if rerr == nil {
		v.Region, v.Name, v.Source, v.Effective = s.homeRegionOut(stored), s.placeOf(stored, cands), source, s.homeRegionOut(stored)
	}
	switch {
	case pol.State == policyUnreadable:
		v.Reason = "policy-unreadable"
		return v
	case pol.Placed != "":
		// R9a: with the stored region unread, effective stays empty even
		// though the fleet is known; reason still says fleet (codex
		// plan-R4 1).
		v.Reason = "fleet"
		if rerr == nil {
			v.Effective = pol.Placed
		}
		return v
	case pol.pooled():
		// A no-region pooled create lands on the home, so it is available
		// only when the home passes the allow-list (codex plan-R3 2). R9a
		// again: nothing about effective is stated when the region is unread.
		v.Reason = "pooled"
		if rerr == nil {
			v.Effective, v.Available = s.homeRegionOut(""), boolp(pol.allows(""))
		}
		return v
	case rerr != nil:
		// The stored region is unknown, so where a create lands is too.
		v.Reason = "policy-unreadable"
		return v
	case ferr != nil:
		v.Reason = "edges-unreadable"
		return v
	}
	reason := "absent"
	if c, ok := findCandidate(cands, stored); ok {
		reason = c.unavailableReason()
	}
	if reason == "" && !pol.allows(stored) {
		reason = "not-allowed"
	}
	v.Reason, v.Available = reason, boolp(reason == "")
	return v
}

// platformInstanceDomain is the domain the instances-manifest PUT stamps on
// a NEW manifest (handleAPIInstancesEnable): "" when this console has no
// valid platform apps domain, which that PUT refuses. With a manifest, its
// own domain governs instead.
func (s *Server) platformInstanceDomain(acct *Account) string {
	if s.cfg.AppsDomain == "" {
		return ""
	}
	d := acct.Sqid + "." + s.cfg.AppsDomain
	if !provision.ValidHostShape(d) {
		return ""
	}
	return d
}

// handleAPIRegionGuess is POST …/regions/guess (spec §9.4). Hints come only
// in the body, so no redirect can echo them. A malformed body is read as
// no hints; a person's request with no ip uses its own address, a service
// principal's never does.
func (s *Server) handleAPIRegionGuess(w http.ResponseWriter, r *http.Request) {
	_, acct, app, ok := s.instanceGate(w, r)
	if !ok {
		return
	}
	var body struct {
		TZ string `json:"tz"`
		IP string `json:"ip"`
	}
	_ = json.NewDecoder(http.MaxBytesReader(w, r.Body, 64<<10)).Decode(&body)
	pol := s.appPolicy(r.Context(), acct, app)
	if pol.State == policyUnreadable {
		jsonError(w, http.StatusBadGateway, msgPolicyUnreadable)
		return
	}
	if pol.Placed != "" || pol.pooled() {
		writeJSON(w, http.StatusOK, map[string]any{"guess": nil})
		return
	}
	ip := net.ParseIP(strings.TrimSpace(body.IP))
	if ip == nil {
		ip = s.personIP(r)
	}
	files, err := readEdgesSnapshot(r.Context(), s.cfg.Releases)
	cands := homeOnlyCandidates()
	if err == nil {
		cands = s.regionCands(files)
	}
	g, ok := guessRegion(cands, pol.allows, guessInput{TZ: body.TZ, IP: ip}, s.cfg.GeoIP)
	if !ok {
		writeJSON(w, http.StatusOK, map[string]any{"guess": nil})
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{"guess": map[string]string{
		"code": s.homeRegionOut(g.Code), "name": s.placeOf(g.Code, cands), "source": g.Source,
	}})
}
```

- [ ] **Step 4: Extend `handleAPIRegions` and register the route**

In `apiregions.go`, change the gate line to
`_, acct, app, ok := s.instanceGate(w, r)`, and build the entries with names:

```go
	type region struct {
		Code      string `json:"code"`
		Armed     bool   `json:"armed"`
		AWSRegion string `json:"aws_region,omitempty"`
		Name      string `json:"name,omitempty"`
	}
	set, _ := armedPlacements(r.Context(), s.cfg.Releases)
	cands := s.candsOrHome(r.Context())
	named := func(code string) region {
		c, _ := findCandidate(cands, s.homeRegionIn(code))
		return region{Code: code, AWSRegion: c.AWSRegion, Name: s.placeOf(s.homeRegionIn(code), cands)}
	}
	out := make([]region, 0, len(set)+1)
	if s.cfg.HomeRegion != "" {
		h := named(s.cfg.HomeRegion)
		h.Armed = true
		out = append(out, h)
		delete(set, s.cfg.HomeRegion)
	}
	for code, armed := range set {
		e := named(code)
		e.Armed = armed
		out = append(out, e)
	}
	sort.Slice(out, func(i, j int) bool { return out[i].Code < out[j].Code })
	writeJSON(w, http.StatusOK, map[string]any{
		"regions": out, "default": s.cfg.HomeRegion,
		"app_default": s.appDefault(r.Context(), acct, app),
	})
```

In `authcli.go`:

```go
	// The per-caller region guess (nearest-region spec §9.4): POST, so the
	// hints never ride a URL a redirect could echo.
	s.cliRoute("POST /api/cli/apps/{account}/{app}/regions/guess", svcrole.Allow(svcrole.Instance), s.handleAPIRegionGuess)
```

Then add the new route to `exemptFromLegacyPin` in
`internal/console/svcroutes_test.go`, beside `GET …/regions`.
`TestInstanceRoleMatchesTodaysAppTokenReach` pins every `instance`-role route
to what an app token reached before service principals, and the guess route
did not exist then:

```go
	// The region guess (nearest-region spec §9.4) stands on GET .../regions'
	// footing: the same deployment topology, filtered by the app's own
	// policy, for a hint the caller supplies. It did not exist before
	// service principals, and titogo's per-instance picker is its consumer.
	"POST /api/cli/apps/{account}/{app}/regions/guess": true,
```

- [ ] **Step 5: Run the tests to verify they pass**

Run: `go test ./internal/console -count=1`
Expected: PASS, including the existing `/regions` tests, whose `regions` and
`default` must be unchanged, and `TestInstanceRoleMatchesTodaysAppTokenReach`.

- [ ] **Step 6: Commit**

```bash
git add internal/console/regiondefault.go internal/console/regiondefault_test.go internal/console/apiregions.go internal/console/authcli.go internal/console/svcroutes_test.go
git commit -m "console: app_default with every reason, place names, POST regions/guess (nearest-region §9.3, §9.4)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```

---

### Task 16: The Instances tab lines

**Files:**
- Modify: `internal/console/server.go`
  - the `page` struct;
  - `handleAppInstances`, at `:2933-2965`.
- Modify: `internal/console/templates/app_instances.html:60-78`
- Test: `internal/console/regiondefault_test.go` (append)

**Interfaces:**
- Consumes: `s.appDefault` (Task 15) and `regionCopy` (Task 6).
- Produces: `page.RegionLine string` and `page.RegionLineWarn bool`.

- [ ] **Step 1: Write the failing test**

```go
// C7 only when the default is CONFIRMED available; C8 whenever it is
// confirmed unavailable; nothing for fleet, pooled, or a default that
// could not be read (codex plan-R1 14). Assertions compare the HTML-escaped
// copy, both ways (codex plan-R1 8).
func TestInstancesTabRegionLines(t *testing.T) {
	starts := html.EscapeString(fmt.Sprintf(regionCopy.StartsIn, ukPlace))
	cantUK := html.EscapeString(fmt.Sprintf(regionCopy.CantStart, "uk"))
	page := func(fx *consoleFixture) string {
		_, _, body := get(t, signInPaul(t, fx), fx.u("/apps/titogo/instances"))
		return body
	}
	anyLine := func(body string) bool { return strings.Contains(body, `id="app-region-line"`) }

	fx, _, _ := fillFixture(t, "uk")
	fx.srv.cfg.AppRegions = true
	if body := page(fx); !strings.Contains(body, starts) {
		t.Fatal("no C7 line for an available region")
	}
	fx.rel.Delete(t.Context(), "control/edges/uk-box.json")
	// No box publishes uk, so its place falls back to the bare code (§14).
	if body := page(fx); !strings.Contains(body, cantUK) || strings.Contains(body, starts) {
		t.Fatal("want C8, not C7, when the region is absent (codex 7)")
	}

	for _, setup := range []func(*consoleFixture){
		func(fx *consoleFixture) { fx.st.SetAppPlacement(OpsAccountID, "titogo", "fleet/acme") },
		func(fx *consoleFixture) {
			m := titoManifest()
			m.Pool = "tito"
			enableInstances(t, fx, "titogo", m)
		},
		func(fx *consoleFixture) { corruptManifest(t, fx) },
		func(fx *consoleFixture) { fx.srv.cfg.Releases = failingList{fx.rel} },
	} {
		fx, _, _ := fillFixture(t, "uk")
		setup(fx)
		if anyLine(page(fx)) {
			t.Error("a fleet, pooled or unreadable default showed a region line")
		}
	}
}
```

Add `"fmt"` and `"html"` to the file's imports. `anyLine` looks for the
line's element id, so it does not depend on the copy's wording.

- [ ] **Step 2: Run it to verify it fails**

Run: `go test ./internal/console -run TestInstancesTabRegionLines -count=1`
Expected: FAIL.

- [ ] **Step 3: Implement it**

In `page`:

```go
	// RegionLine is the Instances tab's app-region note (C7 or C8, spec
	// §10); "" shows nothing. RegionLineWarn marks C8.
	RegionLine     string
	RegionLineWarn bool
```

In `handleAppInstances`, after the placement section:

```go
	if d := s.appDefault(r.Context(), acct, p.AppName); d.Available != nil && d.Reason != "pooled" {
		switch {
		case !*d.Available:
			p.RegionLine, p.RegionLineWarn = fmt.Sprintf(regionCopy.CantStart, d.Name), true
		case d.Region != s.cfg.HomeRegion:
			p.RegionLine = fmt.Sprintf(regionCopy.StartsIn, d.Name)
		default:
			if files, err := readEdgesSnapshot(r.Context(), s.cfg.Releases); err == nil && eligibleCount(s.regionCands(files)) >= 2 {
				p.RegionLine = fmt.Sprintf(regionCopy.StartsIn, d.Name)
			}
		}
	}
```

`Available` is nil for fleet, policy-unreadable and edges-unreadable, so
those show nothing. Pooled is excluded by name.

In `app_instances.html`, directly after `<h2>Placement</h2>`:

```html
{{if .RegionLine}}<p id="app-region-line"{{if .RegionLineWarn}} class="banner error"{{end}}>{{.RegionLine}}</p>{{end}}
```

- [ ] **Step 4: Run the tests to verify they pass**

Run: `go test ./internal/console -count=1`
Expected: PASS.

- [ ] **Step 5: Commit**

```bash
git add internal/console/server.go internal/console/templates/app_instances.html internal/console/regiondefault_test.go
git commit -m "console: Instances tab names where new instances start, only when confirmed (nearest-region §10)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```

---

### Task 17: Roll-forward discrepancy log

**Files:**
- Create: `internal/console/regiondrift.go`
- Modify: `cmd/console/main.go` (call once after `NewServer`)
- Test: `internal/console/regiondrift_test.go`

**Interfaces:**
- Consumes: `Store.NonHomeAppRegions` (Task 8), `provision.ListRecords`, and
  `msgPlacementDiscrepancy` (Task 7).
- Produces: `func (s *Server) LogPlacementDiscrepancies(ctx context.Context)`.

- [ ] **Step 1: Write the failing test**

```go
// internal/console/regiondrift_test.go
package console

import (
	"context"
	"strings"
	"testing"
	"time"

	"github.com/carlosframework/platform/internal/provision"
	"github.com/carlosframework/platform/internal/release"
)

// codex R2-6: shapes that never inherit the app default are not listed;
// a no-region socket-backed instance created after the app is.
func TestPlacementDiscrepancyLog(t *testing.T) {
	fx := newConsoleFixture(t)
	fx.srv.cfg.HomeRegion = "ie"
	created := time.Now().Add(-time.Hour)
	fx.st.CreateAppInRegion(OpsAccountID, "blog", "p@k.dev", "uk", "chosen", created)
	ctx := context.Background()
	put := func(r provision.Record) {
		r.Schema, r.App, r.Account, r.Channel, r.CreatedAt = 1, "blog", release.OpsAccount, "stable", time.Now()
		if err := provision.CreateRecord(ctx, fx.rel, r); err != nil {
			t.Fatal(err)
		}
	}
	put(provision.Record{Host: "blog.go.tito.io", Kind: "static"})
	put(provision.Record{Host: "svc.go.tito.io", Kind: "service", Addrs: []string{"10.0.0.4:8080"}})
	put(provision.Record{Host: "r.go.tito.io", Region: "au", Role: "replica"})
	put(provision.Record{Host: "pooled.go.tito.io", Pool: "tito"})
	put(provision.Record{Host: "uk.go.tito.io", Region: "uk"})
	put(provision.Record{Host: "drift.go.tito.io"})
	var logs strings.Builder
	restore := captureLog(&logs)
	fx.srv.LogPlacementDiscrepancies(ctx)
	restore()
	out := logs.String()
	if !strings.Contains(out, msgPlacementDiscrepancy("drift.go.tito.io", "blog", "uk")) {
		t.Fatalf("drift not listed: %s", out)
	}
	for _, quiet := range []string{"blog.go.tito.io", "svc.go.tito.io", "r.go.tito.io", "pooled.go.tito.io", "uk.go.tito.io"} {
		if strings.Contains(out, quiet+" of") {
			t.Errorf("%s listed", quiet)
		}
	}
}
```

`provision.CreateRecord` is a plain create-only write (`provision.go:820`),
so these fixture records need no manifest.

- [ ] **Step 2: Run it to verify it fails**

Run: `go test ./internal/console -run TestPlacementDiscrepancyLog -count=1`
Expected: FAIL.

- [ ] **Step 3: Implement `regiondrift.go`**

```go
package console

import (
	"context"
	"log"

	"github.com/carlosframework/platform/internal/provision"
)

// discrepancyScanCap bounds one app's record listing. ListRecords treats
// max=0 as "list nothing", so the cap must be explicit.
const discrepancyScanCap = 10000

// LogPlacementDiscrepancies is spec §5.7 step 4, run once at console
// start. For each app holding a non-home region, it lists records with an
// empty region created after the app — the shape a console rolled back
// below the floor would have written. Shapes that never inherit the app
// default are skipped (static, addr-backed, replica, pooled, fleet). A
// deliberate home override looks the same and the record keeps no
// provenance, so each line is a CANDIDATE for review, worded as one (C29).
func (s *Server) LogPlacementDiscrepancies(ctx context.Context) {
	if s.cfg.Releases == nil {
		return
	}
	apps, err := s.store.NonHomeAppRegions()
	if err != nil {
		log.Printf("console: placement discrepancy check: %v", err)
		return
	}
	for _, a := range apps {
		recs, truncated, err := provision.ListRecords(ctx, s.cfg.Releases, a.Sqid, a.App, discrepancyScanCap)
		if err != nil {
			log.Printf("console: placement discrepancy check %s/%s: %v", a.Sqid, a.App, err)
			continue
		}
		if truncated {
			log.Printf("console: placement discrepancy check %s/%s: only the first %d records were checked", a.Sqid, a.App, discrepancyScanCap)
		}
		for _, r := range recs {
			if r.Region != "" || r.Role != "" || r.Pool != "" || !r.DerivesPaths() || r.CreatedAt.Before(a.CreatedAt) {
				continue
			}
			log.Print("console: " + msgPlacementDiscrepancy(r.Host, a.App, a.Region))
		}
	}
}
```

- [ ] **Step 4: Call it at startup**

In `cmd/console/main.go`, after `srv` is built and before serving:

```go
	// Spec §5.7: after any roll, list possible placement discrepancies for
	// operator review. Read-only; never blocks startup.
	go srv.LogPlacementDiscrepancies(context.Background())
```

- [ ] **Step 5: Run the tests to verify they pass**

Run: `go test ./internal/console ./cmd/console -count=1`
Expected: PASS.

- [ ] **Step 6: Commit**

```bash
git add internal/console/regiondrift.go internal/console/regiondrift_test.go cmd/console/main.go
git commit -m "console: list possible placement discrepancies on start (nearest-region §5.7)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```

---

### Task 18: CLI copy (C9 to C15, C13a, C13b, C15b, C15c, C22, C25, C30, C30b, C30c, from the record)

**Files:**
- Create: `region_copy.go` (root, package `main`)
- Test: `region_copy_test.go`

**Interfaces:**
- Consumes: the record's C9 to C15, C13a, C13b, C15b, C15c, C22a to C22d, C25, C30,
  C30b and C30c (Task 0).
- Produces:

```go
func regionCreatedLine(place, source string) string       // C9 to C12
const regionNotRecordedOldConsole                         // C13a
func regionNotAppliedSwitchOff(place string) string       // C13b
func instanceCreatedSuffix(channel, place string) string  // C14
func deployRegionMismatch(app, place string) string       // C15
func deployRegionUnreadable(app string, err error) string // C15b
func deployRegionUnknown(app string) string                // C15c
func canaryRegionMismatch(host, place string) string      // C25
func canaryRegionOnFleet(app, fleet string) string        // C30
func canaryFleetUnknown(app string) string                // C30b
func canaryRegionUnconfirmed(name string) string          // C30c
const flagRegionAppsCreate, flagRegionDeploy, flagRegionCanary, flagRegionInstancesCreate // C22a to C22d
```

- [ ] **Step 1: Write the failing test**

```go
// region_copy_test.go
package main

import (
	"errors"
	"strings"
	"testing"
)

func TestRegionCLICopy(t *testing.T) {
	lines := map[string]string{}
	for _, src := range []string{"timezone", "ip", "default", "chosen"} {
		lines[src] = regionCreatedLine("P", src)
	}
	if len(map[string]bool{lines["timezone"]: true, lines["ip"]: true, lines["default"]: true, lines["chosen"]: true}) != 4 {
		t.Fatalf("each source needs its own line: %v", lines)
	}
	if regionCreatedLine("P", "") != lines["default"] {
		t.Fatal("an unknown source must read as the default")
	}
	all := []string{
		regionNotRecordedOldConsole, regionNotAppliedSwitchOff("P"),
		instanceCreatedSuffix("stable", "P"), deployRegionMismatch("blog", "P"),
		deployRegionUnreadable("blog", errors.New("timeout")), deployRegionUnknown("blog"),
		canaryRegionMismatch("c.go.tito.io", "P"), canaryRegionOnFleet("blog", "acme"),
		canaryFleetUnknown("blog"), canaryRegionUnconfirmed("c"),
		flagRegionAppsCreate, flagRegionDeploy, flagRegionCanary, flagRegionInstancesCreate,
	}
	for _, s := range append(all, lines["timezone"], lines["ip"], lines["default"], lines["chosen"]) {
		if s == "" || strings.Contains(s, "—") || strings.Contains(s, "%!") {
			t.Errorf("bad copy %q", s)
		}
	}
}
```

- [ ] **Step 2: Run it to verify it fails**

Run: `go test . -run TestRegionCLICopy -count=1`
Expected: FAIL.

- [ ] **Step 3: Write the strings from the record**

The drafts are shown. Write each from its record entry verbatim.

```go
package main

import "fmt"

// The nearest-region CLI strings (spec §19 and this plan's additions), as
// approved in the nearest-region copy record. Change none of them without a
// new copy review.

// C9 to C12, by source word; anything else reads as the default.
func regionCreatedLine(place, source string) string {
	switch source {
	case "timezone":
		return fmt.Sprintf("Region: %s (from your timezone). Use --region to choose another.", place)
	case "ip":
		return fmt.Sprintf("Region: %s (from your network location). Use --region to choose another.", place)
	case "chosen":
		return fmt.Sprintf("Region: %s.", place)
	}
	return fmt.Sprintf("Region: %s (the platform default). Use --region to choose another.", place)
}

// C13a
const regionNotRecordedOldConsole = "This console does not record an app region yet, so --region was not applied."

// C13b
func regionNotAppliedSwitchOff(place string) string {
	return fmt.Sprintf("This console is not assigning app regions yet, so --region was not applied. This app's instances start in %s.", place)
}

// C14: the parenthesis content of "created instance {host} (...)".
func instanceCreatedSuffix(channel, place string) string {
	return fmt.Sprintf("channel %s, %s", channel, place)
}

// C15
func deployRegionMismatch(app, place string) string {
	return fmt.Sprintf("%s already runs in %s. --region only applies when deploy creates an app. To place one instance elsewhere, use carlos instances create --region.", app, place)
}

// C15b: deploy STOPS when it cannot read the app's region.
func deployRegionUnreadable(app string, err error) string {
	return fmt.Sprintf("could not read %s's region (%v), so deploy stopped before shipping anything. Try again, or run without --region.", app, err)
}

// C15c: the console answered, but could not read the app's region.
func deployRegionUnknown(app string) string {
	return fmt.Sprintf("the console could not read %s's region, so deploy stopped before shipping anything. Try again, or run without --region.", app)
}

// C25
func canaryRegionMismatch(host, place string) string {
	return fmt.Sprintf("canary %s already runs in %s. --region applies only when canary creates the host. Run carlos canary remove first to start it somewhere else.", host, place)
}

// C30
func canaryRegionOnFleet(app, fleet string) string {
	return fmt.Sprintf("%s runs on your fleet %s, which decides where every instance runs, so --region does not apply. Run the command without --region.", app, fleet)
}

// C30b: an older console cannot say whether a fleet governs.
func canaryFleetUnknown(app string) string {
	return fmt.Sprintf("this console can't say where %s's instances run, so --region can't be checked. Update the console, or run the command without --region.", app)
}

// C30c: the canary's own record could not be read, so whether it already
// runs elsewhere is unknown.
func canaryRegionUnconfirmed(name string) string {
	return fmt.Sprintf("could not confirm where canary %s runs, so nothing was changed. Try again, or run the command without --region.", name)
}

// C22a to C22d: flag help.
const (
	flagRegionAppsCreate      = "region this app's instances start in (default: the region nearest you)"
	flagRegionDeploy          = "region for the app this deploy creates (default: the region nearest you)"
	flagRegionCanary          = "region for the canary instance (default: the app's region)"
	flagRegionInstancesCreate = "region for this instance (default: the app's region, subject to the manifest's allow-list)"
)
```

- [ ] **Step 4: Run it to verify it passes**

Run: `go test . -run TestRegionCLICopy -count=1`
Expected: PASS.

- [ ] **Step 5: Commit**

```bash
git add region_copy.go region_copy_test.go
git commit -m "cli: region copy, as approved

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```

---

### Task 19: Timezone detection

**Files:**
- Create: `timezone.go` (root)
- Test: `timezone_test.go`

**Interfaces:**
- Produces:
  - `func localTimezone(getenv func(string) string, readlink func(string) (string, error), readFile func(string) ([]byte, error)) string`;
  - `func detectTimezone() string`, which wraps `localTimezone` with the `os`
    functions.

- [ ] **Step 1: Write the failing tests**

```go
// timezone_test.go
package main

import (
	"errors"
	"testing"
)

func TestLocalTimezone(t *testing.T) {
	noLink := func(string) (string, error) { return "", errors.New("no") }
	noFile := func(string) ([]byte, error) { return nil, errors.New("no") }
	env := func(v string) func(string) string {
		return func(k string) string {
			if k == "TZ" {
				return v
			}
			return ""
		}
	}
	cases := []struct {
		name     string
		getenv   func(string) string
		readlink func(string) (string, error)
		readFile func(string) ([]byte, error)
		want     string
	}{
		{"TZ plain", env("Europe/London"), noLink, noFile, "Europe/London"},
		{"TZ colon", env(":Europe/London"), noLink, noFile, "Europe/London"},
		{"TZ path", env("/usr/share/zoneinfo/Asia/Kolkata"), noLink, noFile, "Asia/Kolkata"},
		{"localtime link (linux)", env(""), func(string) (string, error) { return "/usr/share/zoneinfo/Europe/Dublin", nil }, noFile, "Europe/Dublin"},
		{"localtime link (macOS)", env(""), func(string) (string, error) { return "/var/db/timezone/zoneinfo/Australia/Sydney", nil }, noFile, "Australia/Sydney"},
		{"etc/timezone", env(""), noLink, func(string) ([]byte, error) { return []byte("Europe/Paris\n"), nil }, "Europe/Paris"},
		// Review Focus 1: real-machine noise is a clean name or nothing.
		{"etc/timezone padded", env(""), noLink, func(string) ([]byte, error) { return []byte("  America/New_York  \r\n# trailing\n"), nil }, "America/New_York"},
		{"TZ bare colon", env(":"), noLink, noFile, ""},
		{"TZ localtime", env("localtime"), noLink, noFile, ""},
		{"TZ posixrules path", env("/usr/share/zoneinfo/posixrules"), noLink, noFile, ""},
		{"TZ POSIX rule string", env("EST5EDT,M3.2.0,M11.1.0"), noLink, noFile, ""},
		{"nothing", env(""), noLink, noFile, ""},
	}
	for _, c := range cases {
		if got := localTimezone(c.getenv, c.readlink, c.readFile); got != c.want {
			t.Errorf("%s: %q, want %q", c.name, got, c.want)
		}
	}
}
```

- [ ] **Step 2: Run them to verify they fail**

Run: `go test . -run TestLocalTimezone -count=1`
Expected: FAIL.

- [ ] **Step 3: Implement `timezone.go`**

```go
package main

import (
	"os"
	"regexp"
	"strings"
)

// tzName is the shape of an IANA zone name the console will accept (spec
// §6.2 step 1). Anything else is not sent: the console then falls back to
// the request's IP, then the home.
var tzName = regexp.MustCompile(`^[A-Za-z0-9_+\-]+(/[A-Za-z0-9_+\-]+){0,2}$`)

var notAZone = map[string]bool{"localtime": true, "posixrules": true, "Local": true}

// localTimezone finds this machine's IANA zone name, portably (no build
// tags; the 20-cross gate builds it for darwin, windows and linux-arm64):
// $TZ, else the /etc/localtime symlink target, else /etc/timezone, else "".
// Windows has none of these, so only a valid $TZ is ever sent there.
func localTimezone(getenv func(string) string, readlink func(string) (string, error), readFile func(string) ([]byte, error)) string {
	if tz := clean(strings.TrimPrefix(strings.TrimSpace(getenv("TZ")), ":")); tz != "" {
		return tz
	}
	if target, err := readlink("/etc/localtime"); err == nil {
		if tz := clean(target); tz != "" {
			return tz
		}
	}
	if b, err := readFile("/etc/timezone"); err == nil {
		line, _, _ := strings.Cut(string(b), "\n")
		return clean(line)
	}
	return ""
}

func clean(s string) string {
	s = strings.TrimSpace(s)
	if _, after, ok := strings.Cut(s, "zoneinfo/"); ok {
		s = after
	}
	if s == "" || strings.HasPrefix(s, "/") || notAZone[s] || len(s) > 64 || !tzName.MatchString(s) {
		return ""
	}
	return s
}

func detectTimezone() string { return localTimezone(os.Getenv, os.Readlink, os.ReadFile) }
```

- [ ] **Step 4: Run them to verify they pass, and cross-compile**

Run:

```bash
go test . -run TestLocalTimezone -count=1
fail=0
for t in darwin/arm64 darwin/amd64 linux/arm64 windows/amd64 windows/arm64; do
  CGO_ENABLED=0 GOOS=${t%/*} GOARCH=${t#*/} go build ./... || { echo "FAILED $t"; fail=1; }
done
[ "$fail" = 0 ] || exit 1  # stops this block before any commit below it
```

Expected: PASS, and no `FAILED` line.

- [ ] **Step 5: Commit**

```bash
git add timezone.go timezone_test.go
git commit -m "cli: portable timezone detection (nearest-region §11)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```

---

### Task 20: Create-app wire, `apps create --region` and the onboarding line

**Files:**
- Modify: `apimode.go:958-1015` (`apiCreateAppResult`, `createApp`)
- Modify: `apps.go:442-510` (`runAppsCreate`)
- Modify: `deploy.go:513-541` (`offerCreateApp`), `deploy.go:36-60` (`runDeploy` flags)
- Modify: `apps_test.go:299-321` and every other `createApp(` call site (`grep -n 'createApp(' *.go`)
- Test: `apps_region_test.go`

**Interfaces:**
- Consumes: Tasks 18 and 19.
- Produces:
  - `func (s *apiSession) createApp(ctx context.Context, a apiAcct, app, place string, controlPlane bool, buildProbe, region, timezone string) (apiCreateAppResult, error)`;
  - new result fields `Region`, `RegionName`, `RegionSource string` and
    `RegionsAvailable int`;
  - `func printAppRegion(w io.Writer, res apiCreateAppResult, asked string)`.

- [ ] **Step 1: Write the failing tests**

```go
// apps_region_test.go
package main

import (
	"bytes"
	"context"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"testing"
)

func TestCreateAppSendsRegionAndTimezone(t *testing.T) {
	var got map[string]any
	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		json.NewDecoder(r.Body).Decode(&got)
		json.NewEncoder(w).Encode(map[string]any{"account": "a", "app": "blog",
			"region": "uk", "region_name": "UK · London", "region_source": "timezone", "regions_available": 3})
	}))
	defer ts.Close()
	res, err := newTestSession(ts.URL).createApp(context.Background(), apiAcct{Seg: "a"}, "blog", "", false, "", "", "Europe/London")
	if err != nil {
		t.Fatal(err)
	}
	if got["timezone"] != "Europe/London" {
		t.Fatalf("body = %v", got)
	}
	if _, sent := got["region"]; sent {
		t.Fatal("region sent without --region")
	}
	if res.RegionName != "UK · London" || res.RegionsAvailable != 3 {
		t.Fatalf("result = %+v", res)
	}
}

func TestPrintAppRegion(t *testing.T) {
	cases := []struct {
		res   apiCreateAppResult
		asked string
		want  string
	}{
		{apiCreateAppResult{Region: "uk", RegionName: "UK · London", RegionSource: "timezone", RegionsAvailable: 2}, "", regionCreatedLine("UK · London", "timezone") + "\n"},
		{apiCreateAppResult{Region: "ie", RegionName: "Ireland · Dublin", RegionSource: "default", RegionsAvailable: 1}, "", ""},
		{apiCreateAppResult{}, "uk", regionNotRecordedOldConsole + "\n"},
		{apiCreateAppResult{Region: "ie", RegionName: "Ireland · Dublin", RegionSource: "default", RegionsAvailable: 2}, "uk", regionNotAppliedSwitchOff("Ireland · Dublin") + "\n"},
		{apiCreateAppResult{}, "", ""},
	}
	for i, c := range cases {
		var b bytes.Buffer
		printAppRegion(&b, c.res, c.asked)
		if b.String() != c.want {
			t.Errorf("case %d: %q, want %q", i, b.String(), c.want)
		}
	}
}
```

- [ ] **Step 2: Run them to verify they fail**

Run: `go test . -run 'TestCreateAppSendsRegionAndTimezone|TestPrintAppRegion' -count=1`
Expected: FAIL to compile.

- [ ] **Step 3: Extend the wire**

In `apiCreateAppResult`:

```go
	// The app region the console STORED (nearest-region spec §9.1), under
	// its name. Absent from an older console, which the CLI reads as "no
	// app region recorded".
	Region           string `json:"region"`
	RegionName       string `json:"region_name"`
	RegionSource     string `json:"region_source"`
	RegionsAvailable int    `json:"regions_available"`
```

Replace `createApp`'s signature and body struct:

```go
func (s *apiSession) createApp(ctx context.Context, a apiAcct, app, place string, controlPlane bool, buildProbe, region, timezone string) (apiCreateAppResult, error) {
	in := struct {
		Name         string `json:"name"`
		Place        string `json:"place,omitempty"`
		ControlPlane bool   `json:"control_plane,omitempty"`
		BuildProbe   string `json:"build_probe,omitempty"`
		Region       string `json:"region,omitempty"`
		Timezone     string `json:"timezone,omitempty"`
	}{app, place, controlPlane, buildProbe, region, timezone}
```

Leave the rest of the function unchanged. Update every other call site
(codex plan-R2 5). The callers are:

- the tests: `apimode_test.go:3041`, `:3052`, `:3060`, `:3066`, `:3099`,
  `:3106` and `apps_test.go:311`, each gaining `, "", ""`;
- `apps.go:497` and `deploy.go:536`, which get the real values in steps 4
  and 5.

`offerCreateApp` also changes signature in step 5, so update its one test
caller, `deploy_wizard_test.go:104`, to pass `""` as the new last argument:

```go
	app := offerCreateApp(context.Background(), s, apiAcct{Seg: "bdk", Exact: true},
		&noAppsError{Account: "tom@keymail.dev"}, "")
```

Run `grep -n 'createApp(\|offerCreateApp(' *.go` afterwards. Every match
must carry the new arity.

- [ ] **Step 4: Add `printAppRegion` and wire `apps create`**

In `region_copy.go`'s neighbour, a new file `apps_region.go`:

```go
package main

import (
	"fmt"
	"io"
)

// printAppRegion is spec §11's one line after "created app …": nothing on
// a single-region deployment, C13a when an older console ignored --region,
// C13b when a console with the switch off did, else C9 to C12 by source.
func printAppRegion(w io.Writer, res apiCreateAppResult, asked string) {
	switch {
	case res.Region == "" && res.RegionSource == "":
		if asked != "" {
			fmt.Fprintln(w, regionNotRecordedOldConsole)
		}
	case asked != "" && res.Region != asked:
		fmt.Fprintln(w, regionNotAppliedSwitchOff(res.RegionName))
	case res.RegionsAvailable >= 2:
		fmt.Fprintln(w, regionCreatedLine(res.RegionName, res.RegionSource))
	}
}
```

In `runAppsCreate`, register the flag and pass it on:

```go
	region := fs.String("region", "", flagRegionAppsCreate)
```

```go
	res, err := sess.createApp(context.Background(), acct, strings.TrimSpace(*app), place2, *controlPlane, probe2,
		strings.TrimSpace(*region), detectTimezone())
```

After the existing "created app" line at `apps.go:502`:

```go
	printAppRegion(stdout, res, strings.TrimSpace(*region))
```

Add the record's U1 text to the usage string at `apps.go:463`.

- [ ] **Step 5: Wire `deploy`'s onboarding**

In `runDeploy`'s flags:

```go
	regionFlag := fs.String("region", "", flagRegionDeploy)
```

Pass it to `offerCreateApp(ctx, sess, acct, na, strings.TrimSpace(*regionFlag))`.
Change that function's signature to take `region string`, and its create call
and output to:

```go
	res, err := sess.createApp(ctx, acct, name, "", false, "", region, detectTimezone())
	if err != nil {
		log.Fatalf("deploy: create app: %v", err)
	}
	fmt.Fprintf(stdout, "%s %s in account %s\n", colorFor(stdout).ok("created app"), res.App, res.Account)
	printAppRegion(stdout, res, region)
	return res.App
```

Add the record's U2 text to `runDeploy`'s usage string.

- [ ] **Step 6: Run the root package tests**

Run: `go test . -count=1 -timeout 30m`
Expected: PASS. The e2e tests shell out through `carlosTestEnv`, so nothing
reaches a real console.

- [ ] **Step 7: Commit**

```bash
git add apimode.go apps.go apps_region.go apps_region_test.go deploy.go apps_test.go apimode_test.go deploy_wizard_test.go
git commit -m "cli: apps create and deploy onboarding send a timezone, take --region, print the region line (nearest-region §11)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```

---

### Task 21: `deploy --region` on an existing app

**Files:**
- Modify: `apimode.go` (add `appRegions`)
- Modify: `deploy.go` (`runDeploy`: `createdNow`, and the preflight before any ship)
- Test: `deploy_region_test.go`, `deploy_region_e2e_test.go`

**Interfaces:**
- Consumes: `deployRegionMismatch`, `deployRegionUnreadable` and
  `deployRegionUnknown` (Task 18).
- Produces:

```go
type apiAppDefault struct {
	Region, Name, Source, Effective, Reason, InstanceDomain string
	Available                                               *bool
	RegionKnown, PlacementKnown                             bool
}
type apiRegionEntry struct{ Code, Name string }
type apiRegionsResult struct {
	Regions    []apiRegionEntry `json:"regions"`
	Default    string           `json:"default"`
	AppDefault *apiAppDefault   `json:"app_default"`
}
func (s *apiSession) appRegions(ctx context.Context, a apiAcct, app string) (apiRegionsResult, error)
func (r apiRegionsResult) nameOf(code string) string // a place name, else the code
func deployRegionPreflight(res apiRegionsResult, readErr error, app, asked string) error
```

`regionE2EConsole` is a root-package test helper, reused by Task 22. It
counts writes, and separately the instance-create requests (`creates`), so
Task 22 can tell a duplicate create from the other writes.

- [ ] **Step 1: Write the failing unit test**

```go
// deploy_region_test.go
package main

import (
	"errors"
	"testing"
)

// codex plan-R1 2: a READ failure stops deploy; only a successful response
// without app_default (an older console) skips the check.
func TestDeployRegionPreflight(t *testing.T) {
	uk := &apiAppDefault{Region: "uk", Name: "P-uk", Effective: "uk", RegionKnown: true, PlacementKnown: true}
	fleet := &apiAppDefault{Region: "uk", Name: "P-uk", Effective: "fleet/acme", Reason: "fleet", RegionKnown: true, PlacementKnown: true}
	boom := errors.New("500 from console")
	cases := []struct {
		name  string
		res   apiRegionsResult
		err   error
		asked string
		want  string
	}{
		{"no flag", apiRegionsResult{AppDefault: uk}, boom, "", ""},
		{"equal", apiRegionsResult{AppDefault: uk}, nil, "uk", ""},
		{"different", apiRegionsResult{AppDefault: uk}, nil, "ie", deployRegionMismatch("blog", "P-uk")},
		{"older console", apiRegionsResult{}, nil, "ie", ""},
		{"read failed", apiRegionsResult{}, boom, "ie", deployRegionUnreadable("blog", boom)},
		{"fleet, stored equal", apiRegionsResult{AppDefault: fleet}, nil, "uk", ""},
		{"fleet, stored different", apiRegionsResult{AppDefault: fleet}, nil, "ie", deployRegionMismatch("blog", "P-uk")},
		// codex plan-R2 3: a 200 whose stored region could not be read.
		{"region unknown", apiRegionsResult{AppDefault: &apiAppDefault{RegionKnown: false, PlacementKnown: true}}, nil, "ie", deployRegionUnknown("blog")},
	}
	for _, c := range cases {
		err := deployRegionPreflight(c.res, c.err, "blog", c.asked)
		got := ""
		if err != nil {
			got = err.Error()
		}
		if got != c.want {
			t.Errorf("%s: %q, want %q", c.name, got, c.want)
		}
	}
}
```

- [ ] **Step 2: Run it to verify it fails**

Run: `go test . -run TestDeployRegionPreflight -count=1`
Expected: FAIL.

- [ ] **Step 3: Implement the read and the preflight**

In `apimode.go`:

```go
// apiAppDefault, apiRegionEntry and apiRegionsResult are the CLI's view of
// GET …/regions (nearest-region spec §9.3). AppDefault is nil from a
// console that predates it.
type apiAppDefault struct {
	Region         string `json:"region"`
	Name           string `json:"name"`
	Source         string `json:"source"`
	Effective      string `json:"effective"`
	Reason         string `json:"reason"`
	Available      *bool  `json:"available"`
	RegionKnown    bool   `json:"region_known"`
	PlacementKnown bool   `json:"placement_known"`
	InstanceDomain string `json:"instance_domain"`
}

type apiRegionEntry struct {
	Code string `json:"code"`
	Name string `json:"name"`
}

type apiRegionsResult struct {
	Regions    []apiRegionEntry `json:"regions"`
	Default    string           `json:"default"`
	AppDefault *apiAppDefault   `json:"app_default"`
}

// nameOf is the place name the console gave code, else the code itself.
func (r apiRegionsResult) nameOf(code string) string {
	for _, e := range r.Regions {
		if e.Code == code && e.Name != "" {
			return e.Name
		}
	}
	return code
}

func (s *apiSession) appRegions(ctx context.Context, a apiAcct, app string) (apiRegionsResult, error) {
	status, body, err := s.getJSON(ctx, apiPath(a, app, "/regions"))
	if err != nil {
		return apiRegionsResult{}, err
	}
	if status != http.StatusOK {
		return apiRegionsResult{}, apiErrorFromBody(status, body)
	}
	var out apiRegionsResult
	if err := json.Unmarshal(body, &out); err != nil {
		return apiRegionsResult{}, fmt.Errorf("regions: unreadable response: %w", err)
	}
	return out, nil
}
```

In `deploy.go`:

```go
// deployRegionPreflight is spec §11: --region on an EXISTING app is
// compared with the app's STORED region before anything ships. A failed
// read STOPS the deploy (C15b), and so does a successful answer whose
// stored region the console could not read (C15c, codex plan-R2 3):
// either way the region is unknown, and shipping would go to whatever the
// app already is. Only a successful response without app_default — an
// older console — skips the check.
func deployRegionPreflight(res apiRegionsResult, readErr error, app, asked string) error {
	if asked == "" {
		return nil
	}
	if readErr != nil {
		return errors.New(deployRegionUnreadable(app, readErr))
	}
	if res.AppDefault == nil {
		return nil
	}
	if !res.AppDefault.RegionKnown {
		return errors.New(deployRegionUnknown(app))
	}
	if res.AppDefault.Region == asked {
		return nil
	}
	return errors.New(deployRegionMismatch(app, res.AppDefault.Name))
}
```

In `runDeploy`, declare `createdNow := false` just before `if *app == "" {`,
and set it in the onboarding case:

```go
		case errors.As(aerr, &na) && interactiveTerminal():
			*app = offerCreateApp(ctx, sess, acct, na, strings.TrimSpace(*regionFlag))
			newly["app"] = *app
			createdNow = true
```

Immediately after that block closes, before `readCapabilities` and any ship:

```go
	if r := strings.TrimSpace(*regionFlag); r != "" && !createdNow {
		res, err := sess.appRegions(ctx, acct, *app)
		if err := deployRegionPreflight(res, err, *app, r); err != nil {
			log.Fatalf("deploy: %v", err)
		}
	}
```

- [ ] **Step 4: Run the unit test to verify it passes**

Run: `go test . -run TestDeployRegionPreflight -count=1`
Expected: PASS.

- [ ] **Step 5: Write the command-level test (codex plan-R1 2, 12)**

This test runs the built `carlos` binary against an in-process console. It
proves that every refusal happens before any write: no POST reaches the
console, and no release object lands in the store.

```go
// deploy_region_e2e_test.go
package main

import (
	"context"
	"encoding/json"
	"errors"
	"net/http"
	"net/http/httptest"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"sync/atomic"
	"testing"
	"time"

	"github.com/carlosframework/platform/internal/console"
	"github.com/carlosframework/platform/internal/release"
)

// regionE2E is an in-process console with titogo claimed at appRegion, the
// signed-in CLI credentials saved, and a front that can fail or strip the
// /regions answer and counts every write reaching the console.
type regionE2E struct {
	bin    string // built before useTempCredentials changes directory
	creds  string
	url    string
	store  release.Store
	mode   atomic.Value // "", "fail", "legacy", "region-unknown", "placement-unknown"
	writes atomic.Int64
	// creates counts instance-create requests (POST …/instances), so a
	// test can tell a duplicate create from the other writes (Task 22).
	creates atomic.Int64
	cst     *console.Store
	// failGet is a store key whose reads fail while the listing succeeds,
	// the way one bucket GET can (Task 22, codex plan-R3 1).
	failGet atomic.Value
}

// flakyStore fails every read of one key.
type flakyStore struct {
	release.Store
	key *atomic.Value
}

func (f flakyStore) Get(ctx context.Context, key string) ([]byte, string, error) {
	if k, _ := f.key.Load().(string); k != "" && k == key {
		return nil, "", errors.New("read timeout")
	}
	return f.Store.Get(ctx, key)
}

func regionE2EConsole(t *testing.T, appRegion string) *regionE2E {
	t.Helper()
	bin := buildCarlos(t) // first: useTempCredentials chdirs out of the module
	creds := useTempCredentials(t)
	clearDeploymentEnv(t)
	const addr, app = "paul@keymail.dev", "titogo"
	tmp := t.TempDir()
	e := &regionE2E{bin: bin, creds: creds, store: release.NewDirStore(filepath.Join(tmp, "store"))}
	e.mode.Store("")
	e.failGet.Store("")
	seed, pubB64, err := release.GenerateKey()
	if err != nil {
		t.Fatal(err)
	}
	priv, err := release.ParseSigningKey(seed, "e2e")
	if err != nil {
		t.Fatal(err)
	}
	pub, err := release.ParsePublicKey(pubB64, "e2e")
	if err != nil {
		t.Fatal(err)
	}
	e.cst, err = console.Open(filepath.Join(tmp, "console.db"))
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { e.cst.Close() })
	var srv *console.Server
	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodGet && r.Method != http.MethodHead && !strings.Contains(r.URL.Path, "/auth/") {
			e.writes.Add(1)
		}
		if r.Method == http.MethodPost && strings.HasSuffix(r.URL.Path, "/instances") {
			e.creates.Add(1)
		}
		if strings.HasSuffix(r.URL.Path, "/regions") {
			switch e.mode.Load().(string) {
			case "fail":
				http.Error(w, `{"error":"boom"}`, http.StatusInternalServerError)
				return
			case "legacy", "region-unknown", "placement-unknown":
				// A successful 200 whose body an older console, or a
				// console that could not read one fact, would send.
				rec := httptest.NewRecorder()
				srv.ServeHTTP(rec, r)
				var m map[string]any
				json.Unmarshal(rec.Body.Bytes(), &m)
				d, _ := m["app_default"].(map[string]any)
				switch e.mode.Load().(string) {
				case "legacy":
					delete(m, "app_default")
				case "region-unknown":
					d["region_known"], d["region"], d["effective"] = false, "", ""
				case "placement-unknown":
					d["placement_known"], d["reason"] = false, "policy-unreadable"
				}
				w.Header().Set("Content-Type", "application/json")
				json.NewEncoder(w).Encode(m)
				return
			}
		}
		srv.ServeHTTP(w, r)
	}))
	t.Cleanup(ts.Close)
	e.url = ts.URL
	srv, err = console.NewServer(console.Config{
		Origin: ts.URL, Keymail: "http://keymail.invalid",
		Registry: filepath.Join(tmp, "registry.db"), Releases: flakyStore{e.store, &e.failGet},
		ReleasePub: pub, Key: priv, AppScheme: "http", AppsDomain: "oncarlos.test",
		HomeRegion: "ie",
	}, e.cst)
	if err != nil {
		t.Fatal(err)
	}
	if err := e.cst.AddMember(console.OpsAccountID, addr, console.RoleOwner, time.Now()); err != nil {
		t.Fatal(err)
	}
	if _, err := e.cst.CreateAppInRegion(console.OpsAccountID, app, addr, appRegion, "chosen", time.Now()); err != nil {
		t.Fatal(err)
	}
	token, expiresAt := e2eDeviceLogin(t, ts.URL, e.cst, addr, "e2e@laptop")
	credFile, err := loadCredentials()
	if err != nil {
		t.Fatal(err)
	}
	credFile.put(credential{Console: ts.URL, Address: addr, Token: token, ExpiresAt: expiresAt})
	if err := credFile.save(); err != nil {
		t.Fatal(err)
	}
	e.writes.Store(0) // the login above is setup, not the command under test
	return e
}

// run runs the built carlos binary as a signed-in user with no project
// config in reach.
func (e *regionE2E) run(t *testing.T, args ...string) (string, error) {
	t.Helper()
	cmd := exec.Command(e.bin, args...)
	cmd.Env = carlosTestEnv(t, "CARLOS_CREDENTIALS="+e.creds)
	cmd.Dir = t.TempDir()
	out, err := cmd.CombinedOutput()
	return string(out), err
}

func (e *regionE2E) releases(t *testing.T) int {
	t.Helper()
	keys, err := e.store.List(context.Background(), release.AppPrefix(release.OpsAccount, "titogo"))
	if err != nil {
		t.Fatal(err)
	}
	n := 0
	for _, k := range keys {
		if strings.Contains(k, "/releases/") {
			n++
		}
	}
	return n
}

func artifact(t *testing.T) string {
	t.Helper()
	p := filepath.Join(t.TempDir(), "app")
	if err := os.WriteFile(p, []byte("#!/bin/sh\n"), 0o755); err != nil {
		t.Fatal(err)
	}
	return p
}

func TestDeployRegionRefusalsShipNothing(t *testing.T) {
	for _, c := range []struct{ mode, want string }{
		{"", deployRegionMismatch("titogo", "")[:20]},          // C15: the stored region is uk
		{"fail", "deploy stopped before shipping anything"},    // C15b
		{"region-unknown", deployRegionUnknown("titogo")[:30]}, // C15c: a 200 that does not know
	} {
		// A subtest each: useTempCredentials chdirs, and only a subtest
		// restores the directory before the next case builds the binary.
		t.Run("mode="+c.mode, func(t *testing.T) {
			e := regionE2EConsole(t, "uk")
			e.mode.Store(c.mode)
			out, err := e.run(t, "deploy", "--app", "titogo", "--region", "ie", "--console", e.url,
				"--channel", "canary/e2e", "--version", "abc1234", "--timeout", "1s", artifact(t))
			if err == nil || !strings.Contains(out, c.want) {
				t.Fatalf("err=%v out=%s, want a refusal containing %q", err, out, c.want)
			}
			if n := e.writes.Load(); n != 0 {
				t.Fatalf("%d write(s) reached the console before the refusal", n)
			}
			if n := e.releases(t); n != 0 {
				t.Fatalf("%d release object(s) were shipped", n)
			}
		})
	}
}

// An older console (a successful /regions without app_default) skips the
// check, and the deploy goes on to ship.
func TestDeployRegionSkipsAgainstAnOlderConsole(t *testing.T) {
	e := regionE2EConsole(t, "uk")
	e.mode.Store("legacy")
	out, _ := e.run(t, "deploy", "--app", "titogo", "--region", "ie", "--console", e.url,
		"--channel", "canary/e2e", "--version", "abc1234", "--timeout", "1s", artifact(t))
	if strings.Contains(out, deployRegionMismatch("titogo", "")[:20]) || strings.Contains(out, "deploy stopped before shipping") {
		t.Fatalf("an older console refused: %s", out)
	}
	if e.writes.Load() == 0 || e.releases(t) == 0 {
		t.Fatalf("deploy never shipped: %s", out)
	}
}
```

The `want` fragments come from the copy functions themselves, so they follow
the approved text. The C15b fragment is a literal substring of its draft; if
review changed it, use a substring of the approved text.
`useTempCredentials`, `clearDeploymentEnv`, `e2eDeviceLogin`,
`loadCredentials`, `credential` and `buildCarlos` are the existing root test
helpers used by `TestE2EDeployThroughConsole`
(`deploy_e2e_test.go:266-330`). `useTempCredentials` calls `t.Chdir`
(`auth_test.go:26`), which is why the binary is built first.

- [ ] **Step 6: Run the command-level tests**

Run: `go test . -run 'TestDeployRegion' -count=1 -timeout 30m`
Expected: PASS.

- [ ] **Step 7: Commit**

```bash
git add apimode.go deploy.go deploy_region_test.go deploy_region_e2e_test.go
git commit -m "cli: deploy --region checks the stored region before shipping and stops on a failed read (nearest-region §11)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```

---

### Task 22: `canary --region` and the instance output

**Files:**
- Modify: `canary.go`
  - `runCanaryDeploy`, at `:338-470`;
  - `canaryUsage`, at `:38`, with record entry U3.
- Modify: `instances_cli.go:288-330` (flag help and output)
- Modify: `apimode.go` (`apiInstanceRecord`: add `RegionName`, `RegionFrom`;
  add `instanceGet`)
- Test: `canary_region_test.go`, `canary_region_e2e_test.go`

**Interfaces:**
- Consumes:
  - Task 18: `canaryRegionOnFleet`, `canaryFleetUnknown`,
    `canaryRegionMismatch` and `instanceCreatedSuffix`;
  - Task 18: `canaryRegionUnconfirmed` (C30c);
  - Task 21: `appRegions`, `apiRegionsResult.nameOf`, `apiAppDefault`
    with `PlacementKnown` and `InstanceDomain`, and `regionE2EConsole`
    with its `failGet` store;
  - Task 15: `instance_domain` on `app_default`.
- Produces:
  - `func canaryRegionCheck(res apiRegionsResult, readErr error, app, name, asked string, get func(host string) (apiInstanceRecord, bool, error)) (canaryPreflight, error)`;
  - `type canaryPreflight struct{ Host string; Rec apiInstanceRecord; Found bool }`;
  - `func canaryPlanFor(pre canaryPreflight, recs []apiInstanceRecord, host, channel string) canaryPlan`;
  - `func (s *apiSession) instanceGet(ctx context.Context, a apiAcct, app, host string) (apiInstanceRecord, bool, error)`;
  - `func instanceCreatedLine(rec apiInstanceRecord, envSuffix string) string`.

The check runs once, before `instancesEnable` (`canary.go:381`), which is a
PUT. It uses only reads: `GET …/regions`, then ONE
`GET …/instances/{host}` (`handleAPIInstanceGet`, `apiinstances.go:485`,
route `authcli.go:614`) for the canary's own host. The host is
`canaryHost(name, app, instance_domain)` (`canary.go:210`), named from
Task 15's `instance_domain` before any write (codex plan-R3 1).

The list (`GET …/instances`) cannot decide this. It stops at 500 records
(`instanceListMax`), sorted by host, and `provision.ListRecords` skips any
record it cannot read (`provision.go:1192-1196`), so a canary past the cap,
or one whose read fails, is invisible to it. The single GET answers 404 only
when the record does not exist (`GetRecord`'s `os.ErrNotExist`); any other
failure is a 502, which refuses with C30c.

`--region` needs both knowledge flags (spec R9a, rev 6.2): the fleet check
reads `effective`, which is empty when the stored region was not read. A
false `region_known` refuses with C30b like a false `placement_known`
(codex plan-R4 1), and the e2e has a fleet-placed case with the region
unread that must refuse with zero writes.

The record that one GET returns, and whether it exists, also decide what
happens next (codex plan-R5 1). `canaryRegionCheck` returns them in a
`canaryPreflight`, and `canaryPlanFor` turns that into create, rechannel or
ready. With `--region` the direct read decides, so a same-region canary the
capped list never showed is reused or rechannelled, never created twice
(which the door would refuse). The `--environment` bind uses the same
decision. Without `--region` nothing is read and the list decides, exactly
as today.

After the PUT, the host built from the PUT's domain must equal the host the
check read. If it does not (an operator re-homed the manifest between the
two), the command stops with C30c before any record write.

- [ ] **Step 1: Write the failing unit tests**

```go
// canary_region_test.go
package main

import (
	"errors"
	"testing"
)

// records is a fake single-record GET: a host it holds is found, any other
// is a 404 (found false), and err fails every read.
func records(err error, recs ...apiInstanceRecord) (func(string) (apiInstanceRecord, bool, error), *[]string) {
	var asked []string
	return func(host string) (apiInstanceRecord, bool, error) {
		asked = append(asked, host)
		if err != nil {
			return apiInstanceRecord{}, false, err
		}
		for _, r := range recs {
			if r.Host == host {
				return r, true, nil
			}
		}
		return apiInstanceRecord{}, false, nil
	}, &asked
}

func TestCanaryRegionCheck(t *testing.T) {
	names := []apiRegionEntry{{Code: "ie", Name: "P-ie"}, {Code: "uk", Name: "P-uk"}}
	known := func(d apiAppDefault) *apiAppDefault {
		d.RegionKnown, d.PlacementKnown, d.InstanceDomain = true, true, "go.tito.io"
		return &d
	}
	plain := apiRegionsResult{Regions: names, Default: "ie", AppDefault: known(apiAppDefault{Region: "uk", Effective: "uk"})}
	fleet := apiRegionsResult{Regions: names, Default: "ie", AppDefault: known(apiAppDefault{Region: "uk", Effective: "fleet/acme", Reason: "fleet"})}
	older := apiRegionsResult{Regions: names, Default: "ie"} // no app_default
	unread := apiRegionsResult{Regions: names, Default: "ie", AppDefault: &apiAppDefault{Region: "uk", Effective: "uk", RegionKnown: true, Reason: "policy-unreadable"}}
	noDomain := apiRegionsResult{Regions: names, Default: "ie", AppDefault: &apiAppDefault{Region: "uk", Effective: "uk", RegionKnown: true, PlacementKnown: true}}
	regionUnknownFleet := apiRegionsResult{Regions: names, Default: "ie", AppDefault: &apiAppDefault{PlacementKnown: true, Reason: "fleet", InstanceDomain: "go.tito.io"}}
	regionUnknown := apiRegionsResult{Regions: names, Default: "ie", AppDefault: &apiAppDefault{PlacementKnown: true, InstanceDomain: "go.tito.io"}}
	boom := errors.New("timeout")
	const host = "c.blog.go.tito.io"
	existingUK := apiInstanceRecord{Host: host, Region: "uk"}
	existingHome := apiInstanceRecord{Host: host, Region: ""}
	other := apiInstanceRecord{Host: "d.blog.go.tito.io", Region: "uk"}
	cases := []struct {
		name    string
		res     apiRegionsResult
		err     error
		recs    []apiInstanceRecord
		readErr error
		asked   string
		want    string
	}{
		{"no flag", fleet, boom, []apiInstanceRecord{existingUK}, nil, "", ""},
		{"new canary", plain, nil, nil, nil, "uk", ""},
		{"another canary is not this one", plain, nil, []apiInstanceRecord{other}, nil, "ie", ""},
		{"existing, same", plain, nil, []apiInstanceRecord{existingUK}, nil, "uk", ""},
		{"existing home read as its name", plain, nil, []apiInstanceRecord{existingHome}, nil, "ie", ""},
		{"existing, different, named by place", plain, nil, []apiInstanceRecord{existingUK}, nil, "ie", canaryRegionMismatch(host, "P-uk")},
		{"fleet, first run", fleet, nil, nil, nil, "uk", canaryRegionOnFleet("blog", "acme")},
		{"fleet, repeat", fleet, nil, []apiInstanceRecord{existingUK}, nil, "uk", canaryRegionOnFleet("blog", "acme")},
		{"older console, first run", older, nil, nil, nil, "uk", canaryFleetUnknown("blog")},
		{"older console, repeat", older, nil, []apiInstanceRecord{existingUK}, nil, "uk", canaryFleetUnknown("blog")},
		{"read failed", plain, boom, nil, nil, "uk", canaryFleetUnknown("blog")},
		// codex plan-R2 3: a 200 whose placement the console could not read.
		{"placement unknown", unread, nil, nil, nil, "uk", canaryFleetUnknown("blog")},
		// codex plan-R3 1: the canary's own record is read, never inferred
		// from a list; a failed read or an unknown domain refuses.
		{"record read failed", plain, nil, []apiInstanceRecord{existingUK}, boom, "uk", canaryRegionUnconfirmed("c")},
		{"no instance domain", noDomain, nil, nil, nil, "uk", canaryRegionUnconfirmed("c")},
		// codex plan-R4 1, R9a: with the stored region unread, effective is
		// empty, so a fleet cannot be seen; the flag is refused either way.
		{"region unknown, fleet", regionUnknownFleet, nil, nil, nil, "uk", canaryFleetUnknown("blog")},
		{"region unknown", regionUnknown, nil, nil, nil, "uk", canaryFleetUnknown("blog")},
	}
	for _, c := range cases {
		get, asked := records(c.readErr, c.recs...)
		pre, err := canaryRegionCheck(c.res, c.err, "blog", "c", c.asked, get)
		got := ""
		if err != nil {
			got = err.Error()
		}
		if got != c.want {
			t.Errorf("%s: %q, want %q", c.name, got, c.want)
		}
		if len(*asked) > 1 || (len(*asked) == 1 && (*asked)[0] != host) {
			t.Errorf("%s: read %v, want at most one read of %s", c.name, *asked, host)
		}
		if c.want == "" && c.asked != "" && pre.Host != host {
			t.Errorf("%s: checked %q, want %q", c.name, pre.Host, host)
		}
		if c.want == "" && c.asked != "" && pre.Found != (len(c.recs) > 0 && c.recs[0].Host == host) {
			t.Errorf("%s: found = %v, want the read's own answer", c.name, pre.Found)
		}
	}
}

// codex plan-R5 1: with --region, the direct read decides the plan, even
// when the capped list does not show the record at all.
func TestCanaryPlanFor(t *testing.T) {
	const host, ch = "c.blog.go.tito.io", "canary/c"
	listed := []apiInstanceRecord{{Host: host, Channel: ch}}
	cases := []struct {
		name string
		pre  canaryPreflight
		recs []apiInstanceRecord
		want canaryPlan
	}{
		{"no --region: the list decides", canaryPreflight{}, listed, canaryPlanReady},
		{"no --region, not listed", canaryPreflight{}, nil, canaryPlanCreate},
		{"read found it, list did not, same channel", canaryPreflight{Host: host, Found: true, Rec: apiInstanceRecord{Host: host, Channel: ch}}, nil, canaryPlanReady},
		{"read found it, list did not, other channel", canaryPreflight{Host: host, Found: true, Rec: apiInstanceRecord{Host: host, Channel: "canary/old"}}, nil, canaryPlanRechannel},
		{"read found none", canaryPreflight{Host: host}, listed, canaryPlanCreate},
	}
	for _, c := range cases {
		if got := canaryPlanFor(c.pre, c.recs, host, ch); got != c.want {
			t.Errorf("%s: %v, want %v", c.name, got, c.want)
		}
	}
}

func TestInstanceCreatedLine(t *testing.T) {
	r := apiInstanceRecord{Host: "a.go.tito.io", Channel: "stable", RegionName: "P-uk"}
	if got := instanceCreatedLine(r, ""); got != "a.go.tito.io ("+instanceCreatedSuffix("stable", "P-uk")+")" {
		t.Fatalf("got %q", got)
	}
	env := environmentSuffix("acme")
	if got := instanceCreatedLine(r, env); got != "a.go.tito.io ("+instanceCreatedSuffix("stable", "P-uk")+env+")" {
		t.Fatalf("with environment = %q", got)
	}
	r.RegionName = ""
	if got := instanceCreatedLine(r, env); got != "a.go.tito.io (channel stable"+env+")" {
		t.Fatalf("an old console's record = %q, want today's line byte for byte", got)
	}
}
```

- [ ] **Step 2: Run them to verify they fail**

Run: `go test . -run 'TestCanaryRegionCheck|TestCanaryPlanFor|TestInstanceCreatedLine' -count=1`
Expected: FAIL.

- [ ] **Step 3: Implement the helpers**

Add to `apiInstanceRecord`:

```go
	RegionName string `json:"region_name,omitempty"`
	RegionFrom string `json:"region_from,omitempty"`
```

In `apimode.go`, beside `instanceDelete`:

```go
// instanceGet reads ONE instance record (GET …/instances/{host}). found is
// false only on a 404, the console's answer for a host with no record; any
// other failure is an error, never "absent" (canary --region relies on
// that, nearest-region plan-R3 1).
func (s *apiSession) instanceGet(ctx context.Context, a apiAcct, app, host string) (apiInstanceRecord, bool, error) {
	status, body, err := s.getJSON(ctx, apiPath(a, app, "/instances/"+url.PathEscape(host)))
	if err != nil {
		return apiInstanceRecord{}, false, err
	}
	if status == http.StatusNotFound {
		return apiInstanceRecord{}, false, nil
	}
	if status != http.StatusOK {
		return apiInstanceRecord{}, false, apiErrorFromBody(status, body)
	}
	var out apiInstanceRecord
	if err := json.Unmarshal(body, &out); err != nil {
		return apiInstanceRecord{}, false, fmt.Errorf("instance: unreadable response: %w", err)
	}
	return out, true, nil
}
```

The error text is the record's O20.

In `canary.go`:

```go
// canaryRegionCheck is spec §11's canary --region rule, run before ANY
// write, so the answer never depends on whether the canary exists yet
// (codex R4-2, plan-R2 4, plan-R3 1):
//   - the flag needs a console that can say whether a fleet governs; a
//     failed read, an older console with no app_default, or one that
//     could not read the placement or the stored region (either flag
//     false, R9a), refuses it on every run (C30b),
//     because the create door would silently replace it with the fleet;
//   - a fleet-placed app refuses the flag every time (C30);
//   - the canary's OWN record is then read by host, through get (one
//     GET …/instances/{host}), never inferred from a capped list that
//     skips records it cannot read. A host the console cannot name (no
//     instance_domain) or a read that fails refuses (C30c); a record in
//     another region refuses (C25), named by place.
//
// It returns what it read (codex plan-R5 1): the host, which the caller
// holds the later instancesEnable answer to, and the record and whether it
// exists, which decide create, rechannel or ready (canaryPlanFor).
func canaryRegionCheck(res apiRegionsResult, readErr error, app, name, asked string, get func(host string) (apiInstanceRecord, bool, error)) (canaryPreflight, error) {
	if asked == "" {
		return canaryPreflight{}, nil
	}
	// Both flags: effective is only stated when the stored region was read
	// too (R9a), and the fleet check below reads effective (codex plan-R4 1).
	if readErr != nil || res.AppDefault == nil || !res.AppDefault.PlacementKnown || !res.AppDefault.RegionKnown {
		return canaryPreflight{}, errors.New(canaryFleetUnknown(app))
	}
	if strings.HasPrefix(res.AppDefault.Effective, "fleet/") {
		return canaryPreflight{}, errors.New(canaryRegionOnFleet(app, strings.TrimPrefix(res.AppDefault.Effective, "fleet/")))
	}
	if res.AppDefault.InstanceDomain == "" {
		return canaryPreflight{}, errors.New(canaryRegionUnconfirmed(name))
	}
	host := canaryHost(name, app, res.AppDefault.InstanceDomain)
	rec, found, err := get(host)
	if err != nil {
		return canaryPreflight{}, errors.New(canaryRegionUnconfirmed(name))
	}
	if found {
		have := rec.Region
		if have == "" {
			have = res.Default
		}
		if have != asked {
			return canaryPreflight{}, errors.New(canaryRegionMismatch(host, res.nameOf(have)))
		}
	}
	return canaryPreflight{Host: host, Rec: rec, Found: found}, nil
}

// canaryPreflight is what canaryRegionCheck read. Host is "" when --region
// was not given; then nothing was read.
type canaryPreflight struct {
	Host  string
	Rec   apiInstanceRecord
	Found bool
}

// canaryPlanFor decides create, rechannel or ready. With --region the
// direct read decides (codex plan-R5 1): the list is capped at 500 and
// skips records it cannot read, so it can miss a canary that exists, and
// creating it again would fail. Without --region the list decides, as
// before.
func canaryPlanFor(pre canaryPreflight, recs []apiInstanceRecord, host, channel string) canaryPlan {
	if pre.Host == "" {
		return canaryRecordPlan(recs, host, channel)
	}
	if !pre.Found {
		return canaryPlanCreate
	}
	return canaryRecordPlan([]apiInstanceRecord{pre.Rec}, host, channel)
}
```

In `instances_cli.go`:

```go
// instanceCreatedLine is the text after "created instance" or "canary
// host": today's "host (channel X<env>)" byte for byte from an older
// console, C14 when the console names the place.
func instanceCreatedLine(rec apiInstanceRecord, envSuffix string) string {
	if rec.RegionName == "" {
		return fmt.Sprintf("%s (channel %s%s)", rec.Host, rec.Channel, envSuffix)
	}
	return fmt.Sprintf("%s (%s%s)", rec.Host, instanceCreatedSuffix(rec.Channel, rec.RegionName), envSuffix)
}
```

- [ ] **Step 4: Wire the flags, the check and the output**

In `runCanaryDeploy`'s flags:

```go
	regionFlag := fs.String("region", "", flagRegionCanary)
```

After `ctx := context.Background()` and `channel := canaryChannel(canary)`
(`canary.go:375-377`), and **before** the `instancesEnable` call:

```go
	// --region is checked with reads only, before the first write (the
	// instancesEnable PUT below): nothing changes when it is refused.
	var pre canaryPreflight
	if asked := strings.TrimSpace(*regionFlag); asked != "" {
		res, rerr := sess.appRegions(ctx, acct, app2)
		get := func(h string) (apiInstanceRecord, bool, error) { return sess.instanceGet(ctx, acct, app2, h) }
		var err error
		if pre, err = canaryRegionCheck(res, rerr, app2, canary, asked, get); err != nil {
			log.Fatalf("canary: %v", err)
		}
	}
```

`ctx` is declared at `canary.go:375`, so the block must come after it (codex
plan-R2 5).

Then the one plan decision, used by the create/rechannel/ready switch and by
the `--environment` bind. Replace `switch canaryRecordPlan(recs, host, channel) {`
(`canary.go:405`) with:

```go
	// One decision, used twice below: with --region it comes from the
	// direct read of the canary's own record (codex plan-R5 1).
	plan := canaryPlanFor(pre, recs, host, channel)
	switch plan {
```

and, in the `--environment` block, replace
`canaryRecordPlan(recs, host, channel) != canaryPlanCreate` with
`plan != canaryPlanCreate`.

Right after `host := canaryHost(canary, app2, m.Domain)`:

```go
	if pre.Host != "" && host != pre.Host {
		// The PUT named a different domain from the one the check read
		// under: the canary checked is not the one about to be written.
		log.Fatalf("canary: %s", canaryRegionUnconfirmed(canary))
	}
```

The comment above the PUT ("the ONLY source of the domain suffix") stays
true for every WRITE: the record is still created under the PUT's domain,
and the check's host is held to it.

In the create branch, pass the flag:
`sess.instanceCreate(ctx, acct, app2, host, strings.TrimSpace(*regionFlag), channel, recKind, env, nil, nil, nil)`.
Print with:

```go
		fmt.Fprintf(stdout, "%s %s\n", pal.ok("canary host"), instanceCreatedLine(rec, environmentSuffix(rec.Environment)))
```

Add the record's U3 text to `canaryUsage`'s first line. In
`runInstancesCreate`, set the flag help to `flagRegionInstancesCreate`, and
the output to:

```go
	fmt.Fprintf(stdout, "%s %s\n", colorFor(stdout).ok("created instance"),
		instanceCreatedLine(rec, environmentSuffix(rec.Environment)))
```

- [ ] **Step 5: Write the command-level tests (codex plan-R1 3, 12; plan-R2 3, 4; plan-R3 1, 6)**

```go
// canary_region_e2e_test.go
package main

import (
	"context"
	"errors"
	"fmt"
	"os"
	"strings"
	"testing"
	"time"

	"github.com/carlosframework/platform/internal/console"
	"github.com/carlosframework/platform/internal/provision"
	"github.com/carlosframework/platform/internal/release"
)

// Two identical runs per case: the same refusal both times, and no write,
// no new record, no manifest and no upload, before or after.
func TestCanaryRegionRefusesBeforeAnyWrite(t *testing.T) {
	const existing = "probe.titogo.bab.oncarlos.test"
	cases := []struct {
		name, mode, region string
		fleet, canary      bool
		ahead              int  // records whose hosts sort before the canary's
		unreadable         bool // the canary's record cannot be read
		want               string
	}{
		{"fleet-placed app", "", "uk", true, false, 0, false, canaryRegionOnFleet("titogo", "acme")},
		{"older console", "legacy", "uk", true, false, 0, false, canaryFleetUnknown("titogo")},
		{"placement unknown", "placement-unknown", "uk", false, false, 0, false, canaryFleetUnknown("titogo")},
		{"existing canary elsewhere", "", "ie", false, true, 0, false, canaryRegionMismatch(existing, "uk")},
		// codex plan-R3 1: past the list's 500-record cap, and unreadable
		// (a list skips it), the canary's own record still decides.
		{"existing canary past the list cap", "", "ie", false, true, 520, false, canaryRegionMismatch(existing, "uk")},
		{"existing canary unreadable", "", "ie", false, true, 0, true, canaryRegionUnconfirmed("probe")},
		// codex plan-R4 1: a fleet-placed app whose stored region the console
		// could not read; R9a leaves effective empty, and the flag is refused.
		{"fleet, region unknown", "region-unknown", "uk", true, false, 0, false, canaryFleetUnknown("titogo")},
	}
	for _, c := range cases {
		t.Run(c.name, func(t *testing.T) {
			e := regionE2EConsole(t, "")
			ctx := context.Background()
			if c.fleet {
				if err := e.cst.SetAppPlacement(console.OpsAccountID, "titogo", "fleet/acme"); err != nil {
					t.Fatal(err)
				}
			}
			for i := 0; i < c.ahead; i++ {
				h := fmt.Sprintf("a%03d.titogo.bab.oncarlos.test", i)
				rec := provision.Record{Schema: 1, Host: h, App: "titogo", Account: release.OpsAccount,
					Channel: "stable", CreatedAt: time.Now().UTC()}
				if err := provision.CreateRecord(ctx, e.store, rec); err != nil {
					t.Fatal(err)
				}
			}
			if c.canary {
				rec := provision.Record{Schema: 1, Host: existing, App: "titogo", Account: release.OpsAccount,
					Region: "uk", Channel: "canary/probe", CreatedAt: time.Now().UTC()}
				if err := provision.CreateRecord(ctx, e.store, rec); err != nil {
					t.Fatal(err)
				}
			}
			if c.unreadable {
				e.failGet.Store(provision.RecordKey(release.OpsAccount, "titogo", existing))
			}
			e.mode.Store(c.mode)
			for run := 0; run < 2; run++ {
				out, err := e.run(t, "canary", "deploy", "--app", "titogo", "--name", "probe", "--region", c.region,
					"--console", e.url, "--version", "abc1234", "--timeout", "1s", artifact(t))
				if err == nil || !strings.Contains(out, c.want) {
					t.Fatalf("run %d: err=%v out=%s, want %q", run, err, out, c.want)
				}
			}
			recs, _, err := provision.ListRecords(ctx, e.store, release.OpsAccount, "titogo", 1000)
			if err != nil {
				t.Fatal(err)
			}
			wantRecs := c.ahead
			if c.canary {
				wantRecs++
			}
			if len(recs) != wantRecs || e.writes.Load() != 0 || e.releases(t) != 0 {
				t.Fatalf("records %d (want %d), writes %d, releases %d; want nothing written", len(recs), wantRecs, e.writes.Load(), e.releases(t))
			}
			if c.canary {
				rec, err := provision.GetRecord(ctx, e.store, release.OpsAccount, "titogo", existing)
				if err != nil || rec.Region != "uk" {
					t.Fatalf("the existing canary = %+v %v, want it still in uk", rec, err)
				}
			}
			if _, err := provision.GetManifest(ctx, e.store, release.OpsAccount, "titogo"); !errors.Is(err, os.ErrNotExist) {
				t.Fatalf("an instances manifest was written: %v", err)
			}
		})
	}
}

// codex plan-R5 1: a same-region canary past the list's 500-record cap is
// the record the command reuses, found by the direct read, never re-created.
// On its own channel it is left as it is; on another it is rechannelled.
// Either way there is no create request and the run goes on to ship.
func TestCanaryRegionReusesACanaryPastTheListCap(t *testing.T) {
	const existing = "probe.titogo.bab.oncarlos.test"
	for _, c := range []struct{ name, channel, env string }{
		{"same channel", "canary/probe", ""},
		{"different channel", "canary/old", ""},
		// The environment bind follows the same decision: an existing
		// record gets its own bind write, never a create carrying it.
		{"same channel, with --environment", "canary/probe", "beta"},
	} {
		t.Run(c.name, func(t *testing.T) {
			e := regionE2EConsole(t, "")
			ctx := context.Background()
			for i := 0; i < 520; i++ {
				rec := provision.Record{Schema: 1, Host: fmt.Sprintf("a%03d.titogo.bab.oncarlos.test", i), App: "titogo",
					Account: release.OpsAccount, Channel: "stable", CreatedAt: time.Now().UTC()}
				if err := provision.CreateRecord(ctx, e.store, rec); err != nil {
					t.Fatal(err)
				}
			}
			rec := provision.Record{Schema: 1, Host: existing, App: "titogo", Account: release.OpsAccount,
				Region: "uk", Channel: c.channel, CreatedAt: time.Now().UTC()}
			if err := provision.CreateRecord(ctx, e.store, rec); err != nil {
				t.Fatal(err)
			}
			args := []string{"canary", "deploy", "--app", "titogo", "--name", "probe", "--region", "uk",
				"--console", e.url, "--version", "abc1234", "--timeout", "1s"}
			if c.env != "" {
				args = append(args, "--environment", c.env)
			}
			out, _ := e.run(t, append(args, artifact(t))...)
			if n := e.creates.Load(); n != 0 {
				t.Fatalf("%d create request(s) for a canary that exists:\n%s", n, out)
			}
			if e.releases(t) == 0 {
				t.Fatalf("the run never shipped:\n%s", out)
			}
			got, err := provision.GetRecord(ctx, e.store, release.OpsAccount, "titogo", existing)
			if err != nil || got.Region != "uk" || got.Channel != "canary/probe" || got.Environment != c.env {
				t.Fatalf("the canary = %+v %v, want it in uk on canary/probe, environment %q", got, err, c.env)
			}
			if recs, _, err := provision.ListRecords(ctx, e.store, release.OpsAccount, "titogo", 1000); err != nil || len(recs) != 521 {
				t.Fatalf("records = %d %v, want the 521 there were", len(recs), err)
			}
		})
	}
}

// codex plan-R3 6: a successful replica create prints C14 with its place,
// not an older console's "(channel X)". This console has no secrets key,
// so no box is eligible and the place falls back to its code.
func TestInstancesCreateReplicaPrintsItsPlace(t *testing.T) {
	e := regionE2EConsole(t, "")
	ctx := context.Background()
	m := provision.Manifest{Schema: 1, App: "titogo", Domain: "bab.oncarlos.test", Channel: "beta"}
	if err := provision.PutManifest(ctx, e.store, release.OpsAccount, "titogo", m); err != nil {
		t.Fatal(err)
	}
	box := `{"v":1,"box":"uk-box","eip":"1.2.3.4","region":"eu-west-2","placement":"uk","hibernation_configured":true,"hosts":{}}`
	if _, err := e.store.Put(ctx, "control/edges/uk-box.json", []byte(box), ""); err != nil {
		t.Fatal(err)
	}
	const host = "r.bab.oncarlos.test"
	out, err := e.run(t, "instances", "create", "--app", "titogo", "--host", host, "--region", "uk", "--role", "replica", "--console", e.url)
	if err != nil {
		t.Fatalf("create: %v\n%s", err, out)
	}
	if want := "created instance " + host + " (" + instanceCreatedSuffix("beta", "uk") + ")"; !strings.Contains(out, want) {
		t.Fatalf("stdout = %q, want %q", out, want)
	}
}
```

For the existing-canary cases, C25 names the region `uk` by its code: the
test console has no box publishing `uk`, so `/regions` gives it no place
name, and `nameOf` falls back to the code. The 520 records ahead of the
canary sort before `probe.` and fill the list's 500-record page. The
unreadable case fails only the store GET of the canary's record key, so the
listing still names it and `ListRecords` skips it.

`TestCanaryRegionReusesACanaryPastTheListCap` is codex plan-R5 1: a `uk`
canary behind 520 records, run with `--region uk`. On its own channel it is
left alone, on another it is rechannelled, and with `--environment` it gets
its bind write. Each case sends no create request (the harness counts
`POST …/instances`), keeps 521 records and goes on to ship.

`TestInstancesCreateReplicaPrintsItsPlace` is codex plan-R3 6's CLI half:
a successful replica create prints C14 (`channel beta, uk`), where an older
console's response gives today's `(channel beta)`. The harness console has
no secrets key, so no box is eligible and the place falls back to its code.

- [ ] **Step 6: Run the root tests**

Run: `go test . -count=1 -timeout 30m`
Expected: PASS, including the byte-for-byte output assertions in
`canary_test.go` and `instances_environment_test.go`.

Then the ordering check: move the `--region` block from step 4 to after the
`instancesEnable` call, and run
`go test . -run TestCanaryRegionRefusesBeforeAnyWrite -count=1`. It must
FAIL on writes. Move it back, and re-run: PASS.

Then the reuse checks (codex plan-R5 1). Replace `plan := canaryPlanFor(…)`
with `plan := canaryRecordPlan(recs, host, channel)` (rev 5's wiring): all
three `ReusesACanaryPastTheListCap` cases must FAIL with "1 create
request(s)". Restore. Then make only the `--environment` condition read the
list again: the `with --environment` case must FAIL. Restore, and re-run:
PASS.

Then the authoritative-read check (codex plan-R3 1): replace the
`canaryRegionCheck` call with a scan of `instanceList`'s records for the
canary's host (rev 3's design). The `past the list cap` and `unreadable`
cases must FAIL: the command passes the check and the PUT is sent. Restore,
and re-run: PASS.

- [ ] **Step 7: Cross-compile, then commit**

```bash
fail=0
for t in darwin/arm64 darwin/amd64 linux/arm64 windows/amd64 windows/arm64; do
  CGO_ENABLED=0 GOOS=${t%/*} GOARCH=${t#*/} go build ./... || { echo "FAILED $t"; fail=1; }
done
[ "$fail" = 0 ] || exit 1  # stops this block before any commit below it
git add canary.go canary_region_test.go canary_region_e2e_test.go instances_cli.go apimode.go
git commit -m "cli: canary --region checks with reads before any write; refuses identically on fleets and older consoles (nearest-region §11)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```

---


### Task 23: The region service-gate script, proved against a real backhaul

**Files:**
- Create: `scripts/region-service-gate.sh`
- Test:
  - `region_service_gate_test.go` (root package `main`, which runs from the
    repo root);
  - `internal/edge/servicegate_test.go`, which drives the script through
    the edge's real backhaul.

**Interfaces:**
- Consumes: the record's G1 to G6. In `internal/edge`'s tests: `hopEdge`,
  `fakeHomeEdge`, `homedOn`, `refreshHomeMap` and `meshKeyKey`
  (`homemap_test.go:42-760`).
- Produces: `scripts/region-service-gate.sh <probe-host> <expected-version> [<edge-eip> ...]`.
  It exits 0 only if every request returns 2xx with
  `X-Carlos-Version == <expected-version>`, and every explicit-edge request
  connected to the EIP it named.

- [ ] **Step 1: Write the failing shell-level test**

```go
// region_service_gate_test.go
package main

import (
	"net"
	"net/http"
	"net/http/httptest"
	"os/exec"
	"strings"
	"testing"
)

// spec §17 step 6 (codex R4-3, plan-R1 4): the gate fails on a 404 and on a
// version mismatch, and a proxy in the environment cannot route around the
// edge it names.
func TestRegionServiceGate(t *testing.T) {
	const version = "abc1234"
	ok := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("X-Carlos-Version", version)
	}))
	defer ok.Close()
	unknown := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		http.NotFound(w, r)
	}))
	defer unknown.Close()
	hostPort := func(s *httptest.Server) (string, string) {
		h, p, _ := net.SplitHostPort(s.Listener.Addr().String())
		return h, p
	}
	run := func(srv *httptest.Server, want string, extraEnv ...string) (string, error) {
		h, p := hostPort(srv)
		cmd := exec.Command("./scripts/region-service-gate.sh", "probe.example", want, h)
		cmd.Env = append(cmd.Environ(), append([]string{"GATE_PORT=" + p, "GATE_INSECURE=1", "GATE_SKIP_PUBLIC=1"}, extraEnv...)...)
		out, err := cmd.CombinedOutput()
		return string(out), err
	}
	if out, err := run(ok, version); err != nil {
		t.Fatalf("a serving edge failed the gate: %v %s", err, out)
	}
	if out, err := run(unknown, version); err == nil || !strings.Contains(out, "FAIL") {
		t.Fatalf("an unknown-host 404 passed: %s", out)
	}
	if out, err := run(ok, "other"); err == nil || !strings.Contains(out, "FAIL") {
		t.Fatalf("a version mismatch passed: %s", out)
	}
	// A proxy that refuses everything: honoured, it would fail every check.
	dead := "http://127.0.0.1:9"
	if out, err := run(ok, version, "HTTPS_PROXY="+dead, "https_proxy="+dead, "ALL_PROXY="+dead, "all_proxy="+dead); err != nil {
		t.Fatalf("the gate went through a proxy: %v %s", err, out)
	}
}
```

- [ ] **Step 2: Run it to verify it fails**

Run: `go test . -run TestRegionServiceGate -count=1`
Expected: FAIL, because the script does not exist.

- [ ] **Step 3: Write the script**

```bash
#!/usr/bin/env bash
# scripts/region-service-gate.sh <probe-host> <expected-version> [<edge-eip> ...]
#
# Nearest-region spec §17 step 6: before CONSOLE_APP_REGIONS goes on, every
# eligible region must serve a fresh probe instance through the REAL public
# path — from outside through public DNS (skip with GATE_SKIP_PUBLIC=1), and
# through every armed edge that is NOT the instance's box. Passes only on
# 2xx with X-Carlos-Version equal to the deployed build. Proxies are
# disabled and ~/.curlrc ignored, and each explicit-edge check verifies it
# connected to the EIP it named, so no intermediary can stand in for an
# edge. GATE_PORT and GATE_INSECURE exist for tests only.
set -uo pipefail
host="${1:?usage: region-service-gate.sh <probe-host> <expected-version> [<edge-eip> ...]}"
want="${2:?usage: region-service-gate.sh <probe-host> <expected-version> [<edge-eip> ...]}"
shift 2
port="${GATE_PORT:-443}"
insecure=""
[ "${GATE_INSECURE:-}" = 1 ] && insecure="-k"
unset http_proxy https_proxy HTTP_PROXY HTTPS_PROXY ALL_PROXY all_proxy
hdr="$(mktemp "${TMPDIR:-/tmp}/region-gate.XXXXXX")"
trap 'rm -f "$hdr"' EXIT
fail=0
check() { # <label> <expected-ip or ""> [curl args...]
  local label="$1" eip="$2"; shift 2
  local ip status version
  # -q must be first: it stops curl reading ~/.curlrc. $insecure is
  # unquoted on purpose so that empty expands to nothing (bash 3.2 safe).
  ip="$(curl -q -s $insecure --noproxy '*' -D "$hdr" -o /dev/null -w '%{remote_ip}' --max-time 20 "$@" "https://$host:$port/")" || {
    echo "FAIL $label: request failed"; fail=1; return; }
  if [ -n "$eip" ] && [ "$ip" != "$eip" ]; then
    echo "FAIL $label: connected to $ip, not $eip"; fail=1; return
  fi
  status="$(awk 'toupper($1) ~ /^HTTP/ {s=$2} END {print s}' "$hdr")"
  version="$(awk -F': *' 'tolower($1)=="x-carlos-version" {gsub(/\r/,"",$2); print $2}' "$hdr")"
  if [[ "$status" != 2* ]]; then echo "FAIL $label: status $status"; fail=1; return; fi
  if [ "$version" != "$want" ]; then echo "FAIL $label: X-Carlos-Version '$version', want '$want'"; fail=1; return; fi
  echo "ok   $label: $status $version"
}
[ "${GATE_SKIP_PUBLIC:-}" = 1 ] || check "public DNS" ""
for eip in "$@"; do
  check "via edge $eip" "$eip" --resolve "$host:$port:$eip"
done
exit "$fail"
```

`chmod +x scripts/region-service-gate.sh`. The output lines are G1 to G6 from
the record.

- [ ] **Step 4: Run it to verify it passes**

Run: `go test . -run TestRegionServiceGate -count=1`
Expected: PASS.

- [ ] **Step 5: Prove the gate against the edge's real backhaul (codex plan-R1 12)**

```go
// internal/edge/servicegate_test.go
package edge

import (
	"context"
	"fmt"
	"net"
	"net/http"
	"net/http/httptest"
	"os"
	"os/exec"
	"strings"
	"testing"
)

// The gate script, run against this package's own backhaul path: a far
// edge serving a remote-homed host passes; the same edge with no mesh key
// mirrored answers unknown-host (edge.go:2699, :1428) and the gate FAILS.
func TestServiceGateAgainstARealBackhaul(t *testing.T) {
	if _, err := exec.LookPath("curl"); err != nil {
		t.Skip("curl not installed")
	}
	const version = "abc1234"
	e, m, _ := hopEdge(t, "annie", "local.test")
	_, eip := fakeHomeEdge(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("X-Carlos-Version", version)
		fmt.Fprint(w, "ok")
	}))
	homedOn(t, m, "betty", eip, "example.com")
	gate := func() (string, error) {
		ts := httptest.NewTLSServer(e)
		defer ts.Close()
		h, p, _ := net.SplitHostPort(ts.Listener.Addr().String())
		cmd := exec.Command("../../scripts/region-service-gate.sh", "example.com", version, h)
		cmd.Env = append(os.Environ(), "GATE_PORT="+p, "GATE_INSECURE=1", "GATE_SKIP_PUBLIC=1")
		out, err := cmd.CombinedOutput()
		return string(out), err
	}
	if out, err := gate(); err != nil {
		t.Fatalf("a working backhaul failed the gate: %v %s", err, out)
	}
	if err := m.s.Delete(context.Background(), meshKeyKey); err != nil {
		t.Fatal(err)
	}
	refreshHomeMap(t, m)
	if out, err := gate(); err == nil || !strings.Contains(out, "FAIL") {
		t.Fatalf("a backhaul with no mesh key passed the gate: %s", out)
	}
}
```

Run: `socktest ./internal/edge -run TestServiceGateAgainstARealBackhaul -count=1`
Expected: PASS. If the first gate fails because the backhaul does not carry
`X-Carlos-Version` through, the gate is right and the probe instance must set
that header. Read `internal/edge`'s response copy path before changing
anything, because the production edge stamps the header from the route row.

- [ ] **Step 6: Commit**

```bash
git add scripts/region-service-gate.sh region_service_gate_test.go internal/edge/servicegate_test.go
git commit -m "scripts: region service gate, proxy-proof, proved against a real backhaul (nearest-region §17 step 6)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```

---

### Task 24: Docs and the release checklist (D1 to D10, from the record)

**Files:**
- Modify: `docs/site/cli.md`
  - `### carlos apps`, at `:112`;
  - `### carlos deploy`, at `:211`;
  - `### carlos canary`, at `:282`;
  - `### carlos instances`, at `:540`.
- Modify: `README.md`
  - the console configuration list;
  - `## Releasing the platform`, at `:227`;
  - the `STATUS_BOX_REGION` entry, at `:578`;
  - the instances manifest `regions` documentation.
- Test: the existing `internal/docsite` gate

**Interfaces:**
- Consumes:
  - the flags from Tasks 20 to 22;
  - the env from Task 9;
  - the script from Task 23;
  - the record's D1 to D10.

The passages below are the drafts Task 0 submitted. Write each from its
record entry verbatim.

- [ ] **Step 1: Write the `cli.md` passages (D1 to D4)**

D1, under `### carlos apps`, after the `carlos apps create` example:

````markdown
`--region <code>` sets the region the app's instances start in. Without it,
the console picks the region nearest you from your timezone, and the command
prints one line saying which and why:

```
carlos apps create --app hello --region uk
```
````

D2, under `### carlos deploy`:

```markdown
When `carlos deploy` creates your first app, it sends your timezone so the
console can pick the region nearest you, and prints the choice. `--region
<code>` chooses instead. On an app that already exists, `--region` must
match the app's region, or deploy stops before shipping anything.
```

D3, under `### carlos canary`:

```markdown
`--region <code>` places a new canary in that region (default: the app's
region). It is refused for an existing canary in another region, and for an
app that runs on one of your fleets.
```

D4, under `### carlos instances`, in the `create` paragraph:

```markdown
Without `--region`, a new instance starts in the app's region.
```

- [ ] **Step 2: Write the README passages (D5 to D10)**

D5, in the console configuration list:

```markdown
- `CONSOLE_APP_REGIONS`: `on` lets new apps be given a region other than the
  home. It needs `CONSOLE_HOME_REGION` set too. With the home unnamed, the
  console logs once and leaves app regions off. Default off. Automatic
  placement outside the home also needs `CARLOS_SECRETS_PUBKEY`, so the
  console can check that a box holds the deployment's secrets key.
- `CONSOLE_GEOIP_DB`: the path to an operator-supplied MaxMind-format `.mmdb`.
  DB-IP Lite is recommended, and it requires the attribution the new-app page
  shows. Unset or unusable turns the IP step of the region guess off, and the
  console starts either way. Replace the file by atomic rename, then restart.
```

D6, in `## Releasing the platform`:

```markdown
**App regions rollback floor.** Once any app holds a region other than the
home, do not roll the console binary below the first release that
understands app regions: an older console places a no-region create at the
home. That release's version is named here, in its release notes, and in the
console roll recipe in carloku-infrastructure; check all three name the same
version before turning `CONSOLE_APP_REGIONS` on. If a rollback below the
floor happens anyway, write down when it started and ended: on its next
start the console logs "possible placement discrepancy" lines, and the
interval tells you which to review. Turning the switch off is the feature's
own rollback: it stops new non-home regions and keeps existing ones in force.
```

D7, in `## Releasing the platform`:

```markdown
**Turning app regions on.** In order:

1. Stage the deployment's `CARLOS_SECRETS_KEY` on every box in every region
   that should take placed instances, and set `CARLOS_SECRETS_PUBKEY` on the
   console. A box without the key runs placed instances with no config, no
   secrets and no store.
2. Roll the box release that publishes `serves_shared_pool`,
   `accepts_inbound` and `secrets_key_fp`. Check every region's
   `control/edges/*.json` carries them, and that each `secrets_key_fp` equals
   the console's.
3. Roll the console with the switch off.
4. On dev, turn the switch on. Create an app and an instance, read their
   region, and run the service gate below for every region.
5. On production, with the switch still off, create one probe instance per
   eligible region with an explicit `--region`, deploy a known build, and run
   `scripts/region-service-gate.sh <probe-host> <build> <every other edge EIP>`
   for each. Every region must pass. Delete the probes.
6. Turn the switch on. Re-run step 5 after any period with the switch off,
   and whenever a new region becomes eligible.
```

D8, at the `STATUS_BOX_REGION` entry:

```markdown
  Keep it geographically consistent with the region's place name in the
  console's region picker (for example "EU · Ireland" beside
  "Ireland · Dublin"); it may stay coarser.
```

D9, beside the instances manifest `regions` documentation:

```markdown
A manifest's `regions` allow-list compares the home as `""`, so listing the
home's code does not admit the home.
```

D10, in `## Releasing the platform`'s checklist:

```markdown
- Refresh the embedded timezone tables when a new IANA tzdata release is out:
  `scripts/update-tzgeo.sh <version>`, then `go test ./internal/geo`.
```

- [ ] **Step 3: Run the docsite gate**

Run: `go test ./internal/docsite -count=1`
Expected: PASS. There are no new command headings, and the fenced examples use
existing verbs.

- [ ] **Step 4: Commit**

```bash
git add docs/site/cli.md README.md
git commit -m "docs: --region flags, CONSOLE_APP_REGIONS, CONSOLE_GEOIP_DB, rollback floor, switch-on runbook, tz refresh (as approved)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```

---

### Task 25: Whole-branch gate

**Files:**
- Create: `approved_copy_test.go` (root)

**Interfaces:**
- Consumes: the copy record (Task 0) and every file it names.

- [ ] **Step 1: Write the approved-copy test**

```go
// approved_copy_test.go
package main

import (
	"bytes"
	"encoding/json"
	"os"
	"testing"
)

// Every string Paul approved in the nearest-region copy review is present,
// byte for byte, in the file the record names (codex plan-R1 13). A string
// edited after review, or never applied, fails here.
func TestApprovedCopyIsApplied(t *testing.T) {
	raw, err := os.ReadFile("docs/superpowers/plans/2026-10-02-nearest-region-copy.json")
	if err != nil {
		t.Fatal(err)
	}
	var rec struct {
		ApprovedBy string `json:"approved_by"`
		ApprovedOn string `json:"approved_on"`
		Entries    []struct{ ID, File, Text string }
	}
	if err := json.Unmarshal(raw, &rec); err != nil {
		t.Fatal(err)
	}
	if rec.ApprovedBy == "" || rec.ApprovedOn == "" || len(rec.Entries) == 0 {
		t.Fatalf("the record has no approval: %+v", rec)
	}
	for _, e := range rec.Entries {
		body, err := os.ReadFile(e.File)
		if err != nil {
			t.Errorf("%s: %v", e.ID, err)
			continue
		}
		if !bytes.Contains(body, []byte(e.Text)) {
			t.Errorf("%s: %s does not contain the approved text %q", e.ID, e.File, e.Text)
		}
	}
}
```

- [ ] **Step 2: Run the full gate**

Run, from the worktree root, as one script:

```bash
set -e
go build ./...
go vet ./...
test -z "$(gofmt -l .)"
fail=0
for t in darwin/arm64 darwin/amd64 linux/arm64 windows/amd64 windows/arm64; do
  CGO_ENABLED=0 GOOS=${t%/*} GOARCH=${t#*/} go build ./... || { echo "FAILED $t"; fail=1; }
done
[ "$fail" = 0 ] || exit 1
(cd launchctx && go vet ./... && go test ./... -count=1)
# The operator rule (Global Constraints): the Unix-socket test packages run
# through socktest, in a fresh short directory under /tmp/g that is deleted
# after the run; everything else uses the on-disk TMPDIR, which the console
# tests need. socktest is the function defined there.
sock='/internal/(host|activator|edge|agentsock|instancectl|instanceexec|unixdial|vet)(/|$)'
socktest $(go list ./... | grep -E "$sock") -count=1
go test $(go list ./... | grep -vE "$sock") -count=1 -timeout 30m
```

Expected: the script exits 0, with no `FAILED` line, and
`TestApprovedCopyIsApplied` among the passes. This is `make test`'s content,
split so each package gets the `TMPDIR` it needs.

- [ ] **Step 2b: Check for strings nobody reviewed (codex plan-R1 13, plan-R2)**

Presence of the approved strings does not prove the absence of others. List
every prose literal this branch adds to non-test Go code:

```bash
BASE=e8406ce6  # the commit this branch was cut from
git diff -U0 "$BASE" -- '*.go' ':!*_test.go' ':!internal/geo/awsregions.go' \
  | grep '^+' | grep -v '^+++' \
  | grep -oE '"([^"\\]|\\.)*[a-z][^"]* [^"]*"' | sort -u
```

Every line printed must be one of three things:

- a record entry's text;
- a message that already existed before this branch and was only moved or
  reused, such as `canary: list instances for %s: %v`;
- code rather than prose, such as a fragment of a Go expression the pattern
  happened to catch.

Anything else is unreviewed copy. Take it to a copy review and add it to the
record before landing.

- [ ] **Step 3: Confirm nothing touches DNS steering (R8)**

Run: `git diff main --stat -- internal/steering internal/host/steering_loop.go infra/ && git diff main -- internal/console/apiinstances.go | grep -n '^+.*Steering'`
Expected: no files under steering or infra, and no added `Steering` lines.

- [ ] **Step 4: Describe the branch**

Write the amadan branch description (`amadan branch describe`). It covers:

- what changed;
- the copy record and its approval date;
- that rollout follows README "Turning app regions on";
- that boxes must hold `CARLOS_SECRETS_KEY` before they are eligible (R9).

Do not push without the person directing this work.

---

## Rollout (operational, after the branch lands; spec §17)

These are not code tasks. They are listed so whoever lands the branch runs
them in order. README "Turning app regions on" (D7) carries the same list for
operators.

1. **Before any roll.**
   - Audit production manifests for `regions`.
   - Confirm that every box publishes its AWS region. On the flagship, the
     Ireland box should publish `region: eu-west-1` and no `placement`. Use
     `CARLOS_AWS_REGION` where needed.
   - Set `CONSOLE_HOME_REGION` and `CARLOS_SECRETS_PUBKEY` on every console
     that will turn the switch on.
   - Confirm that the floor version is named in the README, the release
     notes and the carloku-infrastructure roll recipe.
2. **Stage the secrets key (R9).** Put the deployment's `CARLOS_SECRETS_KEY`
   in `/etc/carlos/host.env` on every box in every region that should take
   placed instances. This is infrastructure work in carloku-infrastructure,
   under AGENTS.md's scoped-approval rule.
   - **Flagship: done (2026-10-03).** `CARLOS_SECRETS_KEY` is on all four
     flagship boxes (Ireland, UK, US, AU), from SSM `/carlos/secrets-key` in
     every region, and config sync and generation are armed everywhere.
     Step 3's check (every box's `secrets_key_fp` equals the console's) is
     still to run once the box release publishes it.
   - Any other deployment still needs this step.
3. **Box release** (Tasks 1 and 1b) to every box. Confirm each region's
   `control/edges/*.json` carries `serves_shared_pool`, `accepts_inbound` and
   `secrets_key_fp`, and that every `secrets_key_fp` equals the console's.
4. **Console release with `CONSOLE_APP_REGIONS` unset.** Behaviour is
   unchanged, and it can be rolled back freely.
5. **Dev.**
   - Turn the switch on with two eligible regions.
   - Create an app in the browser and read its row. Create an instance with
     no region and read its record.
   - Diff `/regions` for an old app against the pre-roll answer.
   - Run the service gate (Task 23) for every eligible region.
6. **Production service gate, switch still off.** For each eligible region:
   - create a probe instance with an explicit `--region`;
   - deploy a known build;
   - run the gate from outside and through every other edge.

   Every region must pass. Then delete the probes.
7. **Flagship: turn the switch on.** From here the R7 floor applies. If a
   rollback below it ever happens, record its start and end, for the
   discrepancy review.
8. **CLI release.**
9. **titogo**, in its own repo, adopts `POST …/regions/guess` and the place
   names.

**Do not arm `CARLOS_STEERING_ZONE_ID` anywhere** (spec §13.3). That belongs
to the follow-on spec.

---

## Self-review

1. **Spec coverage (rev 6.2).**

   | Spec | Covered in |
   | --- | --- |
   | §5.1 to §5.4 | Task 8 |
   | §5.5 | Tasks 12, 13 |
   | §5.6 | Tasks 9, 12 |
   | §5.7 | Tasks 17, 24 (D6) |
   | §6.1, including R9 | Tasks 1, 1b, 10 |
   | §6.2 | Tasks 3, 4, 5 |
   | §6.3, §6.4 | Tasks 2, 11 |
   | §6.5 | Task 5's log test, Task 12's log tests, Task 15's redirect test |
   | §7, including the R9 explicit refusal | Task 14 |
   | §8 | Tasks 12, 13, 14, 20, 22 |
   | §9, including R9a's knowledge flags and rev 6.2's `instance_domain` | Tasks 12, 14, 15, 13, 22 |
   | §9.6 | Tasks 21, 22, with the older-console e2e cases |
   | §10 | Tasks 13, 16 |
   | §11 | Tasks 19 to 22 |
   | §12 | Tasks 14, 15 |
   | §13 (no stamp) | Task 25 step 3 |
   | §14 | Tasks 4, 24 |
   | §15, §16 | the named tests in each task; the persisted-record goldens and the reconcile pairing in Tasks 14 and 1 |
   | §17 | Task 23, Task 24 (D6, D7), Rollout |
   | §19 | Task 0, then Tasks 4, 6, 7, 18 and 24, verified by Task 25 |

2. **Placeholder scan.** Each step carries its code. The facts confirmed in
   the codebase are cited inline:
   - `postJSON` returns `map[string]any`;
   - `putEdges` is create-only, and `setEdges` replaces it;
   - `ListRecords(…, 0)` lists nothing;
   - `MoveApp` and `RenameAppIn` signatures;
   - `fx.srv.redirects`;
   - `Event.Detail` and `Event.Message`;
   - the backhaul test helpers.
3. **Type consistency.**
   - `regionCandidates(snap, homeName, expectedFP)` is defined in Task 10,
     and every console call site uses `s.regionCands(snap)`.
   - These are defined once and used consistently: `regionCandidate`
     (`KeyOK`, `NoKeyProven`), `regionReq`, `appDefaultView`/`apiAppDefault`,
     `apiRegionsResult.nameOf`, `canaryRegionCheck(res, readErr, app, name, asked, get)`
     returning a `canaryPreflight`, `canaryPlanFor`,
     `instanceGet`, `platformInstanceDomain`, `explicitGate`,
     `instanceChoices`,
     `deployRegionPreflight(res, readErr, …)`,
     `instanceCreatedLine(rec, envSuffix)` and `KeyFingerprint`.
4. **Review Focus.** Five items, each with a test in its owning task: Tasks
   19 and 3, Task 5, Task 13, Task 15 (with Task 11), and Task 12.

---

## Review dispositions (Astra, plan round 1, on 9513d52c)

| # | sev | finding | disposition |
| --- | --- | --- | --- |
| 1 | P1 | GeoIP errors can carry the client's IP into the log | **accepted.** Verified: the pinned reader's error names the address. Task 5 logs a fixed category (O5), with a test on an IPv4-only fixture asserting the address never appears. |
| 2 | P1 | `deploy --region` ships when its region read fails | **accepted.** `deployRegionPreflight` stops on any read or decode error (C15b) and skips only for a successful response without `app_default`. Task 21 adds a binary-level test asserting zero writes and zero releases. |
| 3 | P1 | Older consoles let the first fleet canary through | **accepted.** No `app_default`, or a failed read, refuses `--region` on every run (C30b). Task 22's e2e runs the command twice against both shapes and asserts the same refusal and no writes. |
| 4 | P1 | The service gate can pass through a proxy | **accepted.** The script unsets the proxy variables, uses `-q` and `--noproxy '*'`, and checks `%{remote_ip}` against the named EIP. Task 23 tests it under a dead proxy. |
| 5 | P1 | Task 12's tests treat `postJSON`'s map as bytes | **accepted.** Verified at `authcli_test.go:20`. The tests use the map and `errOf`. |
| 6 | P1 | Fixture rewrites used create-only `Put` | **accepted.** Verified at `release/store.go:328`. `setEdges` and `corruptManifest` replace under the current etag and check every error. |
| 7 | P1 | `TestNonHomeAppRegions` had no `bab` account row | **accepted.** It now uses `newConsoleFixture`, whose `NewServer` backfill creates it. |
| 8 | P1 | HTML assertions ignored `html/template` escaping | **accepted.** Rendered-copy assertions compare `html.EscapeString(...)`. The negative check keys on an element id. |
| 9 | P2 | The migration test copied only empty values | **accepted.** It seeds a pre-rebuild row carrying `uk`/`timezone`, and checks it after the rebuild, a reopen, `RenameAppIn` and `MoveApp`. |
| 10 | P2 | The property test allowed "never guess" | **accepted.** It generates manifests, uses `instanceRegionAllowed`, computes expected existence independently, and adds a mutation step. |
| 11 | P2 | Several tests did not reach the protection they named | **accepted.** `Locate` is tested through a counting lookup seam. The service-principal case goes through HTTP with a real service token. A lost race after resolution (seam) and a held apex are tested. Create assertions require 201 first. |
| 12 | P2 | Integration coverage §16 asks for was missing | **accepted.** Added: C20 (seam), pool and fleet precedence with a non-home default, C27 on a placement read failure, every `app_default` reason, person-IP inference through the door, command-level deploy and canary proofs, and the gate against the edge's real backhaul with and without a mesh key. |
| 13 | P2 | Copy was written before review, and approval inferred from commit subjects | **accepted.** Task 0 reviews the complete inventory first, including operator lines, usage, scripts and the fixture README. Later tasks copy from the record, and Task 25 proves every approved string is present in its file. |
| 14 | P2 | Unreadable defaults rendered as "New instances start in…" | **accepted.** C7 shows only when `Available` is true. Unreadable, fleet and pooled show nothing, with tests. |
| 15 | P2 | No missing-coordinate diagnostic | **accepted.** `regionCandidates` logs O7 once per code, with a throttle test. |
| 16 | P2 | Fixtures fetched from a moving branch | **accepted.** One commit is resolved first, and all fixtures, the source JSON and both licences are fetched at that sha. |
| 17 | P2 | Incomplete cross-compile and release gates | **accepted.** The five CI targets use `CGO_ENABLED=0 go build ./...`. The dev service gate, rollback-interval recording, the floor-version check across three documents and the tz-refresh checklist step are all in D6, D7, D10 and Rollout. The scratch and cache rules follow the operator's instructions: default Go cache, `/tmp/g` only for socket paths. |
| 18 | P2 | C25 got a code, not a place | **accepted.** `canaryRegionCheck` names the existing region through `apiRegionsResult.nameOf`. |

Also folded in: **spec rev 6, ruling R9.**

- Task 1b publishes `secrets_key_fp` through `appconfig.KeyFingerprint`.
- Task 9 derives the expected fingerprint from `CARLOS_SECRETS_PUBKEY`.
- Task 10 makes it a positive eligibility fact, adds the `no-secrets-key`
  reason and `NoKeyProven`.
- Tasks 12 and 14 refuse explicit creates into a provably keyless region
  (C31).
- The Rollout stages the key before the box release.

A copy note for Paul is in Task 0: C25 says `carlos canary remove`, but the
verb is `carlos canary rm`.

## Review dispositions (Astra, plan round 2, on fcf15ae9 with spec 9ccfaca6)

Every code block these findings touched was proved, not just read. A
throwaway worktree was cut at the branch base (e8406ce6). The plan's Go
blocks for Tasks 1, 1b, 2 to 12, 14, 15, 18 to 22 were extracted from this
file verbatim and applied. The prescribed tests were run before each
implementation step (all failed, mostly as build failures) and after it (all
passed). The scratch worktree and branch were deleted afterwards. The
results are in the "Proof" column.

| # | sev | finding | disposition | proof |
| --- | --- | --- | --- | --- |
| R1-9 | P2 (partly) | Migration preservation | **accepted**, see R2-6 | `TestRegionValuesSurviveRebuildRenameAndMove` FAIL before Task 8's code, PASS after |
| R1-12 | P2 (partly) | Existing-canary zero-write proof missing | **accepted**, see R2-4 | `TestCanaryRegionRefusesBeforeAnyWrite/existing_canary_elsewhere` PASS |
| R1-13 | P2 (partly) | Diagnostics outside the copy inventory | **accepted.** O11 to O18 added to Task 0, after a scan of every prose literal the scratch diff added. Task 25 step 2b repeats that scan before landing. | the scan found no other unlisted prose |
| R1-17 | P2 (partly) | `/tmp/g` called disk-backed | **accepted.** Task 25 runs socket packages on `/tmp/g` (stated as tmpfs) and everything else on the on-disk `TMPDIR`. | gate script restructured |
| R2-1 | P1 | Incomplete edge reads could prove a region keyless | **accepted.** `readEdgesSnapshot` returns `Complete=false` on any failed GET or malformed file, and `NoKeyProven` requires a complete snapshot. Positive eligibility is unaffected. | `TestNoKeyProvenNeedsACompleteRead` PASS; with `snap.Complete` removed it FAILs ("an incomplete read proved a region keyless") |
| R2-2 | P1 | Ineligible boxes located a candidate | **accepted.** A coded candidate's AWS region is tallied only from eligible boxes; the home keeps its own tally. | `TestCandidateLocationComesFromEligibleBoxes` PASS; tallying every box FAILs ("located uk at ap-southeast-2") |
| R2-3 | P1 | A 200 marked `policy-unreadable` passed the CLI checks | **accepted.** `app_default` gains `region_known` and `placement_known` (additive), and an unread region is left empty, never filled with the home. `deploy --region` refuses on an unknown region (new C15c); `canary --region` refuses on an unknown placement (C30b). | `TestAppDefaultKnownFlags` PASS; e2e `mode=region-unknown` and `placement_unknown` refuse with 0 writes |
| R2-4 | P1 | Existing-canary mismatch checked after a write | **accepted.** One check, with reads only (`GET …/regions`, `GET …/instances`, matched by host prefix `<name>.<app>.`), before `instancesEnable`'s PUT. | `TestCanaryRegionRefusesBeforeAnyWrite` 4/4 PASS with 0 writes, no manifest, record unchanged; with the check moved after the PUT, 4/4 FAIL with "writes 2" |
| R2-5 | P1 | Three compile failures | **accepted.** Task 12's unused `net` import removed. Task 20 names every caller, including `apimode_test.go`'s six, `apps_test.go:311` and `deploy_wizard_test.go:104`. Task 22's block sits after `ctx` is declared. | `go build ./...` and `go vet ./...` clean on the whole scratch tree |
| R2-6 | P1 | The migration test's rename could not pass | **accepted.** The test seeds the ops account row before `RenameAppIn` and `MoveApp`. | PASS (see R1-9) |
| R2-7 | P2 | No test for R9's addr-backed exemption | **accepted.** An explicit addr-backed create into a keyless region must return 201. A mutation step widens the gate. | PASS; with the gate widened to every kind it FAILs ("want 201") |
| R2-8 | P2 | Missing-key diagnostic gated on the switch | **accepted.** O2 logs whenever `CARLOS_SECRETS_PUBKEY` is unset; O1 stays switch-gated. A switch-off case was added. | `TestLogRegionConfig` PASS |
| R2-9 | P2 | Second mutation check proved nothing | **accepted.** The steps restore and re-run the baseline between mutations. | baseline PASS; mutation A FAILs ("existence false, want true"); restore PASS; mutation B FAILs ("existence true, want false"); restore PASS |
| R2-10 | P2 | Cross-build loop returned 0 on failure | **accepted.** Every loop records failures and ends with `[ "$fail" = 0 ] \|\| exit 1`. | all five targets built with exit 0; a loop with a bad target (`plan9/bogus`) exits 1 before the commit line |

Fixes the proof surfaced, beyond Astra's findings, all now in the tasks:

- **Task 1b:** `edgesfile.go` needs the `appconfig` import.
- **Task 9:** `cmd/console/main_test.go` is `package main_test`, so the new
  tests go in `cmd/console/region_test.go` (`package main`).
- **Tasks 11 and 12:** `go vet` rejects unkeyed `geo.Point` literals across
  packages, so they are keyed.
- **Task 15:** the guess route joins `exemptFromLegacyPin`
  (`svcroutes_test.go`), or `TestInstanceRoleMatchesTodaysAppTokenReach`
  fails.
- **Task 21's e2e:**
  - builds the binary before `useTempCredentials` changes directory;
  - runs each refusal case as a subtest;
  - passes `--channel canary/e2e`, so the older-console case really ships.

Whole-package results after all of the above:

- `go test ./internal/console`: `ok` (356.9s).
- `internal/geo`, `internal/appconfig`, `internal/provision` and
  `cmd/console`: `ok`.
- The root package could not be run as a whole on this machine. The CLI
  walks up from the working directory and finds the operator's real
  `/home/paulca/.carlos/config`, so a pre-existing test exits with "not
  logged in". The unmodified base fails identically. Every new root test was
  run by name and passed.
- The 211 existing tests in the changed CLI files (`canary_test.go`,
  `instances_*_test.go`, `apps_test.go`, `deploy_*_test.go`,
  `apimode_test.go`) were run one at a time on both trees. Outcomes were
  identical: 173 PASS on each, and the same 38 blocked on each by that
  config. Task 25 step 2 runs them all in CI, where no such config exists.

## Review dispositions (Astra, plan round 3, on b7c5b60d with spec 9ccfaca6)

Astra judged `region_known` and `placement_known` sound, and Paul accepted
them. Spec rev 6.1 (8baddbaa) records them as R9a with Astra's semantics.

Every code-touching fix was proved the same way as round 2. A throwaway
worktree was cut at the branch base (e8406ce6), and rev 3's blocks were
replayed into it and committed there as the baseline (build, vet and the
region tests all green). Each fix's tests were written into it first and
run failing, then the fix was applied and they were run passing. The final
rev 4 blocks were then copied back out of this file into the scratch tree
and re-run, so what is written here is what was run. The scratch worktrees
and branch were deleted afterwards.

| # | sev | finding | disposition | proof |
| --- | --- | --- | --- | --- |
| R1-13 | P2 (partly) | Task 17's `placement discrepancy check: %v` outside the inventory | **accepted.** O19 added. This round's new strings are in too: O20 (`instance: unreadable response: %w`) and C30c. | a scan of every prose literal the rev 4 scratch diff added found exactly C30c and O20 |
| R1-17 | P2 (partly) | `/tmp/g` invents an exception to the scratch rule | **declined.** It is the brief's explicit instruction for socket tests, and it is needed: a Unix socket path is capped at 108 bytes, and the on-disk `$TMPDIR` here is too deep for Go's test directories plus socket names. Global Constraints now states this reason. Nothing else uses `/tmp/g`, and nothing in it is deleted. | `TMPDIR=/tmp/g go test ./internal/host`: `ok` |
| R3-1 | P1 | A capped or partial instance list let `canary --region` write | **accepted.** `GET …/regions` gains an additive `instance_domain` (Task 15). The CLI names the canary's host from it and reads that ONE record with a new `instanceGet` (`GET …/instances/{host}`, found only on a 404 being false) before any write. A failed read, or no domain, refuses with C30c. After the PUT, the host built from the PUT's domain must equal the host checked, or the command stops before any record write. | `TestAppDefaultInstanceDomain` FAIL before, PASS after (and its domain equals the PUT's own). `TestCanaryRegionRefusesBeforeAnyWrite`: with rev 3's list scan, `past_the_list_cap` (520 records ahead) and `unreadable` (the canary's store GET fails) FAIL, the command reaching the PUT. With rev 4, 6/6 PASS with 0 writes, no manifest, and the canary unchanged. Moving the check after the PUT: 6/6 FAIL with "writes 2". |
| R3-2 | P1 | Pooled defaults declared available when the door refuses them | **accepted.** A pooled `available` is `pol.allows("")`, the allow-list's verdict on the home where a no-region pooled create lands. The `pooled` reason stays, so the Instances tab still shows nothing. | `TestPooledAvailabilityMatchesTheCreateDoor` FAIL before ("available" true for `Regions:["uk"]`), PASS after; the create door agrees in both cases |
| R3-3 | P2 | F1's `<sha>` substitution breaks the byte-for-byte check | **accepted.** Task 0 Step 0 resolves the MaxMind-DB commit before review, so F1 is reviewed with the real sha. Task 5 reads the sha from the approved record and writes the README with `jq -j` of F1's text, substituting nothing. | round trip on a sample record: README equals F1's text byte for byte; a text naming two commits is refused |
| R3-4 | P2 | Nothing proved eligibility is one box's facts | **accepted.** Task 10's `splitCapability` has three same-region pairs that between them publish every fact (including a keyless ordinary box beside a keyed named-pool-only box). Task 10 asserts each is ineligible, Task 11 that none is guessed, and Task 1's `TestEligibilityPairsWithTheReconcileRefusal` runs the real `Reconcile` on an ordinary record for each published answer. | all PASS. Union-of-facts mutation: `TestEligibilityNeverCombinesBoxes` FAILs on all three pairs (the guess test still passes, because a split region has no eligible box to locate it). Publishing `true, true` always: the pairing test FAILs on the three configs the reconciler refuses. |
| R3-5 | P2 | No persisted-record compatibility tests | **accepted.** `TestPersistedRecordsMatchTheirGoldens` compares the stored bytes (`created_at` blanked). The legacy golden was taken from the base tree's own door for the same request; the app-defaulted `uk` golden differs only by `"region":"uk"`. It also asserts no steering stamp in the response. | PASS. Stamping `Steering` on app-defaulted records FAILs it. |
| R3-6 | P2 | Replica creates lost their place name | **accepted.** `region_name` is filled for every non-fleet placement, replicas included. | `TestReplicaCreateNamesItsPlace` FAIL before, PASS after. CLI: `TestInstancesCreateReplicaPrintsItsPlace` prints `(channel beta)` before and `(channel beta, uk)` (C14) after. |

A fix the proof surfaced, beyond Astra's findings:

- **Task 1 Step 8:** three existing `internal/host` tests pin the raw
  edges-file body. With `serves_shared_pool` and `accepts_inbound` always
  published they fail on exactly those two keys. The step now updates the
  four golden literals with a checked `sed`. Task 10's Step 5 also named a
  test that does not exist; it now runs the real names.

Whole-package results on the rev 4 scratch tree:

- `go build ./...`, `go vet ./...` and `gofmt -l` clean; all five
  cross-compile targets built.
- `go test ./internal/console`: `ok` (383.8s). `internal/geo`,
  `internal/appconfig`, `internal/provision` and `cmd/console`: `ok`.
  `TMPDIR=/tmp/g go test ./internal/host`: `ok` after the Task 1 Step 8
  update.
- Every new and changed root test, run by name: PASS.
- The 137 existing tests in the changed CLI files (`canary_test.go`,
  `instances_environment_test.go`, `instances_cli_test.go`,
  `instances_e2e_test.go`, `apimode_test.go`) were run one at a time on the
  rev 3 and rev 4 trees. Outcomes were identical: 112 PASS on each, and the
  same 25 blocked on each by the operator config described in round 2.

For Paul, outside the findings:

- `instance_domain` is a new additive field on `app_default`, not in the
  spec. It exists only so `canary --region` can read its own record before
  writing. Spec §9.3 may want a line for it.
- C30c is new copy, in Task 0's inventory for the copy review.
- C25's draft still says `carlos canary remove`; the real verb is
  `carlos canary rm` (noted in Task 0).

## Review dispositions (Astra, plan round 4, on 5600586a with spec 8baddbaa)

Astra judged `instance_domain` sound, including the check of the PUT's
answer against the preflight host, and Paul approved it on 2026-10-03. Spec
rev 6.2 (56756f09) records it in §9.3 beside R9a, with the R9a consequence
finding 1 found.

The proof was done as in rounds 2 and 3. A throwaway worktree was cut at the
branch base (e8406ce6), rev 4's blocks were replayed and committed there as
the baseline (Task 13 included this time), and the region tests were green
on it. Each fix's tests were written first and run failing, then the fix
was applied and they were run passing. The changed blocks were then
compared with the scratch files byte for byte; every one matched. The
scratch worktrees and branch were deleted afterwards.

| # | sev | finding | disposition | proof |
| --- | --- | --- | --- | --- |
| R1-17 | P2 (partly) | `/tmp/g` exception, and the scripts' `/tmp` fallbacks | **resolved as an operator rule.** Global Constraints now states the operator's rule for this machine: only the Unix-socket test packages (`internal/host`, `internal/activator`, `internal/edge`, and the other packages whose tests bind a Unix socket, listed there) run with `TMPDIR` under `/tmp/g`, because socket paths are capped at 108 bytes and the on-disk `TMPDIR` is too deep. The directory is small and is deleted after the run, and everything else uses the on-disk `TMPDIR`. CI is unaffected. Every socket-test command goes through one `socktest` function, which makes a fresh `/tmp/g/nr.XXXXXX` and deletes it after the run, so nothing else in the shared `/tmp/g` is touched. Task 25 splits the packages the same way. The two shipped scripts keep `${TMPDIR:-/tmp}`: they run on operator machines and in CI, where an unset `TMPDIR` means the system default, and on this machine `TMPDIR` is always set, so they never reach `/tmp` here. | `socktest` over the eight socket packages: `/tmp/g` listing identical before and after. `host`, `instancectl`, `instanceexec`, `unixdial`, `vet`, `agentsock` `ok`. `edge` (1 test) and `activator` (2 to 3 exec-backend tests, 2s timeouts) fail identically on the unmodified base, so they are this machine's, not the branch's |
| R4-1 | P1 | Pooled and fleet branches broke R9a when the stored region was unread | **accepted.** Task 15: in both branches `effective` and `available` are set only after a successful region read; `reason` (`pooled`, `fleet`) still says what the placement is. Task 22: `canary --region` needs both flags, because its fleet check reads `effective`; a false `region_known` refuses with C30b. Spec rev 6.2 states this. | `TestAppDefaultUnreadRegionHidesEffective` FAIL before (pooled: `effective:ie`, `available` set; fleet: `effective:fleet/acme`), PASS after. `TestCanaryRegionCheck`'s two region-unknown cases FAIL before, PASS after. e2e `fleet, region unknown`: rev 4's CLI goes past the check; after, it refuses with C30b twice, 0 writes, no manifest |
| R4-2 | P2 | Refusals listed choices the door then refused | **accepted.** Task 12: one predicate, `explicitGate.refusal`, decides both acceptance and the C18/C19 lists (home's name, plus published codes that are valid, armed and hosting, and not proved keyless). Task 14: a new `instanceChoices` lists, for C16 and C17, what an explicit request for the same socket-backed instance would pass: the allow-list as the door reads it, the armed gate and R9, minus the refused region. An empty list gets a new `b` variant (C16b to C19b, in Task 0 for copy review) instead of "Choose from: .". | Both tests submit every advertised code under the same fixture and expect acceptance. Before: C18/C19 listed `au` (keyless) and `nz` (not hosting); C16 listed `ie, au` with `Regions:["uk","au","nz"]`; C17 listed `au, nz, ie`. After: `ie, uk`; `au`; `au`, each accepted. Mutations: an allow-list-blind `instanceChoices`, and an app list that takes `no-key` codes, each FAIL. `TestAppDerivedRefusals` now expects C17b (its `au` publishes no box) |
| R4-3 | P2 | The unchanged-form contract was untested and broken | **accepted.** The picker block now opens on the name input's line and closes on the script's line, so with no picker the template emits today's bytes. `TestNewAppPageWithoutPickerIsUnchanged` compares the rendered form with the base tree's own rendering (CSRF blanked), with the switch off and with one eligible region. | FAIL before in both cases (an extra `"  \n"` before the button); PASS after. The picker tests still pass |

Whole-package results on the rev 5 scratch tree:

- `go build ./...`, `go vet ./...` and `gofmt -l` clean; all five
  cross-compile targets built.
- `go test ./internal/console`: `ok` (142.0s). `internal/geo`,
  `internal/appconfig`, `internal/provision` and `cmd/console`: `ok`.
  `internal/host` through `socktest`: `ok`.
- Every new and changed root test, run by name: PASS, including the 7
  canary e2e cases and the replica output test.
- A scan of the prose literals rev 5 adds to product code found exactly
  C16b to C19b.

For Paul, outside the findings:

- C16b, C17b, C18b and C19b are new copy, in Task 0's inventory.
- Spec rev 6.2 also writes down that `canary --region` needs
  `region_known` as well as `placement_known`. It follows from R9a, which
  leaves `effective` empty without the stored region, rather than being a
  new ruling.

## Review dispositions (Astra, plan round 5, on 7db8869f with spec 56756f09)

Astra found every earlier finding resolved and accepted the `/tmp/g`
operator rule. One new P1. It was proved as before: a throwaway worktree at
the branch base (e8406ce6), rev 5's blocks replayed and committed there as
the baseline, the new tests run failing against rev 5's wiring and passing
after the fix, and the changed blocks then compared with the scratch files
byte for byte (all matched). The scratch worktrees and branch were deleted
afterwards.

| # | sev | finding | disposition | proof |
| --- | --- | --- | --- | --- |
| R5-1 | P1 | `canary --region` discarded the record it read, so a same-region canary past the list's cap took the create path | **accepted.** `canaryRegionCheck` returns a `canaryPreflight` (host, record, found). `canaryPlanFor` makes the one create/rechannel/ready decision from it when `--region` was given, and the list decides only without `--region`, as today. The `--environment` bind uses the same decision. The Task 21 harness now also counts `POST …/instances`. | `TestCanaryRegionReusesACanaryPastTheListCap` (a `uk` canary behind 520 records, `--region uk`): against rev 5's wiring all three cases FAIL with "1 create request(s)" (the console refuses the duplicate); after, same channel, different channel (rechannelled to `canary/probe`) and with `--environment beta` (bound) all PASS with no create request, 521 records and the release shipped. Reading the list again for the `--environment` decision alone FAILs the third case. `TestCanaryPlanFor` covers the decision table. The other 7 canary e2e cases, the replica test and the deploy tests still PASS |

Whole-tree results on the rev 6 scratch tree: `go build ./...`,
`go vet ./...` and `gofmt -l` clean, and all five cross-compile targets
built. The 17 existing tests in `canary_test.go` were run one at a time on
the rev 5 and rev 6 trees: 17 PASS on each.

Rollout: `CARLOS_SECRETS_KEY` is on all four flagship boxes as of
2026-10-03 (SSM `/carlos/secrets-key` in every region; config sync and
generation armed everywhere), so step 2 is marked done for the flagship.

## Review dispositions (Astra, plan round 6, on 4df028e2 with spec 56756f09)

Astra found every earlier finding resolved. One new P2. It was proved as
before: a throwaway worktree at the branch base (e8406ce6), rev 6's blocks
replayed and committed there as the baseline, the new test run failing
against rev 6 and passing after the fix, and the changed blocks compared
with the scratch files byte for byte (both matched). The scratch worktree
and branch were deleted afterwards.

| # | sev | finding | disposition | proof |
| --- | --- | --- | --- | --- |
| R6-1 | P2 | Task 12 logged spec §6.5's creation line only with assignment on | **accepted.** O8 is logged after every successful insert and lost-race check, in every switch state, naming what was stored. The failed-claim silence tests stay. | `TestClaimLogsOneRegionLineInEverySwitchState`: against rev 6, `assignment off` and `on, home unnamed` FAIL with 0 region lines; after, all three PASS with exactly one line each (`bab/on1: uk (chosen)`, `bab/off1: ie (default)`, `bab/unnamed1:  (default)`) and the stored region and source as named. Wrapping the log in `if s.appRegionsOn()` again FAILs it. `TestClaimLostAfterResolutionWritesNothing` and `TestClaimHeldApexWritesNothing` still PASS |

Whole-tree results on the rev 7 scratch tree: `go build ./...`,
`go vet ./...` and `gofmt -l` clean; all five cross-compile targets built;
`go test ./internal/console` `ok` (147.2s) and `cmd/console` `ok`.
