# Panama: fresh-install correctness and the sunhat deprecation

Panama installs a desktop it does not fully provision. The Quickshell shell names
fonts nothing installs, a cursor theme nothing installs, and a wallpaper that does
not exist; four settings pages shell out to packages no list declares. None of this
shows up on this machine, because this machine accumulated those pieces under
sunhat and never lost them. It shows up on the next machine.

This spec closes that gap, moves every question the installer needs to the front of
the run, and ports the parts of sunhat that still matter — so sunhat can be deleted.

## Why now

Recent work built a large Quickshell desktop: a bar, dock, overview, lock screen,
notification server, and a Settings application with thirty-odd pages. Almost all
of it was verified against a running system. Almost none of it was verified against
a system where Panama had just been installed for the first time.

The existing `tests/quickshell/declared-dependencies-contract` was written for
exactly this class of bug and reports PASS, because it checks only commands that
scripts invoke. Fonts, cursor themes, wallpapers, and packages consumed by GUI
handoffs are invisible to it. A dependency checker with a blind spot is worse than
none, because it reports PASS — that argument is already in that file's own header
comment, and it applies to the checker itself.

## What is broken

Verified against the repository at `3b01f1e`.

| Gap | Evidence |
|---|---|
| No fonts installed | `setup/scripts/change-settings:32` sets `VictorMono Nerd Font 10`. `kitty`, `ghostty`, `hyprlock.conf.template`, `Theme.qml` and `Fonts.qml` all name Nerd Font families. No stage installs a font. `Fonts.qml` documents that a mono family without Nerd glyphs replaces every icon in the shell with tofu. |
| No cursor theme installed | `oreo_blue_cursors` is named in `change-settings:33`, both GTK `settings.ini.template` files, `config/dot/uwsm/env:23`, `config/dot/hypr/env.lua:52`, and `PreferenceSchema.qml:985`. It exists in `~/.local/share/icons` on this machine only. |
| No wallpaper, though the code says one ships | `Wallpaper.qml:32` declares `shippedPath` as `~/Pictures/Wallpapers/faroe_islands.jpg`. `panama-lock:139` falls back to the same path, `hyprpaper.conf` names it twice, and `lock-screen-settings-harness.qml` uses it as a fixture. Nothing ships it. `PreferenceSchema.qml:826` defaults `wallpaperPath` to `""`. |
| `gnome-control-center` undeclared | `SystemSettings.qml:599` allow-lists 23 GNOME panels; 15+ rows across `OnlineAccountsPage`, `RegionPage`, `AccessibilityPage`, `ConnectivityPage`, `ShortcutsPage`, `SoundPage`, `PrivacyPage` and `HealthPage` call `openGnomePanel`. No list declares the package. |
| `snapper` undeclared | `panama-snapshots` drives the entire Snapshots page through `snapper`. |
| Four more undeclared, found by the new contract | Crawling QML for launched commands — which the dependency contract never reads — also caught `gnome-calendar` (`CalendarAgenda.qml:180`), `podman` (`Containers.qml:149`, the Containers page backend), `pipewire-utils` for `pw-dump`/`pw-play` (`PrivacyState.qml:169`, `SoundTest.qml:35`), and `flatpak`, which `install-packages` itself uses. |
| `espanso`, `ghostty` undeclared | Both have configs symlinked by `link-dotfiles`; neither is in a package list. `ghostty` is in Terra and is now declared. **`espanso` is in no enabled repository** — sunhat built it from source — so under the dnf/flatpak rule it stays undeclared. Its config remains linked, which costs nothing, and installing it is a separate decision. |
| `.bashrc` breaks on a fresh box | `config/bash/.bashrc` ends with an unguarded `. "$HOME/.cargo/env"`, which does not exist until `rustup-init` has run, so every shell start emits an error. The file also carries a committed `# >>> Codex installer >>>` block. |
| README describes a desktop that is not installed | The Desktops section advertises GNOME with Forge, Dash-to-Dock, Openbar and Vitals. Nothing in `setup/` installs or configures any of it. |

## Decisions

These were settled during design and are recorded so the plan does not relitigate
them.

**The GNOME session is cut; `gnome-control-center` stays.** These are different
things and only the first is free. The session — Forge, Dash-to-Dock, Openbar,
Vitals, the README's parity claims — is dead weight and goes. The control center is
load-bearing: it is where Panama's own Settings sends you for Online Accounts,
Region, Accessibility, Color, Wellbeing and Wacom, and commit `0c364f3` shows the
handoff list was deliberately narrowed to what Panama does not own. It becomes a
declared dependency rather than an accident of this machine's history.

**The installer asks everything up front, then runs untouched.** sunhat's failure
mode was a prompt or a failure twenty minutes in, with a person needed at the
keyboard to get past it. Panama's answer is not "no prompts" — some of what the
installer needs genuinely changes between machines and between years, and a stale
hardcoded answer is worse than a question. The answer is that every question is
asked before any package work begins, and nothing asks again afterwards.

**Answers are not persisted.** Each run asks fresh. The only default shown is the
current hostname, which today's installer already shows. This is a deliberate
simplicity choice: no answers file to keep in sync, nothing personal on disk, and
re-answering six questions costs less than maintaining state that can go stale.

**Hardware is detected, then confirmed.** Gib's machines span AMD, Intel and NVIDIA
in both laptops and desktops. The installer probes — `lspci` for GPUs,
`mokutil --sb-state` for secure boot, DMI for chassis — and asks a question that
names what it found: *"Found NVIDIA RTX 4070 — install akmod-nvidia and enroll a
secure boot key? [Y/n]"*. Detection alone would remove the ability to decline a
proprietary driver on a machine that has the card; asking alone would make a person
recite their own hardware.

**Nothing personal is committed.** Panama should be cloneable by someone else and
yield a working desktop. Identity, hostnames and machine layout come from prompts
and from gitignored local state — `~/.config/panama/settings.json` is already
exactly this — never from files in the repository.

**Applications are admitted one at a time, and dnf or flatpak only.** A short
always-installed core, plus categories chosen from a `gum` checklist during the
interview, so a work laptop does not acquire emulators and a desktop does not skip
Steam. But the menu starts nearly empty and grows by explicit decision, one
application per decision — it is not seeded by porting sunhat's list.

The categories shipped are `gaming`, `creative`, `communication` and
`virtualization`, one file each under `setup/packages/extras/`. A category mixes
both package managers because the applications do — Steam is in RPM Fusion, Slack
publishes only a flatpak — so a bare line is a dnf package and a `flatpak:` line
is a Flathub ID, and one file holds the whole answer for a category rather than
splitting it across two. The interview reads the menu from the directory, so
adding a category is adding a file, and nothing is preselected: a default here
would install applications nobody chose.

The rule is that every application Panama installs must come from dnf or from
Flathub, with flatpak preferred where both exist. Anything requiring a pinned RPM
URL, a `curl | bash`, or a source build is admitted only case by case, with a stated
reason why no packaged form exists.

sunhat is the argument for this rule, not a counterexample to it. Its
`sources/dotenv.sh` hardcodes `upscayl-2.11.5`, `lact-0.5.4-0.x86_64.fedora-40.rpm`
and `jdk-22` — URLs that were stale within a release cycle and that turn a re-run
into a 404. Every application Panama declares is one it has to keep installable,
and the cost of that promise is only bearable if a package manager is keeping it.

Panama already carries three exceptions: the `oh-my-posh` and `bun` install scripts
in `install-packages`, and `helium-browser-bin` from the `lionheartp/Hyprland` COPR.
They stay — each is idempotent and guarded — but they are the ceiling, not a
precedent. New exceptions need the same standard.

**Bash scripts carry no `.sh` extension.** A shebang and the executable bit select
the interpreter; the extension is redundant. This applies to the 159 existing files
as well as new ones.

## Design

### The installer

`./install` becomes four movements:

```
ascii  →  interview  →  stages (uninterrupted)  →  panama-doctor summary
```

The interview is a new script, `setup/scripts/interview`, that runs before any
other stage and asks, in order:

- Hostname — defaulting to the current one, as today
- Git identity: name, email, editor
- Run `gh auth login`? — skipped without asking when `gh auth status` already passes
- Generate an SSH key? — skipped without asking when one already exists
- Hardware confirmations, each naming what was detected
- Remove Fedora's preinstalled bloat?
- Update firmware with `fwupdmgr`?
- Which application extras to install — a `gum` checklist

Answers reach the stages as `PANAMA_*` environment variables, but not by the
obvious route. Stages run as child processes of `install`, and a child cannot export
into its parent or its siblings — so the interview cannot simply set variables and
expect the next stage to see them. Instead it writes `KEY=value` lines to a
`mktemp` file whose path `install` chose; `install` sources that file, exports the
names, and deletes it on the existing `trap ... EXIT INT TERM` that already restores
the idle settings. The answers exist for the length of one run and never touch a
durable path, which is what keeps "no persisted answers" honest.

The interview is therefore listed in `STAGES` like any other stage, but `install`
treats it specially: it is the only stage whose output the installer reads back.

A stage that needs an answer reads its variable and treats absent as "no". This
matters for re-running an individual stage by hand, which is a debugging path that
must keep working, and it means the interview is never a prerequisite for repairing
one piece of a machine.

The run ends with `panama-doctor`, printing its summary. Today the only signal that
an install worked is a failed-stage count; a failure that produced no non-zero exit
— a font that did not land, a service that did not start — scrolls past unnoticed.
`docs/UPSTREAM-INSPIRATION.md` already lists a first-run health check as a
candidate; the health check now exists, so this is wiring rather than building.

### Stages

The `STAGES` array in `install` is explicit rather than glob-ordered, and stays that
way. The new order:

| Stage | Status | Responsibility |
|---|---|---|
| `interview` | new | Every prompt, before any work |
| `install-packages` | edited | Repos, package lists, extras |
| `link-dotfiles` | edited | Adds the wallpaper and the cursor theme; drops `forge` |
| `change-settings` | edited | Unchanged in substance |
| `link-vicinae-scripts` | unchanged | |
| `setup-identity` | new | git config, `gh auth login`, SSH key |
| `install-hardware` | new | NVIDIA, akmods, secure boot, debloat, firmware — all conditional |

Two things in this table changed while building it, and the reasons are worth
keeping.

There is no `install-cursors`. The cursor theme is vendored into the repository
and exposed by the same narrow per-directory symlink `link-dotfiles` already uses
for icons and Vicinae themes, so it needs no stage of its own — and because
`link-dotfiles` precedes `change-settings`, the ordering constraint that stage
was invented to satisfy holds anyway.

`install-hardware` runs *last*, not fourth. The constraint was always "late", and
fourth of eight is not late: MOK enrollment arms a prompt consumed at the next
boot and a firmware update may ask for a reboot, so a machine that reboots out of
this stage should already be completely configured — dotfiles linked, settings
applied, identity set. Placing it after `setup-identity` costs nothing, since
nothing later depends on it.

Ordering constraints that are not obvious and must not be broken:

- `link-dotfiles` precedes `change-settings`, because `change-settings` sets a
  gsettings value naming a cursor theme. Setting it before the theme exists is not
  an error, but it does mean the first session renders with a fallback. Fonts are
  installed by `install-packages`, which already runs first.
- `setup-identity` follows `install-packages`, because `gh` is installed there.
- `install-hardware` is last, for the reason above. It also has to follow its own
  driver install: akmods generates the certificate that MOK enrollment enrolls, so
  there is nothing to enroll until `akmod-nvidia` is on the machine.

Every stage stays independently re-runnable and independently failable, which is
the existing contract and the reason Panama exists.

### Fonts

There is no font stage. Terra — which `install-packages` already enables — packages
every Nerd Font as an RPM (`victormono-nerd-fonts` and siblings, 3.5.0-1.fc44), and
Fedora now packages the Microsoft core fonts as `mscore-fonts`. So the entire gap
closes by adding five lines to `desktop-packages`:

- `victormono-nerd-fonts` — required. It is what `change-settings` sets as
  `monospace-font-name` and what the shell draws every icon with.
- `jetbrainsmono-nerd-fonts`, `firamono-nerd-fonts`, `cascadiamono-nerd-fonts` —
  Panama ships a font picker; a picker with one option is not a picker.
- `mscore-fonts` — web development means checking pages that assume Arial and
  Georgia resolve.

This supersedes sunhat's approach, which downloaded release zips from GitHub with
`wget` and unpacked them into `~/.local/share/fonts`. That is precisely the install
method this repository is trying to stop using, and it turned out to be unnecessary.

### Cursors

`oreo_blue_cursors` is packaged nowhere — not Fedora, not RPM Fusion, not Terra. It
comes from `varlesh/oreo-cursors` on GitHub, so the dnf/flatpak rule cannot apply and
one of its exceptions has to.

The theme is **vendored into the repository** at
`config/local/share/icons/oreo_blue_cursors` (3.3MB, 63 files, no symlinks) and
exposed by `link-dotfiles` with the same narrow per-directory symlink it already uses
for Panama's application icons and Vicinae themes. Downloading at install time was
rejected: it would add a fourth network exception and a URL that can rot, which is
the failure this spec exists to prevent. Vendoring costs 3.3MB in git once, and then
the cursor is simply always there.

Only the blue variant ships, and nothing else needs changing to make that true:
`cursorTheme` is a free string in the schema, and `panama-desktop-style`'s `catalog()`
enumerates the pointer themes actually present in the XDG icon directories. The picker
therefore offers exactly what is installed, so shipping one theme instead of three
removes options from the list rather than leaving it advertising themes that are not
there.

### Wallpaper

`config/wallpapers/faroe_islands.jpg` (4000x2444, 644K) is committed. `link-dotfiles`
copies it to `~/Pictures/Wallpapers/` when absent, and seeds `wallpaperPath` in
`settings.json` **only when that key is still empty**. An existing choice is never
overwritten — the same rule every other seeding step in `link-dotfiles` already
follows for kitty, tmux, GTK and hyprlock.

### The assets contract

A new `tests/quickshell/declared-assets-contract` covers what the dependency
contract structurally cannot. It fails when:

- A font family named in `config/` or in a `gsettings set` call is not installed by
  `install-fonts`
- A cursor theme named in `config/` is not installed by `install-cursors`
- The default `wallpaperPath` names a file the repository does not ship
- A package invoked by a settings-page helper or a GUI handoff is undeclared
- A directory under `config/dot/` is neither in `link-dotfiles`'s `dirs` array nor
  given explicit handling

All four of today's gaps fail this test. That is the acceptance criterion for the
test itself: it must be written against the broken tree and observed to fail before
the fixes land.

It is named without a `.sh` extension from the start, while its siblings still carry
one until phase 6. That inconsistency is deliberate and temporary — new files follow
the convention immediately rather than being written wrong and renamed later.

### GNOME excision

`config/dot/forge` is deleted and `forge` removed from `link-dotfiles`'s `dirs`.
The README's Desktops section is rewritten to describe one desktop. The comment in
`link-dotfiles` explaining why GNOME and Hyprland coexist goes with it.

`gnome-control-center` is added to `desktop-packages` with a comment naming what
depends on it, so a future reader does not mistake it for GNOME-session residue and
remove it. `gnome-settings-hyprland.desktop` stays.

Searching for the rest of it found nothing else to cut, and that is worth
recording so nobody goes looking again. `change-settings` never enabled a GNOME
extension. The mentions of Dash-to-Dock, Openbar and Vitals scattered through the
shell are comments saying what a component was modelled on — `Dock.qml` explaining
which intellihide behaviour it reproduces, `Theme.qml` naming where a colour came
from — and `DESKTOP-PARITY.md` is the table of what replaced what. Those are
provenance, not residue: they are the reason the components behave the way they
do, and deleting them would cost the explanation while saving nothing. `Vitals` in
`config/dot/quickshell/services/` is Panama's own bar service and merely shares a
name with the extension it replaced.

The panel list this spec gave for the handoff was already wrong when it was
written: it named Wacom, which nothing hands off to, and Region, which is a
subpage of System rather than a panel. The nine actual panels are colour,
keyboard, network, online-accounts, privacy, sound, system, universal-access and
wellbeing, and the allow-list in `services/SystemSettings.qml` is what decides.
The comment on `gnome-control-center` in `desktop-packages` repeated the wrong
list and has been corrected to match.

One test gap turned up here and is closed. The assets contract caught a directory
under `config/dot/` that nothing links, but not the inverse — a name left in the
`dirs` array with no directory behind it, which makes `link-dotfiles` point
`~/.config/<name>` at nothing. Deleting Forge was the exact move that introduces
it, and nothing would have failed if the array entry had been left behind. A
dangling symlink is worse than a missing one, because everything that looks there
finds something.

### The `panama` command

Three subcommands are added to the existing dispatcher, which was written to grow
this way:

- `panama doctor` — front `panama-doctor` from a terminal, not only from Settings
- `panama test` — run every contract under `tests/`, with a pass/fail summary. The
  suite had no entry point and no mention in the README; it is the main safety net
  and it was invisible.

  Building the runner found three tests that nothing was running.
  `calendar_agenda_bridge_test.py`, `home_assistant_bridge_test.py` and
  `kdeconnect_bridge_test.py` are `unittest` suites without the executable bit, so
  no contract invoked them and the first draft of this runner skipped them in
  silence. All three pass, and have presumably passed unobserved for weeks. The
  runner now collects `*_test.py` as well, because a runner with a blind spot is
  worse than no runner for the same reason a dependency checker with one is: it
  reports PASS.
- `panama upgrade` — re-run `./install` from anywhere

### Dropping the `.sh` extension

159 files: 110 under `tests/quickshell`, 47 under
`config/local/share/vicinae/scripts`, 2 under `tests/hypr`. All are already
executable and all already carry a bash shebang, so the rename is `git mv` plus
three comment references and two lines in `panama-settings-commands`, which
generates the Vicinae command files and globs for `settings-*`.

The one real risk is Vicinae's script discovery. Its documentation states a script
command needs a plain text file, the three `@vicinae.*` directives, a shebang, and
the executable bit — no extension requirement. That is close enough to a guarantee
to act on, but not close enough to skip verifying: after the rename, reload with
`vicinae cmd launch core:reload-scripts` and confirm all 47 commands still resolve
before the change is considered done. If discovery does key off the extension, the
Vicinae directory is exempted and the reason recorded here.

**Verified, and discovery does not.** One script was renamed first and reloaded on
its own; it came back as `scripts:panama.capture`, so the whole set followed and
all 47 resolve.

What the probe did turn up is that the extension was never only a filename:
Vicinae's command IDs embed it, so every ID changed — `scripts:panama.capture.sh`
became `scripts:panama.capture`. Nothing in this repository refers to those IDs,
so nothing breaks. The only trace is Vicinae's own `metadata.json`, whose
`visited` map had two Panama entries that are now orphaned; the effect is that two
commands lost their usage ranking and will earn it back. Worth knowing before
renaming these files again, and worth checking for a keybind or deeplink first on
a machine that has one.

### Machine-specific configuration

Monitor layout, keybinds and input differences stay in gitignored local state —
`~/.config/panama/settings.json`, written by the Settings application. Panama ships
defaults; a machine deviates locally. No per-hostname directories enter the
repository, because that is what "cloneable by someone else" costs.

This is a decision to *not* build something. sunhat branched its Hyprland config on
hostname and it was one of the things that made sunhat unmaintainable.

## Ported from sunhat

Everything below blocks sunhat's deletion.

- **NVIDIA and secure boot** — `akmod-nvidia`, `xorg-x11-drv-nvidia-cuda` and MOK
  enrollment via `mokutil --import`. sunhat's version opened an editor to hand-edit
  grub; the replacement does not, because a step that requires a human mid-run is
  the failure this repository exists to avoid. It turned out not to need one:
  sunhat's editor step existed to delete duplicated kernel arguments, and `grubby
  --update-kernel=ALL --args=` replaces an argument that already exists rather than
  appending a second copy, so the duplicates cannot accumulate.

  Enrollment is likewise non-interactive, and did not need a compromise either.
  `mokutil --generate-hash` and `--hash-file` are exactly the pair for this: the
  interview asks for the password, hashes it immediately, and records only the
  hash — so the plaintext never reaches the answers file, the environment, or a
  command line, and the stage still runs without prompting.

  `rebuild_nvidia_kmod` was not ported. akmods already rebuilds on kernel updates
  through its own systemd unit, and a helper that duplicates that is one more
  thing to keep true.
- **Fedora debloat** — behind an interview question. sunhat's list was
  `gnome-contacts`, `gnome-tour`, `gnome-maps`, `totem` and three LibreOffice
  components; on Fedora 44 `totem` has been replaced by `showtime` and LibreOffice
  is not preinstalled at all, so the list is `gnome-contacts`, `gnome-tour`,
  `gnome-maps` and `showtime`. A package that is not installed is never passed to
  dnf, which is what lets the list outlive a Fedora release rather than starting to
  fail on one.
- **Firmware** — `fwupdmgr refresh` and `update`, behind an interview question.
  Panama's Updates page covers this after installation; the installer covers the
  first run.
- **Applications** — the extras *mechanism*, and only the applications explicitly
  agreed. sunhat's ~70 installers are a list of candidates to review, not a manifest
  to port; most of them install by methods that no longer work. Reviewing them is
  its own task, done one application at a time against the dnf/flatpak rule above,
  and it does not block anything else in this spec.
- **Firefox** — the package itself, which Panama did not previously install, plus
  sunhat's `themes/firefox` userChrome. Helium is the daily driver; Firefox stays
  because web development means checking a second engine. If Helium supports
  userChrome, it gets the equivalent treatment.

  The theme is [Edge-Frfox](https://github.com/bmFtZQ/edge-frfox), MIT, vendored
  into `config/firefox` with its provenance recorded — sunhat carried it with
  neither. It is the only piece of Panama's configuration that does not go to a
  path this repository chooses: Firefox owns the profile directory, names it with
  a random salt, and does not create one until the browser has been run. So
  `link-dotfiles` finds or creates a profile and links both halves into it —
  `chrome/` for the CSS and `user.js` for the preference that makes Firefox read
  `chrome/` at all, which without the other is a directory of dead files.

  Two things here were assumptions that turned out wrong, and the contract exists
  for both. Firefox has moved to the XDG directories, so the profile root is
  `~/.config/mozilla/firefox` on a current build and `~/.mozilla/firefox` on an
  older one; writing to the wrong one themes nothing and says nothing. And
  `-CreateProfile` turns out to be non-interactive, so a fresh machine gets the
  theme on the first install rather than the second.

Deliberately not ported: sunhat's `configs/docker/compose_files`. Those are server
workloads — caddy, gitea, homeassistant, nextcloud_aio, pihole, vaultwarden,
wireguard, searxng — and belong to whatever runs them, not to a workstation
installer. They remain recoverable from sunhat's git history.

## Repository hygiene

- **Done.** The seven branches that appeared unmerged were audited with
  `git cherry -v main <branch>`. Six of them — `feat/panama-displays`,
  `feat/panama-settings-pages`, `feat/settings-home-completion`,
  `feat/settings-sound`, `fix/settings-backup-complete` and
  `fix/settings-sidebar-scroll` — had every commit marked patch-equivalent to one
  already in `main`, and every file they added was verified present in `main`. They
  looked unmerged only because the work re-landed through a different route. Deleted,
  along with their worktrees.
- `feat/roadmap-completion` is the sole branch with unlanded work: 5 commits, 13
  files absent from `main`, 62 commits behind. It carries a theme-profile system, a
  dock context menu, overview-thumbnail capture reliability, and wallpaper/lock
  hardening, with its own spec and plan. It does not conflict with this work — it
  *depends* on it, since its lock generator falls back to the shipped wallpaper this
  spec makes exist. Rebasing it is its own task.
- **Done.** Six worktrees remained on branches already merged to `main`
  (`home-accessories-customization`, `panama-commands`, `panama-displays-review`,
  `panama-settings-home-phone`, `settings-notification-rules`, `settings-ownership`),
  and all six are pruned. Each was re-checked rather than trusted to this list:
  `git cherry main <branch>` reported nothing unique and every working tree was
  clean. Two needed the check. `panama-commands` is not on `feat/panama-commands`
  at all but on `feat/gnome-tweaks-parity` — the directory name and the branch had
  drifted apart. And `fix/panama-displays-review` reads `[ahead 3]`, which is ahead
  of its *remote*, not of `main`; all three commits are patch-equivalent to work
  already landed. The branches themselves are left alone: pruning a worktree costs
  nothing, and deleting a branch is somebody's decision rather than tidying.
- Guard the `.cargo/env` source in `config/bash/.bashrc` and remove the Codex block.
- Rewrite the README: one desktop, the real install stages, and the test suite.
- **Done, and it needed nothing.** `docs/settings.md` is generated from
  `PreferenceSchema.qml` by `panama-settings-docs`, and a contract already fails
  when the copy is stale. It is current, all 133 settings, and carries no claim
  about GNOME or Forge at all. The audit this line asked for was a manual reading
  of a file that cannot go stale without a test saying so.

## Phases

Each phase is independently shippable and independently useful.

1. **Install correctness** — fonts and the four other undeclared packages, the
   vendored cursor theme, the shipped wallpaper, the `.bashrc` fixes, and the assets
   contract. Closes every gap that would visibly break a fresh machine.
2. **The interview** — up-front prompts, hardware detection, `panama-doctor` at the
   end of the run.
3. **Hardware flows** — NVIDIA, secure boot, debloat, firmware.
4. **Applications and theming** — the extras menu mechanism, Firefox and its theme.
   The menu ships with the categories agreed at the time and nothing more; filling
   it is ongoing work, one application per decision, not part of this phase.
5. **GNOME excision and documentation** — Forge removal, README rewrite,
   `docs/settings.md` audit.
6. **Hygiene** — the `.sh` rename, worktree pruning, the branch audit, and the three
   new `panama` subcommands.

## Verification

Panama is not going to be tested by reinstalling Fedora. Confidence comes from
three places instead:

- **The assets contract**, which encodes each gap found here as a test that fails on
  the broken tree.
- **The hardware contract**, for the one stage that cannot be verified by running
  it. `install-hardware` installs a proprietary driver and queues a Secure Boot
  enrollment, and the machine Panama is developed on is an AMD desktop; so every
  privileged command is stood in on `PATH` and the contract asserts which answer
  led to which call. That covers the decisions, which is the part that can be
  wrong. It does not cover whether `akmod-nvidia` builds, and does not claim to —
  the first real NVIDIA machine is still the first real test.
- **`panama doctor` at the end of every install**, which reports what is actually
  running rather than what was attempted.
- **`panama test`**, which makes the existing 119 contracts runnable in one command
  for the first time.

One gap this list did not cover was found while doing phase 4 and is worth
recording, because it is the exact failure mode the spec opens with. Every list
in `setup/packages/` is annotated, and `install-packages` passed the whole file
to dnf, comments included; dnf does not ignore an argument it cannot match, so
`set -e` ended the stage on the first annotated list. It could not be seen from
here — on a machine that already has everything, only the comments fail — and
every contract that reads those lists strips comments before comparing, so the
tests were reading a file the installer was not. A checker with a blind spot is
worse than none, and this one had the same blind spot the dependency contract's
own header warns about.

The open risk this leaves is anything that breaks only on a genuinely clean system
and that none of the three notices — a repository that fails to enable, a package
renamed between Fedora releases. That risk is accepted; a VM run can settle it later
if the fixes here prove insufficient.
