feat(bar): AIO pump + case fan RPM pill, and the staged sudo script that keeps its driver - #1411
Merged
Conversation
…hat keeps its driver Two halves of one thing: the workbench's Nuvoton NCT6687D Super I/O is the only source of the AIO pump and case-fan tachos, and nothing loads its driver automatically — it has been live only because a session ran `modprobe nct6683` by hand in August. `nix/system/apply-nct6683-module.sh` (STAGED, not applied — needs sudo) adds "nct6683" to boot.kernelModules. It matches the ASSIGNMENT, never a line number, refuses when there is not exactly one single-line list, backs up, and verifies by reading /etc/modules-load.d/nixos.conf. 🔴 Deliberately NOT `lsmod`: the module is already loaded by hand, so lsmod says yes whether or not the change landed. The rendered modules-load.d file is what systemd replays at boot, so its content IS the persistence claim. `scripts/i3status-fans` + `fansBlock` render it: `2660·1736` Idle · `!0·1736` Critical · `2660·?` Warning · `?` Warning Three decisions worth naming, each pinned by a test: - ALWAYS VISIBLE, against the bar's hide-at-zero house style. A count's quiet state means "nothing to do"; a pump RPM is the number itself, and a cooler pill invisible while healthy is invisible in exactly the state it certifies. - The floor is PER-FAN and OPTIONAL, and only the pump gets one. fan2/fan4-10 read a permanent 0 (nothing plugged in) and a case fan on a zero-RPM PWM curve legitimately stops when idle — a blanket floor would make the pill cry wolf. - An ALARM OUTRANKS an unreadable sibling. An outage may make a reading less trusted; it may never make a recorded alarm quieter (the bar's house rule). The chip is located by its hwmon `name`, never by number: it sat on hwmon11 behind ten nvme/spd5118/k10temp/amdgpu devices whose probe order is not guaranteed, and two of the fixtures' decoys carry a fan1_input of their own. icon = `refresh`: material-nf has NO `fan` key (measured against the 0.36.1 set), and an unknown key renders the whole pill as a red "Failed to render full text". Added to _KNOWN_GOOD_ICONS, whose comment said "these 4" over a set of 5. Verified: 50 new tests + 529 in test_bar_status.py green; three mutants killed, each by its own named guard and its own assertion (ordering swap -> only test_render_an_ALARM_OUTRANKS_an_unreadable_sibling, 'Warning' == 'Critical'; chip-by-number; hide-at-zero -> test_this_pill_is_ALWAYS_VISIBLE). All four render states exercised against real and fake sysfs, then home-manager switch + i3-msg restart, and the pill screenshotted live on the bar as `2810·1749`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019LDvGed1jgLsz2UXSZNTdm Claude-Session-Id: fcc01bfd-a65d-4421-b048-8c23f99a003e
… live, rank 1 is staged as a su Claude-Session-Id: fcc01bfd-a65d-4421-b048-8c23f99a003e
Claude-Session-Id: fcc01bfd-a65d-4421-b048-8c23f99a003e
…larm, and three guards that could not see their own hazard Round 1 of /audit-pr on #1411. Nine findings; the four that mattered were all "the guard cannot observe the thing it names". 🔴 F1 payload — `parse_fan` accepted `:0`, which can NEVER fire. `read_rpm` already maps every negative reading to None, so `rpm < 0` is unreachable: a floor of exactly 0 parsed cleanly, looked configured, and left a stopped pump rendering `0` in neutral Idle forever. The rejection message directly above it already described this failure ("a tacho never reads below 0") — the predicate was `< 0` and was off by one. Now `<= 0`. Boundary measured both ways: `:0.5` still alarms at 0 rpm, `:0` is refused. 🔴 F2 scaffolding — NOTHING fed nix's `command =` literal to `parse_fan`, so a one-character typo shipped a broken pill with a green suite. MEASURED, mutating only that string: `--fan pump=1:` -> 529/529 green while the live pill is a permanent `?` with the pump turning at 2660 RPM; `--fan pump=1:0` -> 529/529 green with the alarm inert. Now pinned by STATE (at least one fan must be armed and must go Critical at 0 rpm), not by spelling, so re-ordering is free and disarming is not. Precedent was already in the same file: loadBlock's threshold. 🔴 F3 scaffolding — the block/script gate guard compared script gates only for UNGATED blocks. MEASURED: flipping fansBlock's `home.file` to `lib.mkIf isLaptop` — workbench renders the block, laptop gets the script — left 529/529 green while graphical.nix carried a 🔴 comment asserting that pairing was protected. Now compares polarity for gated blocks too. ⚠ Deliberately NOT flagging a script deployed MORE widely than its block: the first version did, and reddened on rigcontrolBlock/claudeRunsBlock, both long-standing and both harmless (a spare symlink). A guard that reddens on main's correct config is worse than no guard. 🔴 F4 payload — a crash rendered the SAME `?` that this pill DEFINES as "the chip is absent, nct6683 is not loaded", with no diagnosis anywhere. Any future defect would have impersonated a missing driver and sent the operator to the sudo script. `traceback.print_exc()` before the fallback; stdout is untouched, so i3status-rust cannot be confused. Paired with a control asserting the ORDINARY chip-absent path stays SILENT, or the traceback stops being a signal. 🟡 F6 payload — the sudo script had no test seam, no tests and no host guard, while its sibling apply-nebula-relay.sh has all three. Adds NCT_CFG/NCT_LOADCONF/ NCT_HWMON_ROOT seams, a host guard (refuses a machine with no nct6687/nct6683 hwmon device; NCT_SKIP_HOST_CHECK=1 overrides), a writability check (the old message said "run under sudo" while only proving READ), and 30 hermetic tests. 🔴 The host guard's FIRST version was unobservable: it skipped itself whenever NCT_CFG was set, so on a host WITH the chip, DELETING THE WHOLE GUARD left the suite 26/26 green. Caught by mutation, not by review. NCT_HWMON_ROOT makes both branches reachable on any machine — that decoupling is the actual fix. 🟢 F7 — `NCT_DRY_RUN=0` turned dry-run ON (`[ -n "$DRY" ]`). Fails safe, so it would have read as "the script did nothing again", not as a bug. 0/false/no now mean off. 🟢 F8 — retracted a FALSE comment claiming the chip name "is pasted into a glob". It is not: find_chip globs the literal "hwmon*" and compares with `==`. MEASURED with the guard removed — `*`, `../../etc`, `nct6687/../x`, `''`, `..` all render the identical `?`, against a positive control showing the harness could see a difference. The guard is kept as defence-in-depth; the sink claim is gone from both sites (source + the test docstring that echoed it). 🟢 F5 — raised _EXPECTED_SCRIPT_BLOCKS 12->13 and _EXPECTED_BLOCK_DEFS 21->22 per the file's own instruction. NOT cosmetic: at 21, reintroducing the `^ };` terminator regression yields exactly 21, so the assertion credited with catching it passed and the suite died elsewhere naming the wrong block. Now it dies at the named detector. 🟢 F10/F11 — handoff doc: rank 1 is DONE (operator ran it 14:18:33; verified via /etc/modules-load.d/nixos.conf, not lsmod) and the pill is NOT currently deployed (a later switch from a PR-less checkout removed it — ship.sh after merge). Removed a stale base sha a reader would have gated the BRANCH against. bar/SKILL.md's block table and host-split now carry the pill (no ceiling test covers that file, so no eviction is owed). Not fixed, deliberately: F9 (FanSpec.label is parsed and validated but never rendered — it is command-line documentation; making it visible is a UX choice, not a defect). Verified: 644 tests green (30 new for the sudo script, 62 for the block, 552 in test_bar_status.py). Mutation battery this round: 12 mutants, 12 killed, control green either side, PYTHONDONTWRITEBYTECODE=1 with __pycache__ cleared between mutants — each killed by its OWN named guard. One survivor found and fixed at the mechanism (the host guard above). 🔴 Two harness defects caught and corrected mid-battery, both of which would have scored a false result: `grep -qF "--fan …"` parsed the mutant as an option flag and reported NOT APPLIED for three real mutations, and an earlier assertion fired on the COMMENT explaining why lsmod is not used rather than on code. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019LDvGed1jgLsz2UXSZNTdm Claude-Session-Id: fcc01bfd-a65d-4421-b048-8c23f99a003e
Claude-Session-Id: fcc01bfd-a65d-4421-b048-8c23f99a003e
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes the two ranked items in
claudedocs/handoff-nct6683-bar-integration.md.The workbench's Nuvoton NCT6687D Super I/O is the only source of the AIO pump and case-fan tachos, and nothing loads its driver automatically — it has been live only because a session ran
modprobe nct6683by hand in August.Two halves
nix/system/apply-nct6683-module.sh— STAGED, not applied. Adds"nct6683"toboot.kernelModules. Needs sudo, so it follows the repo convention (nix/system/apply-*.sh) rather than being run from an agent:It matches the assignment, never a line number; refuses unless there is exactly one single-line
boot.kernelModuleslist; backs up; and verifies.🔴 It verifies against
/etc/modules-load.d/nixos.conf, deliberately NOTlsmod. The module is already loaded by hand, solsmodsays yes whether or not the change landed — it cannot distinguish "persisted" from "a human modprobe'd it in August". Measured before the change:nct6683absent from that file while live inlsmod. That file is what systemd replays at boot, so its content is the persistence claim.scripts/i3status-fans+fansBlockrender it, workbench-only, gated!isLaptopin both theblockslist and thehome.file:2660·1736!0·17362660·??nct6683not loadedThree decisions, each pinned by a test
test_this_pill_is_ALWAYS_VISIBLEis parameterized over every state, so a future port of the hide-at-zero idiom reddens the suite.fan2/fan4–fan10read a permanent 0 (nothing plugged in) and a case fan on a zero-RPM PWM curve legitimately stops when idle; a blanket floor would make the pill cry wolf. A fan passed without:MINRPMis display-only and can never raise Critical.!0·?stays Critical) — the bar's existing rule that an outage may make a reading less trusted, never a recorded alarm quieter.Other notes: the chip is located by its hwmon
name, never by number (it sat onhwmon11behind ten unrelated devices; two test fixtures carry decoyfan1_inputs so a by-number lookup fails the suite).icon = refreshbecause material-nf has nofankey — measured against the 0.36.1 set — and an unknown key renders the whole pill as a redFailed to render full text.Verification
Live:
home-manager switch+i3-msg restart, pill screenshotted on the real bar as2810·1749. All four render states driven against real and fake sysfs.Mutation: three mutants, isolated copy,
PYTHONDONTWRITEBYTECODE=1, control green either side — each killed by its own named guard with its own assertion. The ordering swap killed onlytest_render_an_ALARM_OUTRANKS_an_unreadable_sibling(assert 'Warning' == 'Critical'), the other 49 passing.Gate — merged tree at base
01956bf0, both tiers:gate.sh --tier bothchecks.nodetestschecks.pytestsmain, not this PR🔴 The sandbox pytests red is
main's. Control, same derivation built fromorigin/mainalone with this change absent:origin/main(control)+50 collected, +50 passed, +0 failed. All 20 are
test_nebula_relay_apply.py(arrived with #1272), failing on/usr/bin/env: bad interpreter— a condition that exists only in the nix sandbox, which is why both dev-host runs are green. Files this PR neither touches nor can reach;scripts/i3status-fansappears in that log exactly once, as a routinepatchShebangsrewrite.⚠
mainhas moved twice more since01956bf0(#1401 and others) — none sharing a file with this diff, andscripts/run-tests.sh(where the per-target floors live) untouched, so the floors gated against still hold.🤖 Generated with Claude Code
https://claude.ai/code/session_019LDvGed1jgLsz2UXSZNTdm