Files
Panama/docs/superpowers/specs/2026-08-23-settings-redesign-test-backlog.md
T

361 lines
32 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Settings redesign — deferred test runs
No contract is executed while the settings redesign is in flight: many are live
harnesses that open windows, drive overlays, and run display transactions on
the real desktop. Everything below runs **once, at the end of the redesign,
with Gabriel's go-ahead**, and failures get fixed then.
## The run
- `panama test` — the full suite (**169** contracts as of the phase 6 Sound
wave; the top-level README's count line is set to match and is itself
checked by `setup/readme-contract`).
## Known items to verify or investigate at the end
- `quickshell/welcome-contract` — failed once ("the welcome screen did not
open") during a full-suite run storming the live session; passed in
isolation minutes earlier after the Welcome.qml comment edit. Suspected
contention flake, not a regression. Confirm.
- `setup/readme-contract` — counts contracts; keep the README number in sync
as later phases add contracts (phase 2 added `phone-page-contract`).
- Phase 2 additions already written and passing when last run:
`my-home-settings-contract` (renamed from `home-phone-settings-contract`),
`phone-page-contract`, extended `home_assistant_bridge_test.py` and
`kdeconnect_bridge_test.py`.
- Each later phase (Appearance, …) appends its new/changed contracts here
instead of running them.
## Phase 3 (Appearance) — append below
The contracts wave is done. Everything below is written and **still to be
RUN**. Nothing in this list has been executed against a live harness: the
static halves were checked against the tree, but every harness run and the
full-suite pass are deferred to the end-of-redesign sweep.
### New contracts (3)
| Contract | What it pins |
|---|---|
| `quickshell/theme-catalog-contract` | `config/themes.json` shape (10 themes, 6 dark / 4 light), every palette and ansi block accepted by the model's own validators, moon and day byte-identical to the shell's pre-theme literals, `ThemeCatalog.qml`'s embedded fallback carrying both, and `Theme.qml` holding no palette ternary. |
| `quickshell/video-wallpaper-contract` | Three independent pause reasons (`FocusModes.gameRunning`, `Battery.acOnline` plus the preference, `manuallyPaused`) and pausing over mpv's JSON IPC; the mpvpaper argv (`hwdec=vaapi`, `no-audio`, `loop-file=inf`, `input-ipc-server=`); the hyprpaper stop/start choreography and `Wallpaper.refreshActive()`; `Wallpaper.qml`'s video routing branches; the helper's 60-file cap, depth-2 scan and mp4/mkv/webm filter; the still frame for the lock screen; `WallpaperIndicator` in `Bar.qml`'s right-hand row with its `visible` binding, spoken name and no animation; the two schema keys; the doctor check id. |
| `quickshell/settings-titlebar-contract` | No minimize or maximize anywhere in the settings chrome (comments may say the words, code may not); `height: shown ? 48 : 0` so hiding collapses rather than leaving a hole; `buttonsLeft` side-awareness with exactly one anchor released per element; the close button's `activeFocusOnTab`, `Accessible.role`/`name`, Return/Space handlers and focus treatment, and that it is the only button; Escape declared on the shell rather than inside the bar; the `panamaTitlebar` schema entry and its Appearance row. |
### Updated contracts (6)
| Contract | What it now pins |
|---|---|
| `quickshell/accent-controls-contract` | Rewritten around the new editor: the nine surviving components and their qmldir lines, `AccentPicker`/`ThemeProfilePicker` staying deleted *and* unregistered, the four wells and their single `apply()` path, one hyprpicker invocation aimed by a remembered target, `ColorWell`'s hex validation committing on Enter and focus-out but never per keystroke, the six HSV labels, the debounce (the write lives in `commitTimer`, `changeChannel` writes nothing, `releaseTimer` hands the sliders back), `commitActive` recomputing `accentName` through `nearestCuratedName` with `selectProfile`/`updateActive` both routing into it, `Theme.qml` reading `ThemeProfiles.activePalette`, the Themes and Theme editor tabs, four search labels, and the no-animation ban extended over all nine components. Live half rewritten against the rebuilt harness. |
| `quickshell/theme-profiles-contract` | Node half rebuilt for the optional-field model: `shippedProfiles()` with no argument still returns the three built-in fallbacks (back-compat), the ten catalog records survive with palettes and ansi and no effects, palette/ansi/effects survive a stored record with per-key effect clamping, an invalid palette drops the FIELD not the profile, the caller's shipped list owns the id and name space, `editProfile` passes untouched fields through and a shipped fork carries the whole palette, `nearestCuratedName` mappings (mauve→orchid, gruvbox-yellow→amber, grey→slate, per scheme), and `derivePalette`/`resaturatePalette`/`deriveAnsi`/`mixHex` producing palettes the validators accept. Live half now pins the per-mode selection flow: a light/dark flip returns to the theme chosen on that side, never a forced default. |
| `quickshell/desktop-style-contract` | `titlebarMaximizeButton` and `titlebarDoubleClick` asserted **gone** from the schema and from `DesktopStyle`, along with `action-double-click-titlebar`; `panamaTitlebar` added (bool, def true, group titlebar); the button layout pinned close-only on both sides with `minimize`/`maximize` banned from the function body; the Appearance greps moved to Fonts/Sizes/Rendering with exactly five `FontPicker` rows and their five role labels, plus the honest-titlebar subtitle. |
| `quickshell/settings-ownership-contract` | The ColorScheme block was pinning `inactiveBorderDark`/`inactiveBorderLight`, which no longer exist. It now pins the inactive border as the active theme's `gutter`, bans a regrown literal, pins the accent border roles through the new `hyprColor(value, alpha)` signature, requires both roles to be restated on a theme change, and cross-checks Hyprland's startup literals against moon's and day's gutters in `themes.json`. |
| `quickshell/lock-screen-settings-contract` | The "between Background and Shell typography" ordering check named a card that no longer exists. It now pins Background → Video playback → Lock screen order, and that all three sit on the Background tab. |
| `quickshell/settings-search-contract` | Seven fixed cases added for the new surface: themes, theme editor, dark mode, Catppuccin, Gruvbox, video wallpaper, titlebar. The schema-label sweep already covers the new keys automatically. |
### Verified against the new tree, no edit needed
- `quickshell/settings-hardcoded-values-contract` — its scope is `Settings.qml`,
which the theme work did not touch; `Theme.qml`'s literals are covered by
`theme-catalog-contract` instead.
- `quickshell/wallpaper-service-contract` — already carries the video routing
greps.
- `quickshell/wallpaper-settings-contract`, `wallpaper-policy-contract`,
`settings-nav-contract`, `settings-pages-contract`, `manual-contract`,
`settings-docs-contract`, `qmldir-registration-contract`,
`settings-sync-contract`, `settings-backup-contract` — checked, nothing stale.
- `quickshell/panama-doctor-contract` — already lists `input.video-wallpaper`.
### Docs updated in the same wave
- `modules/settings/README.md` — new **Appearance** section (Themes, Theme
editor, backgrounds still and moving, the honest titlebar); the border
ownership paragraph corrected to the theme's gutter.
- `manual/05-making-it-yours.md` — rewritten Appearance chapter: the two
galleries, per-mode memory, the four-well editor, video wallpapers, and why
there is no minimize.
- Top-level `README.md` — contract count 162 → 165, confirmed by
`setup/readme-contract`.
### Still open before the run
- The parallel pipeline wave's files (`gtk-theme-contract`,
`lock-screen-theme-contract`, `palette-contract`, the two bridge tests) are
not counted above. If that wave adds contracts, the README count line and the
count in this file both need bumping again before the suite runs.
- `AppearancePage.qml`'s `tab` property defaults to `"background"` while its
own comment says Themes leads. Decide which is intended before the run;
nothing currently pins it either way.
## Phase 4 (Shell category) — append below
Spec: `2026-08-24-shell-category-redesign.md`. Desktop & Dock became **Shell**
(Bar · Dock · Control Center · Tiling · Workspaces), System gained **Sync &
Backup**, and the dock got its feature wave.
Unlike phases 2 and 3, the static and stubbed contracts in this wave **were
run** as they were written, and every one of them passed in isolation against
the tree. Two things are still deferred. The **live-harness halves** were not
run — the session was locked, and `dock-position-contract` opens a probe shell
while `settings-pages-contract` starts a settings harness, so those ran static-
only (`PANAMA_SETTINGS_STATIC_ONLY=1`) or not at all. And the **full-suite
pass**, the only thing that catches contention between harnesses, happens next
with Gabriel driving.
### New contracts (1)
| Contract | What it pins |
|---|---|
| `quickshell/bar-visibility-contract` | The bar's own neutral family: `barFg`/`barFgDim`/`barFgMuted` exist, each forced tone is anchored on one literal with both dims mixed off it, and the `theme` branch returns `root.fg`/`fgDim`/`fgMuted` **by identity** rather than a copied colour. All thirteen bar-text files bind to those tokens and none paints neutral text with the `fg` family (`Theme.alpha(Theme.fg, …)` hover and separator fills are allowed; semantic tones were never in scope). `Bar.qml`'s scrim reads `visible: Settings.barBackdrop`, its shadow reads `layer.enabled: Settings.barTextShadow` over exactly one `MultiEffect` layer, and neither may be a literal `true` — both shipped hardcoded during the build, which is the regression this exists for. Plus the four widget gates ANDed with their state conditions, `VitalsWidget`'s whole-pill `visible`, `exclusiveZone: Theme.barHeight` still literal, no animation anywhere in `Bar.qml`, and the two right-click jumps landing on `bar`. |
Mutation-checked while writing: hardcoding the shadow, returning a literal from
the `theme` branch, putting one widget back on `Theme.fg`, deleting the vitals
pill's own `visible`, and adding a `Behavior` to `Bar.qml` each fail it with a
message naming the actual problem.
### Updated contracts (5)
| Contract | What it now pins |
|---|---|
| `quickshell/dock-position-contract` | `DockPinsEditor``DockPinsStrip`: `preventStealing` moved to the strip, the retired ↑/↓ buttons replaced by the strip's own keyboard path (Left/Right move, Delete unpins), and a new section for the **live** dock's drag-to-reorder — commits once on release, and measures a slot from a real icon rather than a constant, because a `DockItem` is taller than it is wide and a constant is wrong on one orientation. |
| `quickshell/settings-jump-contract` | `DockContextMenu`'s "Dock settings" now opens `dock`, not the retired `desktop`. |
| `quickshell/settings-pages-contract` | Six new pages added to the page sweep (Bar, Dock, ControlCenter, Tiling, Workspaces, Sync); `vitalsIntervalMs` and the graphics ChoiceGrid now required on `BarPage` rather than `AppearancePage`, since the vitals are bar content and not surface appearance. |
| `setup/projects-contract` | The saved-projects list moved from the deleted `DesktopPage.qml` to `WorkspacesPage.qml`. |
| `quickshell/panama-commands-contract` | The launcher's new `dock-add-app``panama-action dock-pin` command. |
### Verified against the new tree, no edit needed
Every one of these was **run** and passed after the phase-4 changes landed:
- `quickshell/settings-search-contract` — the new `bar`/`controlCenter`/
`datetime` groups and the re-pointed `dock`/`multitasking`/`edges`/`master`/
`notices`/`focus` routes are covered by the schema-label sweep already.
- `quickshell/settings-ownership-contract` — the six intentional mirrors and the
eleven README literals survived the rewrite; no new mirror was introduced.
- `quickshell/settings-nav-contract` — 14 categories, 38 leaves, 2 retired ids
(`home-phone`, `desktop`).
- `quickshell/manual-contract`, `gnome-handoff-contract` (15 handoffs against
38 pages), `control-center-contract`, `welcome-contract`,
`accent-controls-contract`, `theme-catalog-contract`,
`desktop-style-contract` — checked, nothing stale.
`welcome-contract` passed in isolation again here, which does not settle the
phase-3 flake above: that failure only appeared under a storming full-suite run.
### Docs updated in the same wave
- `docs/settings.md` and the 37 `settings-*` launcher commands — regenerated by
`panama-settings-docs` and `panama-settings-commands`; both `--check` modes
clean and both generators verified idempotent. `settings-desktop` is deleted,
`settings-bar`/`-dock`/`-control-center`/`-tiling`/`-workspaces`/`-sync` are
new, and `dock-add-app` joins them.
- `modules/settings/README.md` — new **Shell** section (the five tabs, the bar
token design and why it exists, the pins strip and the live dock's gestures,
the Control Center rule that only real sections get toggles) and a new
**System Sync & Backup** section. Appearance corrected from six tabs to
five, saying where the Shell tab went.
- `manual/05-making-it-yours.md` — new **Shell** chapter section (bar
legibility, widget switches, why `use24Hour` is not there, the pins strip,
the dock's drag/right-click/scroll/preview gestures, **Add App to Dock**,
Control Center sections) plus **Carrying settings between machines**.
`manual/03-windows-and-workspaces.md` — the multi-display workspace switch
now points at Displays, which is where it actually is, instead of the deleted
Desktop & Dock page.
- Top-level `README.md` — contract count 165 → 166, recounted the way
`panama test` collects (executable, or `*_test.py`, excluding fixtures and
`__pycache__`).
### Still open before the run
- Four contracts in the working tree changed for reasons **outside** this
phase and are not accounted for above: `declared-assets-contract` (`pkill`,
from the video wallpaper work), `declared-dependencies-contract` (`cmp`
diffutils), `panama-doctor-contract` (29 → 30 check ids), and
`lock-screen-helper-contract` / `video-wallpaper-contract` (theme-derived
literals and the mpv IPC key quoting). Confirm each belongs to a wave that
intended it before the suite runs.
- `tests/setup/update-command-contract` is untracked and belongs to the
separate `panama update` design, not to this redesign. It **is** inside the
166.
- `quickshell/dock-position-contract` has never had its **live** half run since
the strip landed: it opens a probe shell, and this wave ran under a locked
session. Its static half is what was verified. Run it first in the sweep.
- `quickshell/settings-pages-contract` passed static-only for the same reason.
Its compositor-integration half — the settings harness, its IPC, and the
page-open checks — is unverified against the six new pages.
- `Bar.qml` has one stray indentation glitch at its first `Row` (line 105).
Cosmetic, untouched here because the file is not this wave's to reformat.
- 2026-08-24 full-suite run: 166 contracts, all green except `displays-contract`
and `switcher-contract`, which are live interactive tests that cannot run
behind hyprlock (both passed in the same day's unlocked run; neither
subsystem changed in phase 4). Re-verify after unlock. **Still pending**
and `displays-contract` has since changed (phase 5), so this run is now the
first one that exercises the new material as well.
## Phase 5 (Displays) — append below
Spec: `2026-08-24-displays-redesign.md`. The Displays page became canvas-first,
the per-monitor record grew VRR / colour profile / bit depth / SDR trim /
mirroring, and `pushLayout` stopped clobbering `monitors.lua`'s colour values.
**Nothing in this wave was run.** The test window was closed while it was
written: these are live harnesses that drive the real compositor through
display transactions, and three agents were editing the tree concurrently. What
*was* verified is listed as static below — evaluated directly against the
implementation files without a harness, by loading `monitors.lua` under a stub
`prefs` and by evaluating `DisplayLayout.js` in node. Everything else is
deferred to the sweep.
### Contracts changed (4)
| Contract | What it now pins | Verified |
|---|---|---|
| `quickshell/display-transaction-contract` | The extended record end to end: `applyRecord` merging one field at a time; `vrr` **omitted** when `vrrMode === -1` and emitted when it is not; neutral `sdrsaturation` omitted rather than written; a mirrored rule asking for `position = "auto"`; the mirror x/y carve-out in `matchesLayout`, and that it does not leak to an unmirrored record; a framebuffer format with no 8/10 mapping skipping the bit-depth assertion instead of blocking Keep; `confirm()` persisting the whole record; old-shape stored blobs still validating and an impossible `vrrMode` not; and seven refusals (self-mirror, primary mirroring, absent target, out-of-range vrr/profile/depth/SDR). | Bash and Python syntax; every new static grep checked against the landed `Displays.qml` (including the `vrrMode >= 0` omission branch in `monitorRule`) and against the extended harness; the fake compositor's rule parser dry-run on a real `monitorRule` payload, covering the `position = "auto"` and `mirror =` branches and the `mirrorOf: "none"` readback spelling. Every IPC assertion is **deferred**. |
| `quickshell/display-arrangement-contract` | **Flipped**: the canvas is no longer hidden below two displays. The `visible: … monitors.length` gate is now asserted *absent* from the `DisplayArrangement` element (by AST-free block scan, so the selector chips and Workspaces card may keep theirs), with a solo hint string, a `draggable` flag, and an `enabled:` binding on the `DragHandler`. Plus the mirror badge: `Mirrors ` in the component, `mirrorOf` read off the rects, and two new harness fixtures — solo renders one rect with `draggable == false`, mirrored stacks rect 1 on rect 0 and flags it. | **Statically verified** against the rebuilt `DisplayArrangement.qml`: every grep hits, the `DragHandler`'s `enabled:` is found by the brace-matching helper, and the flip check was mutation-tested both ways — it fails on the pre-redesign page (where the gate sat on the enclosing card, not on the canvas) and passes once the gate is gone. The two new harness fixtures are **deferred**. |
| `quickshell/display-layout-contract` | Mirror geometry in `DisplayLayout.js`: a valid mirror validates; the mirrored record keeps its stored coordinates through `normalize` (the primary's anchor does not apply to a position nothing reads back); it contributes nothing to `bounds`; `canvasRects` stacks its rect on its target's and carries `mirrorOf`/`mirrored`. Five refusals: self, absent target, mirroring primary, a two-hop chain, and a non-string. | **Statically verified** in node against the real `DisplayLayout.js` — every expected value in the two new `jq` filters came from that run, including the 3140/80 the mirrored record keeps. Harness plumbing deferred. |
| `quickshell/displays-contract` | The Lua consumer half: `color_profile` / `bitdepth_value` / `vrr_value` / `sdr_value` / `mirror_value` present, `cm`/`bitdepth`/`sdrbrightness`/`vrr`/`mirror` emitted under Hyprland's own key names, a mirrored entry's `position` forced to `auto`, neutral SDR saturation and `vrrMode = -1` written as absence rather than as a value, and an entry with every new field impossible surviving with its geometry while each bad field drops. Plus the extended-record greps on `Displays.qml`. | **Statically verified**: the whole `LUA` block was run against `config/dot/hypr/monitors.lua` with a stub `prefs` and passes. The `Displays.qml` greps were checked by hand and all hit. The live compositor half is deferred. |
### Cross-agent shapes these contracts now pin
Written from the spec's pinned API while agents A and B worked in parallel, and
re-checked against their files as those landed:
- `DisplayArrangement.canvasSnapshot()` exposes `draggable` alongside
`rects`/`scale`, and passes `canvasRects`' `mirrorOf`/`mirrored` through — it
returns `canvasData.rects`, not the solo-shrunk `tiles`, which is what the
new fixtures assert against. Confirmed in the landed component.
- The solo hint is pinned as the prefix `One display connected` rather than the
full sentence, so the em dash cannot break the grep.
- The mirror badge is pinned as `Mirrors ` in `DisplayArrangement.qml`.
- `soloFixture`/`mirrorFixture` mutate `fixtureService.monitors` and call
`resetDraft()`, which is what the component's own `onMonitorsChanged` does.
Whether that ordering settles before `canvasSnapshot()` reads back is the one
thing only a run can answer.
### Still open before the run
- ~~`PreferenceSchema.qml`'s stale `displays` detail string~~ — resolved: the
detail now names color, VRR override, and mirroring, and the matching grep in
`displays-contract` was updated in the same commit.
- `displays-contract` also greps `DisplaysPage.qml` for `selectedOutput`,
`scalesForMode(`, `primaryFirstMonitors.map(` and `enabled: Displays.canConfirm`.
All four still hit, but the page was still the pre-redesign one when this was
written — re-check them once the rebuilt page lands, particularly
`primaryFirstMonitors.map(`, since the spec replaces the "Connected display"
picker card with selector chips.
- No contract file was added or removed, so the README count line stays at
**166** and `setup/readme-contract` needs nothing.
- Run order for the sweep: `display-layout-contract` first (pure geometry, no
compositor), then `display-transaction-contract` (fake compositor on `PATH`),
then `display-arrangement-contract`, and `displays-contract` last — it is the
only one that drives the physical display, and it refuses to start from a
scale that does not match what `monitors.lua` ships.
## Phase 6 (Sound) — append below
Spec: `2026-08-24-sound-redesign.md`. The Sound page became a complete PipeWire
surface: honest device list with badges and a ghost row, per-channel speaker
test, microphone test, per-app mic mute and per-app output routing, alert-sound
theme, over-amplification, and native device profiles in place of the GNOME
handoff.
**Nothing in this wave was run.** Three agents were editing the tree
concurrently and every harness here drives the live audio graph — the page
harness constructs the real Sound page against the session's own PipeWire, and
the services harness starts a shell. What *was* verified is listed as static
below: bash syntax on every contract, the JSON and pw-metadata fixtures parsed,
and each service's own parsing logic replayed in node or Python against the
landed source so the expected values in the assertions are the values the code
actually produces.
### New contracts (3)
| Contract | What it pins | Verified |
|---|---|---|
| `quickshell/sound-cards-contract` | `SoundCards`' parse of `pactl -f json list cards`: profiles keyed by name becoming an ordered list sorted by pactl priority (the fixture deliberately lists them out of order, so insertion order fails), a profile pactl marked unavailable kept and flagged rather than dropped, `device.description` as the card name, the port hint naming the connected port and reading exactly `No port connected` when none is, `set-card-profile` argv, an empty card or profile name starting no write, unparseable output and a failing read both degrading into `lastError` with an array still in `cards`, and recovery on the next refresh. Plus the static grep on the live `-f json` command, which the fixture seam means nothing else exercises. | **Statically verified**: `SoundCards.parse` and `portHintFor` replayed in Python against the fixture — profile order, availability flags, descriptions and both port hints match the assertions exactly. Every IPC call is **deferred**. |
| `quickshell/sound-routing-contract` | `SoundRouting`: every stream of a group moved, by `object.serial` and not by node id (both are plausible numbers in a log); a serial-less stream falling back to its node id; an empty group or unnamed sink running nothing; and `routeToDefault` releasing the pin through `pw-metadata -n default -d <node-id> target.object` **and** the legacy `target.node`, never through `move-sink-input` — moving a stream to the current default pins it there, which is the bug the button undoes. Plus the id asymmetry both ways, `busy`/`lastError` on refusal, recovery, and the spec-required comment above `routeToDefault`. | **Statically verified** against the landed service: the argv shapes, the serial fallback, the two metadata keys and the early returns all read directly off `SoundRouting.qml`. The runs are **deferred**. |
| `quickshell/sound-defaults-contract` | The one distinction the ghost row depends on: `default.configured.audio.sink` and not `default.audio.sink`. The fixture sets them to different values and writes the configured one first, so neither "last key wins" nor a machine whose configured device is present can make it pass by accident. Also: an unconfigured session reading as `""` rather than as the effective device, an empty store, a non-JSON value on an unrelated key not taking the read down with it, a failed read leaving nothing invented, recovery, the ghost record and its label (Bluetooth address, AirPlay hostname, USB product string, empty), and a static ban on reading `preferredDefaultAudioSink`, which is null in exactly the case the service exists for. | **Statically verified**: `SoundDefaults.parse` and `label` evaluated in node against all five fixtures — every expected string in the contract came from that run. The IPC half is **deferred**. |
### Updated contracts (4)
| Contract | What it now pins | Verified |
|---|---|---|
| `quickshell/sound-page-contract` | Rebuilt around the new page. Kept: the two `SoundDeviceList`s, the Dictation negatives and handoff, the balance and device-row pins, the Quick Settings sharing. Dropped: `openGnomePanel("sound")` and `label: "Device profiles"` — both now asserted **absent**, replaced by the native profile card (`title:`, `SoundCards.cards`, `setProfile(`, `refresh()`, `lastError`). Added: `captureApplications` filtered by `AudioInStream` and reaching `SoundCaptureRow` with a per-app mute, a tracked `PwObjectTracker` over capture nodes, the row hiding itself when nothing is listening; the ghost row asked of `SoundDefaults.absent(root.output)`, rendering after the `Repeater`, and non-interactive by a brace-scan proving every `TapHandler`/`HoverHandler` carries `!root.ghost`; the `device.api` badges; over-amplification gated at 1.5 on the page and on Quick Settings' **output** slider only, with the >100% region marked; the four `SoundRouting` calls in `ApplicationVolumeRow`. The no-shell-out ban is unchanged on the four core files and now extended over all eleven page components. | **The whole static half was run** against the landed tree and passes. The harness half is **deferred**. |
| `quickshell/application-volume-contract` | Grouping pins unchanged; new `clampVolume` case for the `max` parameter — `setVolume(group, 1.4, 1.5)` lands 1.4 and unmutes, 2.5 clamps to 1.5, **no** max clamps to 1 (the default must not quietly follow the preference), a negative clamps to 0, and a non-numeric value changes nothing and returns false. | **Statically verified**: the landed `AudioStreams.js` evaluated in node produced byte-identical output to the `jq` filter's expectations. The IPC run is **deferred**. |
| `quickshell/osd-helper-contract` | Over-amplification and the blip, both out of `settings.json`. Every run now gets its own `XDG_CONFIG_HOME`, because the helper would otherwise read Gabriel's real preferences and pass or fail on which switches he has on. Pins `-l 1.5` on volume up **and down** (wpctl clamps the result, so coming down from 130% would snap to 100% without it), `-l 1` on the microphone in the same run, `-l 1` for off / key absent / file absent / malformed JSON, the blip on up/down/toggle and not on brightness or microphone, silence with `volumeChangeBlip` false, and silence with no sound file — with the OSD still shown in every degraded case. | **Bash syntax only.** The helper is agent A's and had not landed when this was written; see the open item below. |
### Cross-agent shapes these contracts pin
Written from the spec's pinned API while agents A and B worked in parallel, then
re-checked against their files as those landed:
- `PANAMA_SOUND_CARDS_FIXTURE` and `PANAMA_SOUND_DEFAULTS_FIXTURE` are **file
paths** the service `cat`s in place of the live command, read once at
singleton construction. So a contract changes what the file *says* between
cases rather than where it points, and deletes it to make a read fail.
Confirmed against both landed services.
- `sound-services-harness.qml` is new and shared by all three service
contracts. Whichever contract is running sets an inert fixture for the two
services it is not testing, so nothing reaches the live daemon and every line
in a command log belongs to the service under test.
- `SoundDeviceList` asks `SoundDefaults.absent(output)` rather than comparing
configured names itself — the service owns both the comparison and the label.
The contract followed B's refactor to that shape.
- The ghost label is A's `SoundDefaults.label()`, so it reads
`Bluetooth device (AA:BB:CC:11:22:33)` **with** parentheses. `SoundDeviceList`
briefly had its own `tidyName()` producing the same string without them; the
harness and the assertion track the service's version.
### Docs updated in the same wave
- `services/SettingsSearch.qml` — the seven hand-written Sound entries from the
spec (Balance, Speaker test, Microphone test, Alert sound, Applications using
the microphone, Move an application's audio, Device profiles) on top of the
four that were already there, and `groupPages` gained `"sound": "sound"` so
the new schema group self-indexes.
- Top-level `README.md` — contract count 166 → 169, recounted with the same
`find` `setup/readme-contract` uses; that contract was run and passes.
### Still open before the run
- **`osd-helper-contract` is written against a `panama-osd` that had not
landed.** It assumes two things of agent A's helper: that the settings file
is resolved as `${XDG_CONFIG_HOME:-$HOME/.config}/panama/settings.json` (the
`panama-idle` / `panama-lid` spelling, not `panama-palette`'s
`PANAMA_SETTINGS` override), and that the blip's sound file can be overridden
with `PANAMA_OSD_BLIP_SOUND` (matching the existing `PANAMA_OSD_*` seams in
the same script), which is the only way to exercise the missing-file branch
deterministically. If A spelled either differently, the env names in
`run_helper` are the only lines that need changing. Reconcile before running.
- The contract also now asserts `-l` on the **down** step, which the
pre-redesign helper did not pass. The reasoning is in the contract; if the
landed helper only limits the up step, that is a real bug at 150% and not a
contract to relax.
- `sound-page-contract`'s runtime half asserts `.captureApplications >= 0` and
`.captureRowVisible == (.captureApplications > 0)` — true on a machine where
nothing is recording, which is the ordinary case. Getting a positive capture
count under test would mean holding the microphone open from the contract;
the grouping itself is covered by `captureTypesValid` and by the static greps.
- No contract in this wave has had its harness started. Run order for the
sweep: `sound-defaults-contract`, `sound-cards-contract`,
`sound-routing-contract` (all three fixture-fed and cheap), then
`application-volume-contract`, then `osd-helper-contract`, and
`sound-page-contract` last — it is the only one that constructs the real page
against the session's own audio graph.
- Nothing in this wave plays a sound on purpose, but `sound-page-contract`
constructs `SoundPage`, whose microphone test and channel strip are one
IPC-less click away from `pw-play`. The harness exposes no method that
triggers either; keep it that way.
## Stabilization pass — 2026-08-24 — THE RUN HAPPENED
Three read-only reviewers swept the whole redesign (37 findings), three fix
waves applied them all, and the full suite then ran three times with Gabriel's
go-ahead: 162/169, 168/169, then **169/169 green**. The seven interim failures
were four suite-contention flakes (pass solo; theme-profiles hardened with a
catalog-settle wait) and three contract bugs (display-layout's stale mirror
expectation, osd-helper's non-atomic stub log + a set -u `local` expansion
trap, sound-cards' jq context rebind). displays-contract and switcher-contract
— the locked-session holdovers — both pass unlocked. Later phases append new
deferred contracts below as before; this line is the baseline they diverge
from.