fix(nixos): two toggles that reported success and did nothing — wire the state bridges
All checks were successful
Check / eval (push) Successful in 3m16s

BACKLOG #116. `nomarchy.settings` is declared in exactly one place —
modules/home/options.nix:412, the Home Manager side. On NixOS the attribute
does not exist, and `or <fallback>` swallows the missing-attribute error, so
four options that "defaulted from the state" had silently been their fallback
on every machine ever built.

The item said three options, and called them benign. Both halves were wrong,
and re-grepping rather than trusting the account is what found it:

  * There were four. The original enumeration read options.nix instead of
    modules/nixos/ and missed services.nix's printing.enable — the same
    mistake in miniature as the bug it was filing.
  * Two were live user-facing bugs. Control Center is shipped
    (default.nix:337) and reachable from the menu; its Bluetooth and Printing
    toggles wrote settings.{bluetooth,printing}.enable and printed "requires
    rebuild", and the rebuild changed nothing. They had never worked.

The fix is one shape, now uniform: the option declares a STATIC default, and
the implementing module reads the state via theme-state-read.nix (fails closed
on bad JSON, unlike greeter.nix's raw fromJSON — also moved onto the reader
here) and mkDefaults it behind `mkIf (state != null)`. An absent key leaves the
option default as the single source of the fallback; a hand-set system.nix
value still pins it. batteryChargeLimit gets no bridge and loses its dead read:
power.nix's oneshot already reads that key with jq at RUNTIME and prefers it
over the baked value, which is why that menu worked all along.

V2. The bug is proved real before/after on the same flipped state: BEFORE,
bluetooth stays true and printing stays false; AFTER, both flip, and a hand-set
value still outranks the state. Nothing in a build fails when a bridge dies, so
the guards are the point — checks.state-bridges asserts 11 eval cases, and
checks.printing-from-state boots a VM whose only input is the state file and
waits for a running cups.service. The guard was itself proved to fail:
re-breaking the bluetooth bridge makes it throw, naming both assertions. A
check that passes whether or not the property holds is worse than no check
(625b7e3). flake check, option-docs, template-sot, downstream-template-*,
installer-safety, hardware-toggles and battery-charge-limit all pass.

No V3: the mechanism is fully proved headlessly. Design record in ROADMAP §
NixOS-side state bridges (#116); new #117 (PROPOSED) for the control-center
toggles still leaving the rebuild to the user.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-14 16:03:12 +01:00
parent 7353568115
commit a9f3a642ee
8 changed files with 268 additions and 46 deletions

View File

@@ -318,6 +318,37 @@ Design/decision records and a running log of shipped work (items marked
generic apply path is exercised by every manual theme change). The only
on-hardware residual is the live user timer firing on its `OnCalendar`
schedule (the schedule is `systemd-analyze`-validated).
- ✓ **NixOS-side state bridges (#116):** `nomarchy.settings` is declared in
exactly one place — `modules/home/options.nix`, the **Home Manager** side.
On the NixOS side the attribute does not exist, so four options that
defaulted from `config.nomarchy.settings.… or <fallback>` silently got the
fallback on every machine ever built: `or` swallows the missing-attribute
error, which makes a dead bridge and a live one look identical in the
source. Two of them were **user-facing toggles that reported success and
did nothing** — Control Center's Bluetooth and Printing wrote
`settings.{bluetooth,printing}.enable` and printed "requires rebuild", and
the rebuild changed nothing, for as long as those toggles had existed.
**The pattern, now uniform:** the option declares a *static* default and
the implementing module reads the state via `modules/theme-state-read.nix`
(fails closed on bad JSON) and `mkDefault`s it —
`lib.mkIf (stateX != null) (lib.mkDefault stateX)`, so an absent key leaves
the option default as the single source of the fallback and a hand-set
value in `system.nix` still pins it. Done for `greeter.autoLogin`
(eb38008), `bluetooth.enable` (`modules/nixos/default.nix`) and
`services.printing.enable` (`modules/nixos/services.nix`).
`power.batteryChargeLimit` deliberately has **no** eval bridge: `power.nix`'s
oneshot reads the key out of the live state with `jq` at *runtime* and
prefers it over the baked value, which is why its menu worked all along —
the phantom read was dead but harmless, and is simply gone.
**The guard is the point:** nothing in a build fails when a bridge dies, so
`checks.state-bridges` asserts at eval that a flipped state file reaches
the config (and that a hand-set value still outranks it), and
`checks.printing-from-state` boots a VM whose *only* input is the state
file and waits for a running `cups.service`. Adding a bridge = copy the
shape and add a case. **Lesson:** `or <fallback>` on a config read is an
error-swallower — prefer an explicit `!= null` test over a fallback
expression, and grep the whole of `modules/nixos/`, not just `options.nix`
(the first pass at this item did the latter and undercounted).
- **Nicer shell out of the box:** ✓ zsh is the default login shell, with a
starship prompt themed from the JSON, autosuggestions + syntax
highlighting, and modern-CLI ergonomics — `cat`→bat (theme "ansi", so it