Skip to content

feat(bar): AIO pump + case fan RPM pill, and the staged sudo script that keeps its driver - #1411

Merged
ZacxDev merged 5 commits into
mainfrom
feat/nct6683-fans-bar
Sep 9, 2026
Merged

feat(bar): AIO pump + case fan RPM pill, and the staged sudo script that keeps its driver#1411
ZacxDev merged 5 commits into
mainfrom
feat/nct6683-fans-bar

Conversation

@ZacxDev

@ZacxDev ZacxDev commented Sep 8, 2026

Copy link
Copy Markdown
Member

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 nct6683 by hand in August.

Two halves

nix/system/apply-nct6683-module.sh — STAGED, not applied. Adds "nct6683" to boot.kernelModules. Needs sudo, so it follows the repo convention (nix/system/apply-*.sh) rather than being run from an agent:

sudo bash nix/system/apply-nct6683-module.sh     # NCT_DRY_RUN=1 to see the edit only

It matches the assignment, never a line number; refuses unless there is exactly one single-line boot.kernelModules list; backs up; and verifies.

🔴 It verifies against /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 — it cannot distinguish "persisted" from "a human modprobe'd it in August". Measured before the change: nct6683 absent from that file while live in lsmod. That file is what systemd replays at boot, so its content is the persistence claim.

scripts/i3status-fans + fansBlock render it, workbench-only, gated !isLaptop in both the blocks list and the home.file:

render state meaning
2660·1736 Idle both turning, above their floors
!0·1736 Critical the pump has stopped
2660·? Warning one tacho unreadable
? Warning chip absent — i.e. nct6683 not loaded

Three decisions, 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. test_this_pill_is_ALWAYS_VISIBLE is parameterized over every state, so a future port of the hide-at-zero idiom reddens the suite.
  • The floor is per-fan and OPTIONAL — only the pump gets one (500 RPM). fan2/fan4fan10 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. A fan passed without :MINRPM is display-only and can never raise Critical.
  • An alarm outranks an unreadable sibling (!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 on hwmon11 behind ten unrelated devices; two test fixtures carry decoy fan1_inputs so a by-number lookup fails the suite). icon = refresh because 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.

Verification

Live: home-manager switch + i3-msg restart, pill screenshotted on the real bar as 2810·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 only test_render_an_ALARM_OUTRANKS_an_unreadable_sibling (assert 'Warning' == 'Critical'), the other 49 passing.

Gate — merged tree at base 01956bf0, both tiers:

leg result
dev-host gate.sh --tier both PASS — 21163 collected / 21161 passed / 0 failed; node 1449/1449
nix checks.nodetests PASS — 1449/1449
nix checks.pytests RED — inherited from main, not this PR

🔴 The sandbox pytests red is main's. Control, same derivation built from origin/main alone with this change absent:

tree collected passed failed
origin/main (control) 13068 13048 20
merged 13118 13098 20

+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-fans appears in that log exactly once, as a routine patchShebangs rewrite.

main has moved twice more since 01956bf0 (#1401 and others) — none sharing a file with this diff, and scripts/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

ZacxDev and others added 3 commits September 8, 2026 13:52
…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
ZacxDev and others added 2 commits September 8, 2026 19:00
…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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant