Commit Graph

208 Commits

Author SHA1 Message Date
170f843481 Make the jailed lib/tests gate able to fail, without breaking measurement
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
The jail recorded each case's exit code but reported only timeouts,
discarding every other non-zero status. A suite with a failing assertion
exited 0 through the push gate -- the same blind spot that let an earlier
session ship 2 failing assertions in the nc_php suite. Found by negative
control, not by reading: a deliberately failing assertion still exited 0.

Propagating that status unconditionally was wrong, and caught before landing:
the jail's own usage example passes a `bogus` case that exits non-zero to
cover an error path, so a clean 5/5 = 100.00% run started failing. That is
the mode the 105-lib campaign runs in.

So the behaviour sits behind --fail-on-case-error, default off, passed only
by ci_mirror.sh and shell-tests.yml. Verified in all four quadrants:
measurement + error case = 0, gate + error case = 1, gate + clean suite = 0,
gate + broken assertion = 1.

Both gates also pass --min 0: with no --measure the jail was measuring
run_all.sh itself against its default --min 100 and failing the push at
54.55%, a coverage verdict neither gate asked for. ci_mirror.sh now prints
the jail's output on failure, so a red gate can be diagnosed.

The added flag pushed the jail over the 250-line cap, so the in-namespace
case runner moved to lib/shell_coverage_case_runner.sh, following the
existing lib/shell_coverage_jail_setup.sh split. All four quadrants were
re-run after the extraction.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016sxxsDHWgdRhmdmzNMA6XM
2026-08-22 12:16:18 +02:00
61ff5eacac Cover nc_php.sh (98.82%) and teach the jail to measure foreign platforms
nc_php.sh was the next-hardest library on the allowlist. It targets a
Debian Raspberry Pi -- it edits /etc/php/<ver>/apache2/php.ini, drives
occ as www-data and installs a crontab -- so none of its write targets
exist on this Arch host and the jail had nothing to mirror.

- --seed-dir / --seed-file create the paths a foreign-platform subject
  expects. Verified with a probe: /etc/php/8.2/apache2 exists inside
  the jail while /etc/php does not exist outside it.
- A failed bind is now FATAL, and missing mountpoints are created
  first. A bind that fails silently is indistinguishable from
  unreachable code in the report, which is the misdiagnosis this
  runner exists to prevent.
- Mirroring skips unreadable targets (/root as a normal user) rather
  than aborting under set -e.

nc_php.sh: 84/85 = 98.82%, 24 assertions. It STAYS on the allowlist,
because 98.82% is below the bar. Line 111 -- the crontab first-run
fallback -- hung the suite on both attempts to cover it, so it is left
uncovered and documented rather than tried a third time.

Two harness hazards found the hard way and written down: a top-level
stub function shadows the subject's own function for every later
assertion (this silently dropped coverage 83.53% -> 57.65% while the
tests still passed), and a stub for a piped-into command must drain
stdin or the writer takes SIGPIPE.

dot_resolver_install.sh still measures 39/39 = 100.00%.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DbD4nco3EgL1UFoZKmKsLE
2026-08-22 11:30:32 +02:00
bca70b67b5 Shell coverage: convert the ratchet to a measured 100% bar
The gate was a presence check -- 'a run_all.sh exists beside this lib' --
which an empty suite that asserts nothing satisfies. It now measures.

100% is reachable with no suppressions and no production source changes:
run the suite inside a user+mount namespace and the sudo-writes, systemd
units and nftables rules execute for real against a throwaway /etc.

- meta/scripts/shell_coverage_jail.sh: the runner, promoted out of a
  session scratchpad. Two fixes it needed on the way: bind targets mirror
  the real directory tree (an empty bind makes writes fail with ENOENT,
  which reads as unreachable code), and sudo gets a pass-through stub
  because real sudo cannot setresuid inside a userns.
- It fingerprints git status before and after and fails the run if a
  write escapes the jail. Verified with a deliberate escapee.
- dot_resolver_install.sh: 39/39 = 100.00%, 28 assertions. Picked by
  scoring every allowlisted lib on root ops, system writes and
  destructive calls -- it scored highest in the repo. Allowlist 106 -> 105.
- jscpd was already over its own threshold at 2.45% and would have
  rejected every commit staging a .sh file. Excluding vendored .venv/
  shell alone gives 1.67%; also excluding lib/tests/ gives 1.47%.

The gate is deliberately NOT wired into the hook chain yet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DbD4nco3EgL1UFoZKmKsLE
2026-08-22 11:20:58 +02:00
d42d8ff20d Shrink the shell-coverage allowlist 115 -> 106: cover scripts/lib
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
Gives linux_configuration/scripts/lib a real test suite, which takes all
nine of its libraries off meta/shell-coverage-allowlist.txt at once —
coverage is per-directory, so this is the highest-ratio first batch.

139 assertions across four suites, all offline: the mtk toolkit is driven
through its existing MTK_ROOT_FIXTURE seam, and adb is shimmed onto a
narrowed PATH. Nothing reads the wall clock, this host's udev rules, or a
real device, because the suite runs in shell-tests.yml and again on every
pre-push via ci-mirror — a host-dependent assertion would be a flake that
blocks unrelated pushes.

The gate only checks that tests/run_all.sh EXISTS, so an empty one would
mark all nine libraries covered while testing nothing. Each of the four
load-bearing rules was therefore mutation-tested and the suite required to
go red: classify_device's && -> ||, discover_partitions' init_boot/boot
ordering, mtk_select_device's refusal to guess between two phones, and
is_hour_in_range's 10#$ octal guard.

That last one initially SURVIVED. Dropping 10#$ makes bash abort with
"value too great for base", a non-zero exit indistinguishable from an
honest out-of-range result, so the status-only assertion passed either
way. It now asserts stderr is empty.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 23:52:09 +02:00
1d7271f023 Phase 2: enforce the 250-line cap and stop pushing red
Two pre-commit hooks and one pre-push extension, plus the _glyph_art split
that had to land first — the cap gate cannot be wired while any file is over
it, and the lyricanki note-ascender glyph had taken _glyph_art.py to 253.

file-length-cap runs check_file_length.sh --all repo-wide rather than on
staged files, so a file that grows past the cap in some other commit is still
caught. Proven to block, not assumed: with a deliberate 261-line file staged
the hook exits 1 and names it; with the file removed it passes.

ci-baseline-green is the user's option (a): refuse to build on a main that is
already red. It fails closed on a definitively failing baseline and warns-and-
passes on anything it cannot determine — offline, no gh, unauthenticated, no
settled runs — each call capped at 5s. Blocking every commit on network
reachability buys nothing when (b) is the real per-change guarantee.
CI_GREEN_SKIP=1 is the escape for a fix-forward commit. python-tests.yml is
excluded on purpose: it is path-filtered, so a shell-only commit never
triggers it and its "latest run" would produce false reds.

This hook already earned its place. Run against a red main earlier in this
session it refused with "Shell tests (failure)", which is how the broken
SETUP_SOURCES was found instead of being pushed over.

ci_mirror gains the shell-tests workflow's side-effect-free half — every
lib/tests/run_all.sh plus the workflow's own test list, the user's option (b).
That list is parsed out of shell-tests.yml rather than copied: a hardcoded
duplicate drifts the moment someone adds a test, and a mirror that quietly
checks less than CI is worse than no mirror because it reports safe. The
Arch-container half stays unmirrored — it wants root and an Arch userland.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F3PzGc75gBcdxP3PkHXGcV
2026-08-21 16:41:31 +02:00
d72f03ea93 setup_midnight_shutdown: split into lib/ — the cap is now zero
1734 -> 109 lines across eight libs plus one payload file. This was the last
file over the 250-line cap; check_file_length.sh --all now reports "all
checked files are within 250 lines".

This file needed a different proof from the others. Its product is scripts
generated into /usr/local/bin via heredocs, so "did the functions move
verbatim" is not the question that matters — "does it still emit the same
bytes" is. Every heredoc body was hashed before the split: 11 of 12 are
byte-identical afterwards, and the twelfth (the 224-line config-unlock body)
now lives in lib/payloads/shutdown-config-unlock.sh.in at hash
1a5c67cfd0244101, exactly matching what it replaced. No new bodies appeared.

The "~39 top-level functions" this file is documented as having are not real.
log(), now_epoch(), cmd_add() and friends sit at column 0 because they are
inside the generated scripts. The brace-matching extractor keeps each heredoc
with its owning generator, so they moved as part of it. That is also why
verify_shell_split.sh reports a bare "-log": it cannot tell a heredoc-internal
definition from a top-level one. log() is not lost — it is line 17 of the
payload, which is where it always ran from.

$_MS_PAYLOAD_DIR is defined in the same file as its use, directly above it,
following the _NL_PAYLOAD_DIR lesson from earlier in this campaign.

--help is byte-identical, shellcheck is clean with no suppression added.
`setup` itself is not executed — it installs systemd units, makes the config
immutable and arms a shutdown timer.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F3PzGc75gBcdxP3PkHXGcV
2026-08-21 16:27:54 +02:00
fbe9134c22 pacman_wrapper: split into flat libs under the 250-line cap
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Python tests / test (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
929 -> 217 lines across five pw_*.sh libraries. This file IS /usr/bin/pacman
here (the symlink points at the deployed copy), so the design constraint was
not readability but that no failure mode may leave the machine unable to
install packages.

Fail OPEN, unlike the other splits in this campaign. A missing lib warns on
stderr and execs real pacman unwrapped. Failing closed would mean a missing
lib leaves no way to install anything, including the missing lib. This is the
same choice the existing pacman_lock_lib.sh block already makes, and it is
verified rather than asserted: with pw_guard.sh removed from a scratch-dir
copy, the wrapper warns, execs, and returns real `-Qi bash` output at rc=0.

The libs load at the very top, before the fast path, because the fast path
calls needs_unlock() to decide whether to exec straight through, and that
function now lives in pw_guard.sh.

They are deployed flat beside the wrapper — the shape pacman_lock_lib.sh and
heavy_job_lock.sh already use in /usr/local/bin — as required copies, and are
added to the integrity manifest. Tamper detection was round-tripped in a fake
root: 8 manifest entries verify clean, and appending one line to
pw_challenge.sh is detected and the transaction denied.

Verified by execution, not just textually: real `-Qi bash` and `-Q` through
the split wrapper are byte-identical to the pre-split baseline, same 2824
packages. verify_shell_split.sh reports all 14 top-level functions moved
verbatim.

The deployed copy is deliberately NOT updated by this commit and still hashes
548fc2b5... exactly as before the work started; live pacman is unaffected.
A live `pacman -S` remains unverified — the Bash tool deadlocks on db.lck and
a failed experiment costs the package manager.

Cap is now down to one file.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F3PzGc75gBcdxP3PkHXGcV
2026-08-21 16:19:26 +02:00
79a49846b9 setup_night_lockdown: define _NL_PAYLOAD_DIR where it is used
The split referenced $_NL_PAYLOAD_DIR from both install functions but
defined it nowhere, so `setup_night_lockdown.sh setup` would have died
with an unbound variable partway through installing the lockdown. My
earlier scratch verification set the variable by hand, which is exactly
what hid the bug: the emitted artifacts hashed correctly while the real
entry path could not have reached them.

It is now defined in each lib that uses it, eight lines above the use,
via `: "${_NL_PAYLOAD_DIR:=...}"` so an explicit override still wins. A
definition placed in the entry script was silently stripped by an
autoformat pass three times during this work, and a half-run lockdown
installer is not an acceptable failure mode.

Also drops two zero-byte payload files left behind by the .sh -> .sh.in
rename.

Re-verified without presetting anything: install_enter_script and
install_unlock_script emit mode-0755 files hashing 4baaf405... and
eafce28d..., byte-identical to the live /usr/local/bin copies. help and
an unknown command match their baselines; status differs only by the
05:00 countdown ticking 13h to 12h between runs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 16:02:59 +02:00
f6bfdff76a setup_night_lockdown: split into lib/ under the 250-line cap
918 -> 138 lines, across five libs plus two data-file payloads.

The two generated scripts became data files rather than heredocs.
ENTER_EOF alone was 279 lines, so any lib containing it would have been
over the cap however the rest was arranged. Both heredocs were quoted,
so their bodies were already literal text with no build-time
interpolation, and copying them verbatim is exactly equivalent.

The payloads are named *.sh.in on purpose. The pre-commit hook globs
staged files by '\.(sh|bash|zsh)$', runs shfmt -w on them and re-stages
the result -- and shfmt does reformat these bodies. That would have
changed their bytes and destroyed the property this split is verified
by. The .in extension keeps them out of every .sh-globbing hook without
a suppression or an exclude regex.

cat + chmod 0755 became a single `install -m 0755`, closing the window
the original left between creating the file and making it executable.

The load-bearing check: running install_enter_script and
install_unlock_script against a scratch directory produces mode-0755
files whose sha256 sums are identical to the copies already deployed in
/usr/local/bin on this machine. The split provably emits the exact bytes
that are running today, and it touched no system file -- sha256sum -c on
both deployed artifacts passes unchanged.

help, status and an unknown command are byte-identical to their
pre-split baselines. A line-multiset check leaves exactly six lines
unaccounted for: the two cat/heredoc openers, the two terminators and
the two chmod calls that install -m now performs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 15:59:35 +02:00
72e1b6a8b1 block_compulsive_opening: test the allow/block decision
21 assertions for cco_state.sh, the lib that decides whether a launch is
permitted. Coverage measured: 62/66 lines = 93.94%. The only uncovered lines
are kill_app's pkill/pkill -9 pair, which must never run in a test on a
machine whose apps are live. cco_wrapper/cco_install/cco_report stay at 0%:
every function in them moves system binaries, writes /etc/pacman.d/hooks or
kills processes.

The assertion that matters most is the one in the negative direction: a
PREVIOUS hour's stamp must NOT keep blocking. If that decision inverted, a
legitimately-closed app could never be reopened, and the failure would look
like the tool working. Both directions are pinned, plus the empty-stamp case
and stale suspend-marker cleanup.

Also generalises run_all.sh from test_leechblock_*.sh to test_*.sh. This
directory now holds suites for more than one entry script, and a prefix glob
silently skips every suite added after it — including inside the CI discovery
step added in fbec4656, which runs this file.

The harness asserts its own fixture is complete rather than letting a typo in
a global surface as a confusing unbound-variable from inside the code under
test. That check is also what makes the fixture's globals legible to
shellcheck, which cannot see them being read across the source boundary — a
real check that happens to remove SC2034, not a suppression.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F3PzGc75gBcdxP3PkHXGcV
2026-08-21 15:53:54 +02:00
f5edebae4a block_compulsive_opening: split into lib/ under the 250-line cap
705 -> 182 lines, with state, the launch gate, install/uninstall and
reporting moved into cco_state.sh, cco_wrapper.sh, cco_install.sh and
cco_report.sh.

The entry script keeps its exact deployed path and stays a real file
there, not a symlink: the app wrappers already installed on disk
hard-code /usr/local/bin/block-compulsive-opening.sh, and the pacman
hook invokes it. _CCO_LIB_DIR probes for lib/ beside the script and
falls back to the script's own directory, so the repo tree and the flat
/usr/local/bin deploy layout both work; both branches were exercised.

install_all deploys the libs and uninstall_all removes them in this same
commit, so there is never a deployed copy that is split but missing its
libs -- the failure that had to be repaired for install_leechblock.

The libs deploy flat into /usr/local/bin rather than into
/usr/local/share/digital_wellbeing/lib, because install_leechblock_payload
does an rm -rf mirror of that directory and would delete them.

Verified beyond the usual gates: a line-multiset check reports zero
missing functional lines, and all three heredoc bodies -- the generated
app wrapper, the generated pacman hook and the help text -- are
byte-identical, which verify_shell_split.sh cannot check. status output
matches the still-monolithic deployed copy byte for byte.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 15:51:06 +02:00
63454fa544 install_leechblock: first test suite for the split libs
29 assertions across two files, run via lib/tests/run_all.sh.

Coverage measured, not waived: leechblock_fetch.sh 38.57% (27/70),
leechblock_browsers.sh 36.47% (31/85), config and firefox 0%. The uncovered
lines are exactly the bodies that must never run under test — curl/tar/unzip
downloads, pkill + sudo binary patching, npm install, and writes to
/etc/*/policies.json. Closing the gap needs those externals shimmed.

What the tests actually pin down:
- all three get_latest_tag fallbacks (releases API, tags API, Location
  header), plus JSON-null-is-not-a-tag and the all-sources-exhausted
  non-zero return. A silent failure there installs the wrong version.
- the 8+3 browser tables including every hyphenated key, and the
  command -v gating that decides whether a binary is patched at all.

Also fixes a latent set -u abort: wire_up_browsers interpolates VERSION_DIR,
VERSION and TAG, which only resolve_version sets. Calling it on its own
aborted on an unbound VERSION_DIR; they are now read through locals with
defaults, leaving production output unchanged.

Harness note: PATH must be narrowed to the shim dir for "no browser
installed" to be testable on a machine that has five, but every external the
code under test calls has to be seeded there too — a missing mkdir aborted
the suite under set -e with no stderr, which under kcov looked like a
coverage-tool bug rather than a PATH bug.

The hyphenated array subscripts in the test are quoted for the same reason
they are in the lib: shfmt -w rewrites an unquoted [google-chrome-stable]
into a subtraction expression. Reproduced live in this file during the work.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F3PzGc75gBcdxP3PkHXGcV
2026-08-21 15:39:15 +02:00
1dceca6600 install_leechblock: split into lib/ under the 250-line cap
485 -> 114 lines, with the six install phases moved into four sourced
libs: leechblock_fetch.sh, leechblock_config.sh, leechblock_browsers.sh
and leechblock_firefox.sh.

detect_browsers, replace_browser_in_place and wire_up_browsers stay in
one lib on purpose: detect_browsers writes the BROWSERS/FIREFOXES arrays
that wire_up_browsers reads, and splitting them would create a
write-only cross-file global.

Four justified body changes, all recorded in the evidence:

- Quoted the six hyphenated associative-array keys. shfmt 3.13.1 parses
  an unquoted [google-chrome-stable] as arithmetic and rewrites it to
  [google - chrome - stable], which would break browser detection
  entirely. The pre-commit hook runs shfmt -w on staged files and
  re-stages them, so this corruption would have landed in the commit
  unseen -- this is what caused the previous attempt at this split to be
  reverted. Behaviour-preserving: quoted and unquoted are the same key.

- SCRIPT_DIR is assigned in the entry script rather than inside
  inject_default_config. Left in the lib, ${BASH_SOURCE[0]} resolves to
  lib/, so the defaults JSON lookup would miss and default blocking
  rules would silently never be seeded.

- install_firefox_policy takes ff_found as a parameter instead of
  reading it as a cross-file global, fixing a genuine SC2154 without a
  suppression.

- Added require_cmd unzip. It was used bare under set -Eeuo pipefail, so
  a missing unzip would abort after the version dir was populated but
  before the 'current' symlink was made, leaving a half-installed tree.

verify_shell_split.sh reports additions only -- no -/+ pair -- so
get_latest_tag and replace_browser_in_place moved byte-identically.
--help and unrecognized-flag output are byte-identical to the baseline.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 15:32:48 +02:00
47478791d8 hosts/install: split into lib/ under the 250-line cap
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
913 lines down to 150. The ten functions were only 202 lines; the other 712
were a single top-level block, now eight phase functions across six libs. As
in the previous split the bodies were wrapped WITHOUT re-indenting, because
five heredocs use quoted terminators that must stay at column 0 (shfmt
indented them afterwards, which is safe).

The ~320-line custom-blocklist heredoc became custom_entries.hosts. It was
321 of the 370 lines of hosts_write.sh and cannot be split, so that lib could
not otherwise fit the cap; a hosts-format data file is also far easier to
audit and diff than a quoted heredoc.

That move broke a guard, which is the important part of this commit:

  extract_custom_entries_from_script sliced the blocklist out of the script
  between "# Custom blocking entries" and the heredoc terminator. With the
  entries in a data file there is no terminator, so it would have extracted
  NOTHING -- and check_custom_entries_protection, the guard whose entire job
  is refusing to drop blocked domains, would have compared against an empty
  list and passed everything. It now reads the data file, and the extracted
  set is identical: 254 domains, diffed.

A second, pre-existing bug surfaced while testing:

  extract_unblock_entries_from_script matched its OWN marker lines.
  PROTECTED_UNBLOCK_LIST_START and _END are word characters, so the
  '^# [a-zA-Z0-9._-]+$' pattern pulled both into the whitelist as if they
  were domains. Both are in this machine's live state file right now.
  Renaming either marker would have read as the whitelist growing and blocked
  the install outright.

There is no dry-run or status mode, so this file cannot be proven by running
it: an aborted run leaves /etc/hosts mutable with the guard's bind mount down.
The proof is textual instead. Every one of the 8 wrapped phase bodies is
byte-identical to its original line range; a line-multiset check found 0 lines
lost; and an indentation-insensitive comparison against the pre-split file
leaves exactly 8 absent lines, each an intentional edit (the rewritten
extractor, three `readlink -f "$0"` made overridable, the /etc/hosts fallback
made overridable, and the heredoc markers). /etc/hosts is byte-unchanged
across the whole session, and `--help` output is identical.

36 assertions. Both anti-tamper guards at 100% line coverage (75/75 and
49/49), covering both directions of each: removing a blocked domain is
refused and adding one allowed; growing the whitelist is refused and shrinking
it allowed. A same-count swap is refused too, which a count-only check would
have let through.

The install phases themselves are deliberately untested -- running them would
take down the protection they install. That is weaker evidence than the two
previous splits had, and it is inherent to this file rather than a gap.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F3PzGc75gBcdxP3PkHXGcV
2026-08-21 14:45:04 +02:00
241652cade generate_study_materials: split into lib/ under the 250-line cap
1017 lines down to 148. Six libs: the per-language doc-URL builders (split in
two only for the cap), the lookup/dispatch layer, and one per generation phase.

The hard part was not the ten functions but the 335-line top-level block,
which extract_shell_functions.py cannot move because it only relocates
functions. It is now generate_doc_links / generate_anki_cards /
generate_llm_prompt, wrapped WITHOUT re-indenting the bodies: five heredocs
use quoted terminators, which must stay at column 0.

Proof the split is behaviour-preserving: a golden fixture captured before any
edit, re-checked after every step. All three outputs -- anki_cards.txt,
documentation_links.md, llm_anki_prompt.md -- stay byte-identical by sha256sum,
and stdout matches too.

Four latent defects surfaced while writing the tests, all fixed:

  python_doc_url required a $2 it never reads, and get_doc_url has always
  called it with one argument. That only worked because the entry script runs
  `set -e` without `-u`, so "$2" silently expanded to empty; any caller with
  `set -u` aborted. Defaulted, along with three more positionals documented as
  optional but declared required.

  The two imports loops hardcoded `head -20` while the other seven honoured
  TOP_N, so --top was silently ignored for imports.

  Only one of nine term loops skipped comment lines, so a '#' line in an
  analysis file became a flashcard whose front was a comment.

  Introduced and fixed within this change: the comment filter has to run
  BEFORE `head -n` or comment lines count against the limit, and it needs
  `|| true`, since grep exits 1 when it filters every line and the callers run
  under `set -e`.

The golden fixture cannot show the last three, because it has no comments and
fewer than 20 imports -- which is exactly why they survived. A second fixture
with 25 imports and comment lines, at --top 3, emitted 20 imports and two
comment cards before, and 3 imports with zero leakage after.

166 assertions across 4 test files. 100% line coverage on study_doc_urls.sh
(38/38), study_doc_urls_more.sh (30/30), study_doc_lookup.sh (49/49) and
study_gen_llm.sh (54/54). study_gen_docs.sh 93.10% and study_gen_anki.sh
96.15%, where every remaining line is a `} >>"$FILE"` closing a brace group
whose output a passing test asserts -- the same kcov attribution artifact
recorded for services_hosts.sh.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F3PzGc75gBcdxP3PkHXGcV
2026-08-21 14:32:08 +02:00
08f3c764b2 check_and_enable_services: a warning must never mask an error
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
Eight checks assigned status="warning" unconditionally, so a check running
later in the same function overwrote the status="error" an earlier one had
set. The five other sites in this family already guard the assignment with
`if [[ $status != "error" ]]`, so this was an inconsistency, not a design.

Only half of it was load-bearing, and the two halves fail differently:

  services_units.sh (4 sites) reports through report_and_fix, which repairs
  ONLY on "error". A downgrade there meant no repair ran at all. The is-active
  check is the LAST status assignment in each of the three functions, so it
  could mask any error above it -- in check_periodic_systems, three separate
  is-enabled checks. A timer that was both disabled and inactive, which is the
  ordinary shape of a broken timer, was reported and then left broken.

  services_hosts.sh (4 sites) gates its own repair on `status != "ok"`, so the
  repair still ran. There the bug printed a real error as a yellow [!] warning
  and understated the summary row.

Demonstrated rather than argued, against a simulated broken machine (timer
disabled AND inactive, unit files present):

  without the guards:  status=warning, repair ran 0 times
  with the guards:     status=error,   repair ran 1 time

The seven assertions that previously pinned the broken behaviour now assert
the correct behaviour; they were rewritten first and failed, then the guards
made them pass. Two further cases cover the masking shape directly: a broken
guard-lib instance must still be repaired when a later check warns.

`--status --dry-run` output is still byte-identical to rev 6691cb26, because
this machine has no disabled timer and no missing pacman hook -- the buggy
path is never taken here, which is precisely why it went unnoticed. The A/B
above is the evidence the live diff cannot provide.

222 assertions across 10 files. services_units.sh holds 100% coverage;
services_hosts.sh rises from 86.05% to 93.02%. test_services_hosts.sh needed
splitting twice to stay under the 250-line cap (hosts_fix, resolved), and
stage_hosts_ok moved into the harness so both new files can use it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F3PzGc75gBcdxP3PkHXGcV
2026-08-21 14:16:01 +02:00
0a69d18390 check_and_enable_services: 100% coverage via a redirectable path prefix
Follow-up to 9085a4e5, which recorded services_units.sh at 84.51% because
its remaining branches sit behind [[ -f /etc/systemd/system/*.service ]] --
absolute root-owned paths that exist on this machine, so the else-branch
never runs and no PATH shim can redirect it.

All 87 such paths across the seven check libs now go through $SYSROOT, read
from SERVICES_ROOT. The prefix is deliberately NOT defaulted. Three commands
in hosts_fix_resolved run outside `run` and so are not stopped by --dry-run
(chattr -i on resolved.conf.d, find -delete over its *.conf, and an append to
resolved.conf); with an empty default, a test that forgot to set SERVICES_ROOT
would really delete this machine's resolved.conf drop-ins. Unset is therefore
a hard error and only an explicitly empty value means the real filesystem.

The harness now owns PATH outright: $FAKE_BIN for shims and staged fake apps,
$REAL_BIN for a curated set of symlinks to genuine coreutils. The machine's
/usr/bin is deliberately absent -- a really-installed discord and chromium
otherwise make the "no target apps present" branches untestable, which is
exactly what happened until these tests caught it.

Coverage, measured against run_all.sh because two test files jointly cover
services_common.sh after the 250-cap split: common 55/55, report 27/27,
units 72/72 (was 84.51%), wrappers 100/100, apps 66/66, browser 79/79 --
seven libs at 100%. services_hosts.sh reads 86.05% and services_hosts_fix.sh
97.30%, and those 14 lines are a kcov attribution artifact rather than a gap.
Falsified rather than assumed: the suite prints "51 passed, 0 failed" when run
under kcov, so no group is skipped, and every supposedly-uncovered line is an
issues+=("...") whose string can only reach stdout through the print loop
below it -- with a passing assertion matching that exact string.

Two falsification checks, both re-run after the rewrite: `--status --dry-run`
as root stays byte-identical to rev 6691cb26 modulo the Date: line, and the
sha256sums of /etc/hosts, /etc/nsswitch.conf and /etc/systemd/resolved.conf
plus the contents of resolved.conf.d are unchanged across a full suite run.
The second one is what makes a missed prefix site detectable instead of a
matter of inspection: the repair paths really do write.

220 assertions across 8 test files. Also fixes a bug in the harness's own
lsattr shim, where printf with a leading-dash format string parsed the dashes
as options and silently produced nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F3PzGc75gBcdxP3PkHXGcV
2026-08-21 14:08:11 +02:00
9085a4e508 check_and_enable_services: split into lib/ under the 250-line cap
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
1225 lines down to 202, with the 21 functions moved into eight sourced
libraries grouped by subject. The entry script now holds only the
configuration, the argument parsing and the order the checks run in.

check_hosts was the one function that could not move verbatim: at 260 lines
it would have stayed over the cap on its own, so its repair half is now
hosts_fix_nsswitch / hosts_fix_resolved / hosts_repair_all in
services_hosts_fix.sh. Repair order is preserved (resolver config, then the
hosts file, then the guard-lib migration), and hosts_repair_all returns
non-zero under --dry-run so a dry run can no longer promote a status to "ok"
on the strength of repairs that never happened.

SERVICE_STATUS was a write-only global across five libs, which is a real
SC2034 rather than noise: the pre-commit hook runs shellcheck with no -x and
genuinely cannot see the one reader. It now goes through set_service_status /
get_service_status, which also rejects a duplicate write — each check owns
exactly one summary row, so a second write means two checks are fighting over
it. No suppression was added anywhere.

Proof the behaviour is unchanged: `--status --dry-run` run as root against
this machine is byte-identical to the same command from a worktree at the
pre-split revision, modulo the Date: line. verify_shell_split.sh reports
check_hosts as the single expected difference; the other 20 functions hash
identically. 72 assertions across three test files pass, with 100% line
coverage on services_common.sh (54/54) and services_report.sh (27/27).

services_units.sh measures 84.51% (60/71). The 11 uncovered lines are the
else-branches of [[ -f /etc/systemd/system/*.service ]] and
[[ -f /usr/local/bin/*.sh ]] probes — absolute root-owned paths that exist on
this machine and that no PATH shim can redirect. Reaching them needs a
root-prefix rewrite of live enforcement logic, which is not a line-count fix
and would forfeit the verbatim-move proof for everything else.

Found while testing, deliberately NOT fixed here: in all three unit checks
the is-active branch assigns status="warning" unconditionally, overwriting
the status="error" the is-enabled branch set just above it. A timer that is
both disabled and inactive therefore never gets repaired, because
report_and_fix only repairs on "error". It is identical in the pre-split
file, so this split neither caused nor hides it. The tests pin the current
behaviour and name it; fixing it changes enforcement and earns its own commit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F3PzGc75gBcdxP3PkHXGcV
2026-08-21 13:47:05 +02:00
7d7b3c71f9 focus_owner: split EnforcementRunner.kt under the 250-line cap
Extracts four cohesive concerns into standalone files:
LocationAcquisition (fix acquisition), HomeLocationStore (home
read/write), PolicyPinning (VPN/DNS pin + visibility sweep),
SweepablePackages (installed-package enumeration). Public API
unchanged -- EnforcementRunner.best(...) deliberately stays put since
EnforcementRunnerBestTest.kt calls it directly; every other public
method is a one-line delegating wrapper.

Verified with gradlew compileDebugKotlin + test --rerun-tasks
(BUILD SUCCESSFUL, 40 tests / 0 failures, including the 6
EnforcementRunnerBestTest cases unmodified).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
2026-08-21 13:17:59 +02:00
6efb4baf04 focus_owner: split DevicePolicyBridge.kt under the 250-line cap
Some checks failed
Pre-commit checks / pre-commit (push) Has been cancelled
Shell tests / shell-tests (push) Has been cancelled
Shell tests / arch-tests (push) Has been cancelled
Kotlin has no part-of like Dart, so this extracts three cohesive
method groups into standalone internal helper classes held as
private delegates: DevicePolicyVpnDns (VPN/Private DNS pinning),
DevicePolicyLocation (location permissions), DevicePolicyUninstallGuard
(uninstall blocking). DevicePolicyBridge's public API is unchanged --
every method MainActivity.kt/EnforcementService.kt call directly
still exists as a one-line delegating wrapper.

Verified with gradlew compileDebugKotlin + test --rerun-tasks
(BUILD SUCCESSFUL, 40 tests / 0 failures).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
2026-08-18 23:56:21 +02:00
327d48badd Archive legacy hosts-guard subsystem: superseded by guard-lib
setup_hosts_guard.sh was NEXT_SESSION_PROMPT.md's next split target, but
live-machine inspection showed it (and 16 supporting files - enforce-*.sh,
6 systemd units, install_pacman_hooks.sh, pacman-hooks/, psychological/,
both READMEs) is fully retired in favor of guard-lib, migrated 2026-07-04.
User confirmed archive over split, then confirmed the larger 17-file scope
once the directory was surveyed. Full history preserved via git filter-repo,
live at github.com/kuhyx/testsAndMisc-archive/tree/main/linux_configuration/scripts/periodic_background/hosts/guard.
plugins/{nsswitch,resolved}-plugin.sh stay - they're guard-lib's live
plugins, referenced by absolute path from /etc/guard-lib/targets/*.conf.

check_and_enable_services.sh's check_hosts() rewritten to check guard-lib
state (guardctl file-guard status) instead of the retired units/scripts it
used to check and self-repair - verified live, now reports the hosts
service ok against the actual migrated machine instead of a false error.
copilot-instructions.md and one install.sh comment updated to match.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
2026-08-18 22:49:54 +02:00
015eb48714 diagnose_pacman_hook_stall.sh: split 493 -> 249 + 6 libs, 100% coverage
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
Confirmed live (not dead code) via the dead-code check-in before any code
was touched -- the last two candidates this pattern was tried on were both
archived instead. Kept run_one/cleanup/log_size/main together in the entry
script: a prior session (2527fb3) already proved splitting run_one out
produces a real SC2153/SC2034 pair since PACMAN_PID/LAST_ELAPSED cross the
EXIT trap, not a suppressible false positive.

Fixed a real bug found while writing tests: print_summary's inline awk had
a dead `NR == 0` guard (printf on an empty array still emits one blank
line), reachable via `-n 0`. Refactored mem_available_mb, descendant_pids/
kill_tree, capture_stall's two output blocks, and print_summary's median
calc after confirming empirically (bash -x tracing) that kcov's
xtrace-based coverage can never see a `done <redirect>`, `} >redirect`, or
inline-awk line -- user chose to fix the code over the coverage tool.
descendant_pids specifically needed an explicit-fd process substitution
(not a pipe) to keep its array mutation in the current shell.

Made PACMAN_BIN/PACMAN_LOG/CACHE_DIR/PACMAN_LOCK/LOAD_FLOOR_MB/
LOAD_MIN_FREE_MB env-overridable for testing. Caught and fixed, before it
ran unbounded, a --with-load test that would have dd'd 13-20+ GB into
/dev/shm using this box's real available memory.

56 lib-level + 13 entry-level PATH-shim/subprocess assertions, 0 failed.
The entry test itself grew past 250 lines while adding coverage and had to
be split into a shared harness plus two test files, mid-session.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
2026-08-18 22:33:57 +02:00
071d706d8a Archive install_plagiarism_tools.sh: unused, moved to testsAndMisc-archive
Full history preserved via git filter-repo, including the pre-rename path
from before the single_use/periodic_background split. Live at
github.com/kuhyx/testsAndMisc-archive/tree/main/linux_configuration/scripts/single_use/utils.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
2026-08-18 20:50:05 +02:00
2a7b9d90ac install_plagiarism_tools.sh: split 534 -> 94 + 3 libs, with a real test harness
Wraps the four install stages into named functions, one per lib/plagiarism_*.sh
sibling (matching install_exercism.sh's convention), and extracts the two
embedded Python heredocs into real, lint-clean check_plagiarism.py/sherlock.py
files -- check_plagiarism.py needed a second split into plagiarism_similarity.py
to also clear the 250-line cap. Every function reaches 100% shell coverage via
a PATH-shim test harness (fake python3/pip/git/java/wine); the two Python CLIs
were separately verified against a real venv with actual nltk/scikit-learn.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
2026-08-18 20:41:18 +02:00
b11b93457f deploy: split 875 -> 241 + 6, finishing phone_focus_mode
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
Held until last on purpose: all five enforcer splits this session grew
deploy.sh's two hardcoded file lists, so cutting it earlier would have left
every "add to BOTH lists" step pointing at a stale location. Those lists are
final as of the focus_ctl commit, so this is now safe.

do_deploy was 421 lines -- over the cap on its own, so moving it whole would
have achieved nothing. It is cut at the phase boundaries it already prints
("[4/7] Uploading scripts...", "[5/7] Setting permissions...") plus two more
inside the push phase, giving seven named functions instead of one wall.

The two hardcoded lists now live in SEPARATE files, which is the real risk
this commit introduces. deploy_phases.sh holds the push list and
deploy_install.sh the cp list; each header names the other and says what
happens when they drift -- the file is staged, never lands, and whatever
sources it fails to start with no obvious cause. Checked in both directions
after the move: the only asymmetry is config_secrets.sh, which a conditional
block handles so the phone's real coordinates survive a deploy. That was
asymmetric at HEAD too.

check_coords now RETURNS whether the coordinates are placeholders instead of
assigning $NEEDS_GPS_FETCH. The flag is written in deploy.sh and read in
deploy_phases.sh, so assigning it from deploy_gps.sh as well left that file
writing a global it never reads. Behaviour proven identical to HEAD: real
coords -> 0, placeholders -> 1.

NO TESTS, and that is a deliberate limit rather than an omission. Every
phase here is a sequence of adb calls against a real device; a stub layer
deep enough to mean anything would be modelling the phone, not testing the
script. Verification is that deploy.sh parses, sources all six libraries and
reaches its usage message -- which catches a broken split, not a wrong one.

515 assertions across ten phone suites still pass; run_phone.sh --help
exits 0.

Over-cap 16 -> 15, and NO phone_focus_mode file is over the cap any more.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
2026-08-17 17:53:52 +02:00
3ddc13f671 focus_ctl: split 1091 -> 171 + 8 siblings, one per enforcer
The largest file in phone_focus_mode, and the last one blocking deploy.sh:
splitting it grows deploy.sh's two hardcoded lists, so those lists are only
final now. All 55 functions moved verbatim.

Three while-read loops were audited before extraction. Two call pm in their
bodies and moved untouched. The third, iter_whitelist_packages, is fed by a
pipe from printf and reads nothing from stdin -- already safe, and now said
so in ctl_usage.sh's header so the distinction is not lost by a later editor
who has only read the warning in daemon_apps.sh.

136 assertions and 22 mutations, all killed. Three survived the first run
and every one was a defect in MY OWN tests, not in the subject:

  - cmd_status carries a THIRD copy of the Haversine, and I had asserted
    only that it printed "some number". A wrong Earth radius sailed through.
    Now the value is checked against a seeded fix ~4.3 km from home.
  - the two whitelist filters were each tested with input the OTHER filter
    also rejects, so either could be deleted with nothing failing. Fixed
    with a punctuation-free undotted token (only the dot check rejects it)
    and a dotted token carrying an illegal character (only the charset check
    does), making each independently observable.

That last investigation turned up dead code: the `#*)` case arm is provably
equivalent, because `set -- $line` makes a comment's first token "#", which
the dot check already rejects. Removed rather than covered by a test that
cannot fail.

Coverage is NOT 100% and the real numbers are in the evidence: 41.79% to
92.98% across the eight, up from 5-22%. Unlike the daemon split, most of
what remains is genuinely untested -- the per-enforcer status blocks that
print mount state, APK hashes and offload settings, each needing its own
device fixture. The mutations prove the assertions that exist are load-
bearing; they do not prove those branches work.

jscpd checked in a clean worktree at 1.57%: eight files each carrying a
near-identical start/stop/status trio is exactly what that gate is for, so
the tests drive the trios through tables instead of repeating them.

Two drift hazards recorded, not fixed: ctl_is_curfew_now duplicates
daemon_location.sh's is_curfew_now (pinned by an agreement test across every
boundary minute), and cmd_status's awk is the third Haversine.

Over-cap count 17 -> 16. deploy.sh is the last one here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
2026-08-17 17:46:04 +02:00
7dc3c94160 config.sh: split 571 -> 250, with a fingerprint that actually works
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
The brief's prescribed safety net for this file DOES NOT WORK, and that is
the main finding here. It specifies asdict + json.dumps(default=str) +
sha256 against baseline 1624750c. Four policy fields are frozenset, which
json cannot encode, so default=str stringifies them in SET ITERATION ORDER
-- random per process. Three consecutive runs gave three different digests;
pinning PYTHONHASHSEED=0 gave one. The recipe can never match a recorded
baseline, so it would have flagged every split as a regression regardless
of correctness.

The repo already ships a deterministic serializer. Before trusting it I
checked it was not a weaker proxy: policy_to_json emits all 16 dataclass
fields plus 6 more, so it is strictly stronger than asdict. Baseline
83e05e82, unchanged through all six extractions.

Two audits ran BEFORE any extraction:

  - the loader regex-scans config.sh's TEXT ALONE, so the fifteen names it
    reads are pinned here. Moving one does not error -- it silently yields
    an empty set, and for WHITELIST that reads as "hide everything".
  - extractions are variable-scoped, never line-range. A line-range attempt
    cut through the WHITELIST string, leaving an unterminated quote; 15
    variables vanished from the environment WHILE THE FINGERPRINT STILL
    MATCHED, because the loader's regex tolerated the mangled file. That is
    why the environment diff exists alongside the hash.

The environment check earned its keep twice: it also caught STATE_DIR being
left behind the source line, which expanded every path against an empty
prefix (/status.json rather than the real path) with the policy hash green
throughout. 79 variables, byte-identical either side, and every parsed list
compared entry-by-entry: 29/16/46/1/1/2/15, all matching.

Rationale prose moves to docs/policy-lists.md with pointers left behind,
including the blocks inside the list strings -- the loader and the shell
both ignore '#' lines there.

All five siblings are in both deploy lists. deploy.sh still sources the
chain from the checkout, run_phone.sh --help exits 0, 379 shell assertions
pass across nine suites, and focus_policy's 137 pytest cases pass.

config.sh is at EXACTLY 250 with no headroom; 184 of those lines are
loader-pinned lists that cannot move. Over-cap count 18 -> 17.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
2026-08-17 17:17:02 +02:00
094c4765f3 focus_daemon: split 543 -> 206 + 165 + 140 + 94, with 110 tests
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
Location/curfew goes to daemon_location.sh, the derived state files to
daemon_state.sh, the app layer to daemon_apps.sh. All 23 functions moved
verbatim -- nothing restructured, which matters more here than anywhere
else in this session.

focus_daemon.sh has five `while IFS= read -r pkg; do ... done <file` loops
and EVERY body calls pm. Piping any of them in lets pm inherit and drain
the loop's stdin, so it would process one package instead of all of them --
measured elsewhere in this repo as 1 of 54. That audit ran BEFORE the
extraction, not after coverage came back short, and all five moved
untouched with a note above them saying why.

CURRENT_MODE and _CURFEW_TICK_CACHED are each written in both the entry
script and a library, so both stay declared with main, same call as
NET_BUILT in the curfew split.

110 assertions and 33 mutations, all killed. Two survived the first run and
both were defects in my own tests: the whitelist and sysprotect exactness
checks used a SUPERSTRING of a listed entry, which fails under grep -qF and
grep -qxF alike and so proved nothing about which was in use.

Two subjects are NOT at 100%, and the numbers are reported as measured:

  daemon_state.sh     67/67 = 100.00%
  daemon_location.sh  26/40 =  65.00%
  daemon_apps.sh      71/76 =  93.42%

Both shortfalls are kcov attribution failures, shown to be artifacts rather
than gaps. daemon_location.sh's missing lines are all inside the embedded
awk Haversine; mutating the Earth radius and dropping the longitude term
both fail the suite, and an isolated probe shows a one-line awk measuring
100% where the identical multi-line one measures 0%. Collapsing the formula
to satisfy the tool would cost the readability of the only real mathematics
in the repo, so it stays.

daemon_apps.sh's five are the trailing `done <file` of the very loops
described above. That is the same artifact piping fixed in curfew_net.sh,
and it is exactly what must NOT be done here. Redirecting two of them from
/dev/null fails the suite, so the lines run; all three are pinned in the
mutation spec instead.

Over-cap count 19 -> 18.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
2026-08-17 16:54:04 +02:00
0a07684cca hosts_enforcer: split 421 -> 207 + 166 + 91, both libs at 100%
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
The bind-mount logic goes to hosts_mount.sh, the Magisk-module and
cache-invalidation logic to hosts_magisk.sh. Both flat siblings, both in
both deploy lists; the two lists were then checked to correspond exactly
in both directions, since a push with no matching cp stages a file that
never lands and the enforcer would fail to source it.

Audited for stdin-draining `while read` loops BEFORE extracting, because
piping one would change behaviour in a way stubs cannot catch. There are
none here. Also confirmed no function writes a global, so there was no
cross-half hazard of the kind that kept NET_BUILT in curfew_enforcer.sh.

69 assertions, 43/43 and 62/62 lines, 17 mutations all killed. Two survived
the first run and both were real findings:

  - the unmount give-up threshold was not pinned. Raising it from 5 to 500
    changed nothing observable, so the enforcer could have spun 500 times
    per poll against a mount that was never going to clear. Now asserted as
    a call count.
  - the `[ ! -f $HOSTS_TARGET ] && return 1` guard in
    is_bind_mounted_correctly is PROVABLY EQUIVALENT: sha256_of returns
    empty for a missing file, so the existing -n check already rejects that
    case. No test can distinguish it. Equivalent code is a claim of a check
    that is not being made, so it is deleted rather than covered by a test
    that would pass either way. Old and new bodies were compared across all
    four input states -- both-missing, target-missing, match, mismatch --
    and agree on every one.

That deletion is the only non-verbatim function; the other 19 moved
untouched, making this the first fully verbatim extraction of the session.

Ordering is asserted where it is load-bearing: /system must go back to ro
after rw, and +i must be restored after being dropped. Presence assertions
alone would pass with either pair reversed.

UNVERIFIED ON DEVICE -- the highest-stakes of the four enforcer splits now
outstanding, since a failure to source leaves nothing blocking while the
phone looks normal. Over-cap count 20 -> 19.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
2026-08-17 16:30:56 +02:00
9f2e208257 curfew_enforcer: split 367 -> 244 + 156, net layer at 100%
The nine network-curfew functions -- the per-UID iptables allow-list that
cuts every app outside $NIGHT_WHITELIST off the network overnight -- move
into curfew_net.sh. NET_BUILT stays in the enforcer: both files write it,
so it is a genuine shared global rather than a seam violation.

Placed as a FLAT sibling, not under lib/. deploy.sh pushes phone scripts
into one directory, so a lib/ path would simply not exist on the device;
dns_iptables.sh and tether_iptables.sh sit beside their enforcers for the
same reason. Added to BOTH deploy lists, and deliberately not to the chmod
list -- sourced files need no exec bit there, matching the other two.

41 assertions, 60/60 lines, 17 mutations all killed. The one that matters
most flips the 10000-19999 REJECT to ACCEPT, which would silently disable
the entire cut-off while leaving a chain that looks populated. The suite
also asserts rule ORDER, not just presence: every ACCEPT must precede that
REJECT, or the whitelist blocks the very apps it exists to permit.

night_uids and fill_net_chain are reported as non-verbatim by
verify_shell_split.sh, correctly: both ended in `done < file`, a line kcov
instruments and bash never reports, which alone held the file at 96.77%.
Both are now piped. Neither loop reads stdin internally, so the subshell is
immaterial -- unlike pull_apks in backup_capture.sh, where the same change
would reintroduce a known bug. Behaviour was checked rather than assumed:
night_uids emits the same uids, and fill_net_chain emits an identical
11-rule sequence, diffed against the pre-split bodies.

UNVERIFIED ON DEVICE -- joins tether_enforcer.sh and dns_enforcer.sh.
Over-cap count 21 -> 20.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
2026-08-17 16:18:05 +02:00
61be666329 phone_backup: split 333 -> 229 + 114, with the library at 100%
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
The capture half -- dump_packages, pull_apks, pull_sdcard, dump_settings
and dump_focus_state -- moves into lib/backup_capture.sh. SDCARD_DIRS goes
with pull_sdcard, its only reader: left behind it would be a global the
entry script never reads (SC2034), and the hook runs shellcheck without -x
so each file has to stand alone.

This code decides what a phone backup actually contains and had no test at
all. It has one now, against a fake device mocking adb_cmd -- the single
boundary all five functions cross. 22 assertions, 52/52 lines, and 13
mutations all killed. One of those mutations reintroduces the stdin-
draining bug pull_apks' own comment documents (it pulled 1 of 54 packages
before it was fixed); the suite catches it.

Nine of the ten functions moved verbatim per verify_shell_split.sh.
dump_focus_state is the exception and the tool correctly says so: its
`{ ... } >file` group hid a line kcov instruments but bash never reports,
and rewriting it as individual redirects drew SC2129 instead. One printf
satisfies both. Its rendered focus-state.md was diffed against HEAD's
output on the same inputs -- byte-identical.

Same for three multi-line literals (SDCARD_DIRS and two mapfile process
substitutions), which were stuck at 1/N covered for the same reason.

phone_backup.sh is PC-side and in neither deploy list, so no list edits.
--help exits 0; --verify on an empty dir exits 1 naming the four missing
files, matching HEAD exactly. Over-cap count 22 -> 21.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
2026-08-17 16:02:25 +02:00
0f210e5532 monitor: backfill to 100% coverage, fixing two defects it exposed
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
The split shipped verbatim-proven and green, but measuring it afterwards
showed the tests barely touched it: monitor.sh 44.44%, the policy probes
7.69%, and the health probes never loaded at all. Every _check_* decides
whether phone enforcement is reported as healthy, so an untested probe is
an enforcement failure that reports itself as fine.

The mock stubs adb_root_shell at the function boundary rather than putting
a fake adb on PATH: the probes never invoke adb, they call a function from
adb_common.sh, so a PATH stub would never be consulted. Dispatch is by
pattern, not exact string, because the real commands embed awk programs
with escaped $2, nested quotes and 2>/dev/null redirections.

Two defects the new tests found:

  - _mon_escape_json never escaped double quotes. Written as
    ${escaped//"/\"}, bash reads the bare quote as opening a quoted string,
    so the pattern silently degraded to `/\` with an empty replacement. Any
    check message containing a quote produced invalid JSON.
  - monitor_print_format_warning's multi-line array literal and
    collect_snapshot's `{ ... } >file` group held 12 lines that no test
    could ever cover: kcov instruments the continuation lines and the
    closing brace, but bash only reports a statement at its first line.
    Reproduced in an isolated 9-line case before changing anything.

Assertions are mutation-proofed: 18 mutations of the probes' status
decisions, 18 killed. The first run had one survivor -- the repairable
flag was asserted nowhere -- now pinned in both directions.

  monitor.sh                104/104 = 100.00%  (was 44.44%)
  monitor_checks_policy.sh    65/65 = 100.00%  (was  7.69%)
  monitor_checks_health.sh    79/79 = 100.00%  (was     0%)
  test_monitor.sh          82 passed, 0 failed (was 9)

Cases live in two sourced files purely to stay under the 250-line cap;
test_monitor.sh remains the single entry point, so one coverage command
still measures all three subjects. Over-cap count unchanged at 22.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
2026-08-17 15:54:04 +02:00
a174b2a3c4 meta: measurable shell coverage, and dns_iptables.sh at 100%
kcov works here once invoked correctly, so shell coverage is measurable
after all. shell_coverage.sh wraps it as a gate; the XML parsing lives in
shell_coverage_report.py per the rule against inlining another language's
logic in a shell script.

Two kcov traps are documented in the wrapper because both fail silently.
It must be handed the test script DIRECTLY: given `bash script.sh` it
instruments the bash binary, finds no shell source, and reports 0/0 at
0.00% -- indistinguishable from "nothing is covered". And per-line detail
exists only in cov.xml; coverage.json carries the percentage alone, so it
cannot say which lines are missing.

First measurement put dns_iptables.sh at 68/74 = 91.89%, with all six
uncovered lines in error and edge branches: chain creation failing, jump
insertion failing, the stale-jump de-dupe log, and the v6 trusted-resolver
ACCEPT. The stub grew failure injection and jump seeding to reach them,
and four new cases take it to 74/74 = 100.00%.

The gate fails closed: deleting the three ensure_chain failure cases drops
it to 93.24%, names lines 23, 24, 37, 40, 41, and exits 1.

Not wired into pre-commit -- kcov is absent on CI, and most shell files
here have no test at all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
2026-08-17 15:29:08 +02:00
4000c7a2ca dns_enforcer: split 325 -> 171 + 170, and give it a working test
The iptables chain machinery (ensure_chain, trusted_dot_ips,
fill_chain_v4/v6, expected_rule_count, chain_intact, enforce_iptables)
moves into dns_iptables.sh — a sibling, not a lib/ member, because lib/
does not exist on the phone and deploy.sh copies these files flat into
$REMOTE_DIR. Both deploy lists grew the entry, matching how
tether_iptables.sh is carried; it is correctly absent from the chmod 755
line, since a sourced library needs read, not execute.

extract_shell_functions.py refuses to anchor a source line in a
#!/system/bin/sh script, as documented — but when it refuses it still
writes the library and does NOT remove the functions from the source, so
a naive run leaves every function defined twice and the entry script
longer than it started. The excision was done separately; the hash tool
confirms all 14 functions moved verbatim.

test_dns_enforcer.sh is deleted rather than repaired. Its six assertions
targeted extract_ping_ip, extract_package_uid and append_unique_line —
none of which dns_enforcer.sh has ever defined. It died at its source
line and had never run a single assertion since it was added.

Its replacement, test_dns_iptables.sh, covers all seven moved functions
with 20 assertions, staged the way test_tether_enforcer.sh stages its
subject. Three independent mutations were each caught: undercounting the
trusted-resolver ACCEPT, miscounting rules per DoH IP, and moving the
ACCEPT after the blanket REJECT. Shared staging lives in
dns_iptables_harness.sh so both test files stay under the cap.

Not verified on the phone — no deploy has run, by instruction.
Over-cap count 23 -> 22.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
2026-08-17 15:10:10 +02:00
728301c9a4 monitor: split 449 -> 215 + 138 + 136, partitioned by probe kind
lib/monitor.sh was 199 lines over the cap. Its eleven _check_* probes
move into two siblings: monitor_checks_health.sh (format indicators,
battery, storage, the three enforcer daemons) and
monitor_checks_policy.sh (hosts blocklist, DNS chain, launcher,
companion app, boot persistence).

A single monitor_checks.sh was tried first and left at 265 lines — still
over, the net-zero outcome. Two libraries split the cluster at its own
seam and land both comfortably under.

The _MONITOR_* constants are partitioned by reader so no file assigns a
global it never reads. The five read only by the policy probes move into
that file; the ones monitor.sh reads itself or shares between both
children stay put. shellcheck is clean on each file standing alone.

Not a deployment concern: lib/monitor.sh is PC-side, consumed by
run_phone.sh, and deploy.sh never references lib/ — so neither copy list
changes.

Verified three ways: test_monitor.sh reports 9 passed / 0 failed, the
same count as before; verify_shell_split.sh confirms all 25 top-level
functions moved verbatim; and run_phone.sh --help exits 0, proving the
source chain resolves in the real consumer. Over-cap count 24 -> 23.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
2026-08-17 14:59:41 +02:00
2cd62587d6 tether_enforcer: split 267 -> 182 + 103, and grow both deploy lists
First phone_focus_mode split, so it sets the pattern. The nine iptables and
tethering-offload functions move to a SIBLING file, not a lib/ member:
deploy.sh copies a hardcoded per-file list into REMOTE_DIR and never deploys
lib/, so anything sourced on the phone has to sit beside the entry script.

deploy.sh has two hardcoded lists, not one - an adb push into the staging
dir and an adb_root cp out of it. Both were updated; missing the push would
have left the copy pointing at a file that was never staged.

The test harness copies only the entry script into its scratch dir, so it
was taught to copy the sibling too. That is the same coupling as the phone
deployment, in a form that can actually be run - and running it is the
evidence: 17 passed, 0 failed, identical to the pre-split baseline, with the
daemon sourcing the new sibling exactly as it will on the device.

This script is #!/system/bin/sh with no `set -e`, so the extractor refused to
guess an anchor rather than placing the source line somewhere wrong. It went
after the existing config.sh source by hand.

All 20 functions verbatim. Not verified on the phone - the next deploy is
where that gets confirmed.

Over-cap files 25 -> 24.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
2026-08-17 14:38:53 +02:00
2c95425a60 steam_compatibility: split 663 -> 140 + 199 + 156 + 124 + 103
Grouped by the state each function touches rather than by size:

  steam_requirements.sh  199  requirement parsing, scoring, and the hardware
                              detection that feeds it
  steam_report.sh        156  the run loop and the cache it reads
  steam_api.sh           124  library discovery, Steam and ProtonDB calls
  steam_credentials.sh   103  credentials, usage text, presentation helpers

The grouping was forced by the SC2034 writer/reader rule, not chosen. Two
arrangements were discarded: detect_system alone in a lib writes the three
SYSTEM_* globals without reading them, and load_cache_map alone writes
CACHE_MAP without reading it. Both were fixed by putting the writer next to
its reader - detect_system beside score_game, load_cache_map beside main.

Verified: all 33 functions verbatim; every lib passes shellcheck standalone
with no suppression; and the script actually runs - `--help` prints the usage
block and exits 0, which is what proves the four source lines resolve and
main still dispatches.

Over-cap files 26 -> 25.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
2026-08-17 14:32:41 +02:00
76e73830f6 shell: split clean_audio, enforce_vbox_hosts and migrate_hosts_guard
clean_audio.sh          419 -> 230 + 204
  enforce_vbox_hosts.sh   443 -> 220 + 236
  migrate_hosts_guard.sh  444 -> 167 + 162 + 141

migrate_hosts_guard needed two libraries: with all ten work functions moved
the single lib was 295 lines, so migrate and rollback were separated.

The extractor's brace matcher required index > start and so silently skipped
one-line `warn() { ...; }` definitions. Fixed, and regression-checked against
the committed fresh-install/main.sh split byte-for-byte.

The harness earned its keep on a fourth file: adding `export` to libre_translate
to satisfy SC2034 changed two function bodies, and verify_shell_split.sh caught
what lint had accepted. Those exports were reverted, and libre_translate.sh is
parked - what remains after its clean seams both assigns and reads the same
globals, so getting it under the cap needs parse_args restructured rather than
relocated, which is a refactor rather than a verbatim move.

That cost is now written down: a lib must contain the readers of every global
it assigns. The precedent at install_pacman_wrapper.sh:29 is about
definitions-only libs, not about assignments as such, and the pre-commit hook
runs shellcheck with no -x, so a source= directive will not make it follow the
link. This is the shell analogue of the monkeypatch-target trap.

Verified: all 8, 13 and 19 functions verbatim; all four libraries source with
no side effects; shellcheck passes standalone on every file with no
suppression; bash -n passes on all seven. None of the three was executed -
they drive ffmpeg over real media, mutate VirtualBox guest disks, and rewrite
systemd units.

Over-cap files 29 -> 26.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
2026-08-17 14:27:57 +02:00
efd7f022fd fixes/utils: split nvidia_troubleshoot 336 -> 169+181, passwordless 374 -> 195+193
Two single-use scripts with no deployed copy, so the entry+lib shape is
safe. Each keeps the ordering, the safety checks and the user-facing output;
the configuration writers move to a library beside it.

Both scripts write `set -e # Exit on any error`, so extract_shell_functions.py
now matches the directive rather than the whole line. Regression-checked by
re-deriving the committed fresh-install/main.sh split byte-for-byte.

Verified: all 6 and all 9 top-level functions verbatim via the shfmt
harness; both libraries source with no side effects and define every
function moved into them; bash -n passes on all four.

Neither script was run. nvidia_troubleshoot.sh writes /etc/X11 config and
installs pyroveil; setup_passwordless_system.sh writes sudoers and lightdm
autologin, which is what this machine's login depends on.

Over-cap files 31 -> 29.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
2026-08-17 14:18:06 +02:00
dbf2f29b00 _transcribe_diarize: split 356 -> 164 + 143 + 105
The last Python file over the cap:

  _transcribe_media.py    143  _try_import, both duration probes,
                               get_media_duration, ffmpeg transcode, cleanup
  _transcribe_cluster.py  105  _resample_linear, _kmeans_cosine and its
                               iteration loop
  _transcribe_diarize.py  164  the pipeline: audio loading, the classifier,
                               embedding extraction, diarize_segments

The graph is a tree: _transcribe_media imports nothing local, _cluster takes
only _try_import from it, _diarize imports from both. All 13 top-level
definitions verified AST-identical to HEAD, and an AST scan confirms the
three module constants travelled to the modules that use them.

transcribe_fw.py now imports get_media_duration from _transcribe_media
rather than through _transcribe_diarize. Ruff removed that re-export as
unused and broke the import; running the program is what caught it, and
naming the owning module is the correct fix under the no-barrels rule.

Verified: transcribe_fw.py /nonexistent.wav still exits 2 and --help exits
0; 63 tests pass at 100% line and branch on all three modules; 338 pass in
the directory. Over-cap files 33 -> 32, and no Python file is over the cap
any more - the remaining 32 are 29 shell, 2 kotlin, 1 dart.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
2026-08-17 13:59:39 +02:00
fb51e991fe _transcribe_diarize: cover 0% -> 100% before splitting
The largest untested Python file left in the repo, at 356 lines against the
cap. Tests land first so the split that follows is verified against tests
that predate the seam.

63 tests: duration probing through both ffmpeg paths, the transcode fallback
and temp-file cleanup, audio loading's three outcomes, linear resampling,
cosine k-means, the ECAPA classifier loader, per-segment embedding
extraction, and diarize_segments end to end.

numpy is real here, not a double. It is a declared dependency, and the core
of this file is clustering maths - assertions against a mocked numpy would
prove nothing about whether two speakers actually get two labels. The
_try_import seam passes numpy through while stubbing torch and speechbrain,
so the tests still run in CI with neither installed. The clustering tests
assert real properties: two opposed groups separate, magnitude is ignored
because the metric is cosine, and a fixed seed reproduces.

The last three coverage gaps were _load_audio's ffmpeg fallback in both
outcomes, the minimum-slice widening, and the zero-iteration k-means branch.

Split across six test modules after two intermediate arrangements went over
the cap, and one move silently broke a test by separating it from its
doubles - caught by the test count holding at 63 through every rearrangement.
338 tests pass in the directory, up from 275 by exactly the 63 added.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
2026-08-17 13:54:04 +02:00
bda24b6305 transcribe_helpers: split 289 -> 177 + 142
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Python tests / test (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
Split along the natural seam now that the file is fully covered:

  _transcribe_probes.py  177  _try_import, the four dependency probes,
                              generate_sine_wav, prepare_model, test_cuda
  transcribe_helpers.py  142  the eight _handle_* functions and main()

All 18 top-level definitions verified AST-identical to HEAD, and an AST
scan confirms neither module references a name it does not bind.

transcribe.sh is untouched - it invokes tools/transcribe_helpers.py by
path, and that path still exists with the same name and executable bit.
The CLI tests' patch targets did not move either, because main() dispatches
to names imported into its own namespace.

Verified by running every subcommand and reading the exit status directly,
since those codes are the contract transcribe.sh branches on:
python-version 0, check-faster-whisper 7, check-ctranslate2 1,
check-diarization 0, deps-installed 0; generate-wav writes the same
96044-byte WAV as before the split. 41 tests pass at 100% line and branch
on both modules; 243 pass in the directory. Over-cap files 35 -> 34.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
2026-08-17 13:33:38 +02:00
7587cd9604 transcribe_helpers: cover 0% -> 100% before splitting
transcribe_helpers.py is 289 lines against the 250-line cap and was
completely untested. Tests land first, in their own commit, so the split
that follows is verified against tests that predate the seam.

Left at its current path rather than moved under python_pkg/: transcribe.sh
invokes it by path inside a dedicated ML venv, and transcribe_fw.py imports
two siblings by bare name, so the closure is five files (1236 lines), not
three. Tests go in linux_configuration/tests/ instead, whose conftest
already puts standalone script directories on sys.path - the same
arrangement the usage_report work used, and still collected by CI.

The ML dependencies are never installed in CI and the module reaches them
only through _try_import, so patching that one seam covers every probe in
both directions. An AST scan at every nesting level confirmed there is no
other path to them: the only direct numpy imports sit under TYPE_CHECKING.

41 tests, 100% line and branch. The last gap was the dispatch guard at
line 284, unreachable through the CLI because argparse's choices list and
the dispatch dict carry the same eight commands; covering it by admitting a
ninth choice also documents why the guard is there.

Split across four test files because a single module hit 291 lines - over
the cap this work exists to clear.

Verified: python-version prints 3.14, check-faster-whisper exits 7 with no
ML deps present, generate-wav writes a 96044-byte WAV; 243 tests pass in the
directory, up from 202 by exactly the 41 added.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
2026-08-17 13:29:07 +02:00
377a352d48 vscode_optimizer: split _optimize.py 498 -> 100 + 5 modules
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Python tests / test (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
The last python file over the 250-line cap. Now that it sits at 100% line
and branch coverage, split along its natural seams:

  _types.py     48  the three dataclasses, so nothing imports a sibling
                    merely to name a type (which would have made a cycle)
  _render.py    69  the ANSI palette and the printers
  _config_io.py 104 variant discovery, JSONC parsing, reads and writes
  _hardware.py  106 _run and the four probes
  _plan.py      156 the pure hardware -> proposed-changes functions
  _optimize.py  100 the CLI entry: _apply_variant and main

Definitions were moved by line range rather than retyped. A throwaway
ast.dump identity check against HEAD confirms all 26 top-level definitions
are unchanged - formatting-blind, logic-sensitive.

The first attempt raised NameError: name '_BO' is not defined at runtime;
_BO was used by main() but missing from the _render import list. Rather than
fix that one failure alone, an AST scan over all six modules for names used
but not bound confirmed nothing else was missing.

Test imports now name the owning module; no re-export barrels. One patch
target genuinely moved - the guard-rail test patches _config_io.Path, since
that is the namespace resolving the name when _discover_variants is called
directly rather than through main().

Verified: same 98 tests pass with every module at 100% line and branch;
python3 -m python_pkg.vscode_optimizer --dry-run reproduces the same
18-change plan and writes nothing; repo-wide pytest 1846 passed at 100%;
over-cap files 36 -> 35.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
2026-08-17 13:15:50 +02:00
9d12c03ccc vscode_optimizer: move under python_pkg and cover 0% -> 100%
meta/scripts/optimize_vscode.py was 498 lines against a 250-line cap and
completely untested, and at that path the repo-wide gate (--cov=python_pkg,
fail_under = 100) could not see it. Moved verbatim to
python_pkg/vscode_optimizer/_optimize.py - git records a pure rename, and
the content is byte-identical to HEAD.

The move and the tests are one commit by necessity: any intermediate state
with the module under python_pkg/ but uncovered turns every pytest run in
the repo red, including ci-mirror on pre-push for unrelated work.

Running it by path no longer works after the move (sys.path[0] would be the
module's own directory), so __main__.py replaces it, following the existing
focus_policy convention rather than a sys.path insert. Nothing invoked the
old path: grep over .sh/.desktop/.service/.yml/.py found zero callers.

98 tests reach 100% line and branch coverage. Patching is confined to real
boundaries - subprocess.run, the module-level Path, builtins.input, sys.argv
- so the forthcoming split has few monkeypatch targets to re-point.

Writing the tests surfaced a pre-existing bug, left unfixed here and pinned
by a strict xfail: _VENDOR_KW's "ati" key is a substring of "VGA compatible
controller", which appears in every lspci display line, so it matches before
"intel" is ever tried and Intel iGPUs are misreported as AMD.

Verified: python3 -m python_pkg.vscode_optimizer --dry-run reproduces the
pre-move 18-change plan and writes nothing; the real settings.json is
untouched; repo-wide pytest is 1846 passed at 100%; over-cap files 37 -> 36.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
2026-08-17 13:10:10 +02:00
ff1a1b6a0e usage_report: split usage_report.py 426 -> 101 + 195 + 117 + 68
The entry point now holds only the parser, mode dispatch and main. Three
modules take one concern each: _usage_report_logs (where the logs are,
the state file, per-day segment planning), _usage_report_emit (preflight,
stdout, clipboard), _usage_report_run (the two run paths and the
aggregation they drive).

Verified by the 100%-coverage tests from the previous commit. An AST
comparison of all 22 top-level defs and classes reports 22 identical, 0
mismatches.

All four modules hold 100% line+branch. 204 tests before and after. Both
CLI modes were run against the machine's real logs, which is the check
the unit tests cannot make: they stub the collaborators, so only a real
run proves the new import graph resolves.

Two lessons, recorded because the next target is a 498-line file:

This cut was too big. Two names had to be relocated after their first
placement created an import cycle, and four modules' worth of monkeypatch
targets needed re-pointing. A smaller first slice would have been cheaper.

Patching a moved function must target the module whose namespace resolves
it - the caller's, not the one it was moved to. Getting that wrong on
_run_since made main() call the real function, which read the actual atop
logs and hung the suite rather than failing.

Three import bugs (Path and _fmt_h left under TYPE_CHECKING but used at
runtime, dataclass missing where _Aggregates landed) were caught
immediately by the coverage from the previous commit.

Over-cap 38 -> 37.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
2026-08-17 12:46:06 +02:00
ec67387c07 usage_report: cover usage_report.py 47% -> 100%
25 tests in four files, no production change - the module is
byte-identical to HEAD. Lands before the split for the usual reason.

Covers the CLI surface (every default, every flag, main's dispatch), both
run paths including the shared empty-window-means-a-full-day fallback,
_run_since's no-data path and both state-advance branches, the
clipboard/emit side (tool selection, a failing tool, no tool at all),
_preflight's three outcomes, _aggregate_segments with a gap day and with
a present-but-empty log, and _log_descriptions.

Also covers _compute_window, which turns out to be dead: a shim
documented as "kept for backwards import compatibility" with zero callers
anywhere in the repo. Tested rather than deleted here, since removing it
is a behaviour change that does not belong in a coverage commit - but it
is a deletion candidate.

185 stmts / 44 branches, 0 missed. Directory: 179 -> 204 passing.

No suppressions: mypy rejected a `list.append(...) or 0` lambda, replaced
with a real closure; ten ruff S108 findings on hardcoded /tmp paths became
/nonexistent sentinels, which is more accurate anyway since those paths
are never opened.

Written as two files of 273 and 282 lines, then split to four under the
cap before committing - the DropZone lesson from earlier today.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
2026-08-17 12:21:21 +02:00
b5dbe1e67a usage_report: split _usage_report_parsing.py 425 -> 243 + 206
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Python tests / test (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
Lifts the atop subprocess machinery into _usage_report_atop.py: running
atop, streaming its parseable output, building/caching the atop_agg C
helper, and reading its TSV. What stays is record parsing, epoch windows
and per-day merging.

Verified by the 100%-coverage tests from the previous commit. An AST
comparison of all 16 functions against HEAD reports 16 identical, 0
mismatches.

_usage_report_render.py imported _run from the parsing module; it now
imports from the module that owns it. That is the same re-export barrel
the last split shipped and had to fix in a follow-up - caught in the same
commit this time. usage_report.py needed no edit at all.

Both modules hold 100% line+branch (94/28 and 130/34, 0 missed). 179
tests before and after; the five new test files collect 8+6+4+14+5 = 37,
which is last commit's 36 plus the one re-homed out of
test_usage_report_modes.py, where it referenced a constant that had
moved out from under it.

The two test files added last commit were themselves over cap (373, 272)
so they are split five ways here - the DropZone lesson from earlier
today, where a lint fix silently added an over-cap file.

Ran the program: a real report from today's logs, through the native
atop_agg path, which is precisely the code this commit moved.

Over-cap 39 -> 38.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
2026-08-17 12:12:11 +02:00
e165ae8987 bucket_catch: 45 eslint errors -> 5, no suppressions
These were unmasked, not caused, last session: `lint` is
`tsc --noEmit && eslint src`, and a broken tsc had been short-circuiting
the && so eslint had never run on this package.

The binding constraint is the 100% coverage gate, not the linter. Most
idiomatic fixes for no-non-null-assertion add a branch that no test can
reach, which trades a lint error for a coverage failure - and the
no-suppressions rule forbids escaping either way. So every fix here is
one that adds no branch:

- no-non-null-assertion 18 -> 2: a waitFor already proves result.current
  is non-null, so capture it into a local instead of asserting on each
  read. App.tsx's files[0]! was pure noise (the same position also
  reported no-unnecessary-type-assertion). usePuzzleGameLoop's shift()!
  became index-then-shift, since the while condition already indexes [0]
  unasserted.
- non-nullable-type-assertion-style 9 -> 0: the rule-swap trap - eslint
  asks for `!`, which the other rule forbids. Narrowed with instanceof.
- unbound-method 10 -> 1: keep the MockInstance vi.spyOn already returns
  rather than detaching the method. This surfaced a latent bug: one test
  calls restoreAllMocks() mid-test, so the beforeEach handle was stale.
- no-unnecessary-type-assertion 5 -> 1, require-await 1 -> 0.

Coverage held at 100%: 584/217/98/535 -> 585/217/98/536. The +1 statement
and +1 line are the index-then-shift split; branches unchanged at 217 is
the number that proves no fix added an untested path. 145 tests, tsc 0.

The 5 remaining are reported, not silenced. App.tsx's fileResult! needs
the phase state to carry the result ({ kind: "done"; result }) - three
smaller structures all failed the same way. The rest are documented in
the evidence file.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
2026-08-17 11:48:45 +02:00
ad646541ad usage_report: split _usage_report_render.py 310 -> 181 + 123 + 24
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Python tests / test (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
Verified by the 100%-coverage tests from the previous commit, written
before this seam existed.

_cpu_table/_dedupe_ram/_ram_table/_gpu_table move to
_usage_report_tables.py with the two constants they own. _md_escape and
_fmt_h move to _usage_report_format.py: both the tables module and the
render module need them, so a third module is what avoids a cycle.
Import sites name the owning module; no re-export barrel.

usage_report.py needed no edit - it imports _fmt_h and _render_report
from the render module, and both names still resolve there.

Evidence the move was verbatim: an AST comparison of all 10 functions
against HEAD reports 10 identical, 0 mismatches. That matters because I
retyped rather than cut-and-pasted, and did briefly introduce a literal
× where the original had × - ruff's RUF001 caught it.

Coverage stays 100% line+branch on all three (15/55/54 stmts, 4/10/14
branches, 0 missed). Test count 43 before and after, summed across the
five files it now lives in - the check that matters for a test split,
since a file outside the runner's glob is silently never collected.
Directory total 143 passed, unchanged.

Ran the real program: a full report from today's atop + pmon logs, every
section rendering through the new boundaries including the RAM sibling
dedupe and its (+20 more) truncation.

Over-cap 41 -> 39. No suppressions; a shared test-helper module was tried
and abandoned because name-tests-test would have needed an exclusion.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
2026-08-17 11:34:08 +02:00