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>
The pacman-wrapper installer copied install_leechblock.sh to
/usr/local/share/digital_wellbeing/ along with three data files, but no
lib/. After the split in 1dceca66 the deployed entry script sources
every install phase from lib/ beside itself, so deploying it without
that directory leaves a copy that dies on its first `source`.
This is not hypothetical: the live deployed copy was found in exactly
that state at the start of this work -- the previous session's split
entry script, deployed and then left behind when the repo was reverted.
It failed on every invocation. It has been repaired and now carries the
libs.
The glob is leechblock_*.sh rather than *.sh because lib/ is shared with
unrelated music_* helpers that have no business in /usr/local. The copy
mirrors rather than merges, so a lib deleted upstream does not linger.
Verified against a fake root and then for real: the deployed copy runs,
its sha256 matches the repo, and inject_default_config resolves the
defaults JSON and the seeder from the deploy root rather than from lib/.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Four */lib/tests/run_all.sh runners existed — periodic_background, hosts,
single_use/utils and the new digital_wellbeing one — and no workflow ran any
of them. Every assertion the cap-split sessions wrote was executing only on
the author's machine.
Discovered rather than listed: each split adds another runner, and a
hardcoded list is what let the first four go unnoticed. The step fails if
discovery matches zero runners, so a future path change cannot quietly turn
this into a vacuous green step.
Verified by running the step's exact logic under bash: 4 runners discovered,
all exit 0. Checked for Arch-only dependencies first — the only `command -v`
probe in the harnesses is `unshare` (present on ubuntu-latest, and guarded),
and no suite touches pacman, chattr, systemctl or adb, so these belong in the
Ubuntu job rather than the Arch container.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F3PzGc75gBcdxP3PkHXGcV
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
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>
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
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
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
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
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
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
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
Extracts the two AlertDialog builders and the build() body-selection
logic into a new status_page_dialogs.dart part file, following the
same part-of-main.dart split pattern status_body.dart already used.
Pure extraction, no behavior change -- verified with dart analyze and
the full flutter test suite (94/94 passing).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
The rsvg-convert fix landed in a commit that only touched workflow
files, which python-tests.yml's path filter doesn't watch -- so the
workflow never re-ran and gh run rerun replays the old, pre-fix
definition instead of the current one. Adding the workflow's own
path to its filter and workflow_dispatch closes both gaps.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
ubuntu-latest lacks librsvg2-bin, so test_render_writes_png was
skipped in CI, leaving vector.py:156 uncovered and both Pre-commit
checks and Python tests red at 99.98% (1 line miss). Installing the
real dependency lets the test run instead of excluding the line.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
Regression from this session's earlier commit (327d48b): the test still
referenced pacman-hooks/*.sh and install_pacman_hooks.sh, which that commit
archived to testsAndMisc-archive. shell-tests.yml runs this test directly
(not scoped by pre-commit's --files), so it went red on CI without being
caught locally before the push.
Dropped the assertions tied to the archived files; kept and extended the
ones checking pacman_wrapper.sh's guard-lib fallback logic, which was
already fully guard-lib-native and unaffected by the archive.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
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
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
Evaluates github.com/RyanAlberts/best-of-Agent-Harnesses (160 projects)
against the existing ~/.claude/ setup: hooks, skills, session-autopsy
pipeline, and prior independent arrival at the token-spend thesis.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
Full history preserved via git filter-repo, including both pre-reorg
root paths (libre_translate.sh and the subtree-imported
Bash/libre_translate.sh) since both were genuine ancestors of the
current file. Live at
github.com/kuhyx/testsAndMisc-archive/tree/main/linux_configuration/scripts/single_use/misc/testsAndMisc-bash.
Asked the dead-code check-in before any split work started this time,
per the lesson from the plagiarism-tools session. NEXT_SESSION_PROMPT.md
now points at diagnose_pacman_hook_stall.sh.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
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
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
Three of the old brief's own instructions did not survive contact:
- the config.sh fingerprint recipe CANNOT WORK. Four policy fields are
frozenset, so json.dumps(default=str) serialises them in per-process
iteration order; three runs gave three digests. Replaced with the repo's
own deterministic exporter (baseline 83e05e82) AND a diff of the sourced
environment, because the hash alone passed clean through both bugs hit
this session.
- extract_shell_functions.py does NOT reliably place its source line. It
got the position wrong three ways: before SCRIPT_DIR existed, before
STATE_DIR (every path then expanded against an empty prefix), and on a
nested re-source instead of the top-level one.
- "100% line coverage, always" now has a measured exception, and stating
it as an absolute is actively dangerous. daemon_apps.sh has five
uncovered `done <file` lines whose loops call pm; a reader who pipes
them -- as this session correctly did in curfew_net.sh, where the body
is pure -- reintroduces a measured 1-of-54 bug in the code that decides
which apps get disabled at home. It is now a PROHIBITION at the top of
the file, with the line numbers and mapfile as the only alternative.
Also new: the kcov artifact taxonomy with an explicit safe/unsafe column,
the mutation tooling and the rule that an equivalent mutation means dead
code to delete rather than a test to write, and the two hazards
phone_focus_mode now carries (config.sh at exactly 250 with fifteen
loader-pinned variables enforced only by a comment; deploy.sh's two
hardcoded lists now living in two files).
Two numbers were checked rather than recalled, and both were wrong in the
draft: 120 mutations across six specs, not 127; and TWO copies of the
Haversine, not three -- daemon_location.sh's calc_distance IS focus_daemon's
original, moved, which I had double-counted in an earlier commit message.
Running two mutation specs concurrently leaves a mutation applied and reads
as a survivor; that cost a false 16/17 here and is now written down.
295 -> 250 lines. The old brief counted itself among the over-cap files and
was 45 lines past the gate that never checks .md.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
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
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
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
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
Four evidence files committed today cite mutation runs -- 18, 13, 17 and
17 killed -- that lived in a scratch directory. Evidence pointing at a run
nobody can repeat is not evidence, so the lists move into the repo behind a
spec-driven runner.
Mutations are literal find/replace strings in JSON, not sed expressions.
The shell version this replaces escaped each pattern twice and used | as
its field separator, which collided with || and reported six mutations as
SURVIVED when they had never been applied at all -- a false negative in the
one tool whose entire job is catching false negatives.
Two deliberate choices follow from that:
- a no-op (find matched nothing) counts as a FAILURE, not a skip. A stale
spec entry otherwise stops testing its behaviour without saying so.
- a timeout counts as killed: a mutation that hangs the suite has still
been detected by it.
The conversion itself exercised the first rule -- it reported 8 no-ops from
mangled escapes rather than quietly scoring them as killed. Each was fixed
against the real source line.
All four specs now reproduce their committed figures exactly, and the
working tree is byte-clean after every run.
Not wired into pre-commit: it runs the whole suite once per mutation, which
is minutes, not seconds.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
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
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
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
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
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
On a script with no `set -e` anchor -- every #!/system/bin/sh phone
enforcer -- the tool wrote the library, then exited on the missing
anchor, leaving the functions in the entry script as well. The result
defined each moved function twice, lints clean, and passes a
function-body hash check, so nothing downstream catches it. It cost a
split of dns_enforcer.sh this session: 325 lines in, 331 lines out.
Both writes now happen after the anchor search. A missing anchor is a
warning naming the exact line to paste, not an exit, so the move still
completes. Added a shrink guard that refuses to write when the entry
script would not get shorter, and an explicit error when the selected
function set is empty.
Verified both paths: dns_enforcer.sh (no anchor) warns and still drops
171 -> 158 with the functions gone, phone_backup.sh (anchored) drops
333 -> 264 with the source line auto-inserted as before.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
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
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
The shell-splitting work has been referring to check_file_length.sh --all
as its per-commit gate. No such script was ever committed: it is absent
from the working tree and from every commit in history, so each split so
far was checked by eye.
Cap is 250 lines, source extensions only (sh/bash/zsh, py, kt/kts, dart,
ts/tsx/js/jsx). Markdown, HTML, JSON, YAML and lockfiles are out of
scope, as are third_party/ and docs/superpowers/.
--all reports exactly 24 files over the cap, independently reproducing
the count the brief carried. That agreement is what confirms the
extension scope is the intended one.
Deliberately NOT wired into .pre-commit-config.yaml: 24 files are still
over, so a hook would block every commit. It becomes a hook once --all
exits 0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
_detect_gpu matched every _VENDOR_KW key against the whole lspci line.
Every display line contains "VGA compATIble controller", so the "ati" key
fired unconditionally and "intel" was never reached: Intel iGPUs were
reported as AMD, switching on the GPU-accelerated terminal and the
AMD-only Electron flags.
Match against hw.gpu_model — the description after the last colon, which
the line above already extracts — instead of the whole line.
The strict=True xfail guarding this is removed and becomes a normal
passing regression test, and the two parametrised cases that
characterised the bug (Intel -> AMD, unknown vendor -> AMD) are corrected
to Intel and Unknown. Added this machine's real second display device,
"[AMD/ATI] Raphael", which must still resolve to AMD.
Verified by real run: --dry-run still reports the RTX 3090 as NVIDIA with
24576 MB VRAM and still proposes 18 changes, matching the pre-fix
baseline. Full gate: 1984 passed, 100% coverage.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
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
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
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
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
Generalises the method proved on fresh-install/main.sh. Line-range slicing
is the obvious approach and it is wrong: these scripts interleave top-level
commands between their function definitions, so a range slice sweeps them
into the library where they run at source time and out of order. This walks
the file brace-by-brace, lifts only the function blocks, and leaves every
other line where it was.
Verified by re-deriving the already-committed main.sh split in a throwaway
worktree at HEAD~1: both files come out byte-for-byte identical to the
hand-made version that passed the shfmt harness, the structural flow
comparison, and the source-time side-effect check.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
The five package-installation functions move to lib/packages.sh; main.sh
keeps the top-level flow and sources the library after `set -e`.
The first attempt sliced by line range and was thrown away. This file
interleaves top-level commands - sudo -v, git config, the cp of
makepkg.conf and pacman.conf - between its function definitions, so a range
slice swept them into the library, where they would have run at source time
and in the wrong order. Sourcing that version blocked on a sudo password
prompt, which is how it was caught. The rebuild extracts function blocks by
brace counting and leaves every other line exactly where it was.
Verified with the new shfmt harness (all 5 functions verbatim), by a
structural comparison of the top-level flow showing exactly one line added
and none missing, and by sourcing the library to confirm it defines all five
functions with no side effects. The script itself was NOT run: it does
`sudo pacman -Sy`, overwrites /etc/makepkg.conf and /etc/pacman.conf, and
installs AUR packages.
Over-cap files 32 -> 31.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
The five Python splits this session were made safe by a throwaway ast
identity check. There is no ast for bash, so meta/scripts/verify_shell_split.sh
does the same job with shfmt -mn (minify: strips comments and indentation,
keeps logic), hashing each top-level function and naming the one that differs.
shfmt is already a REQUIRED_TOOLS binary, so this adds no dependency.
Validated in all three directions before trusting it: an unchanged copy is
IDENTICAL, a copy with blank lines inserted is IDENTICAL, and a one-character
logic change ($1 -> $2) is caught and named. The first mutation attempt used
sed 's/exit 1/exit 2/' on a file with no 'exit 1' and reported IDENTICAL - the
harness was fine, the test was vacuous. That trap is now documented, because a
normalizer that silently passes everything is worse than none.
Also triaged all 29 over-cap shell scripts for deployed copies, which the
trace harness cannot see. The finding that changes the plan: deploy.sh copies
a hardcoded per-file list into /data/local/tmp/focus_mode and never deploys
lib/, so an entry+lib seam would break focus mode on the phone at its first
source - for 8 of the 29, not the 2 the brief named. Those scripts source only
config.sh, which is on the copy list, so a sibling-file seam works provided
deploy.sh's list grows in the same commit.
16 scripts have no deployed copy and are safe to split conventionally.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
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
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
_transcribe_progress.py 91 _run_progress_loop, _format_progress_line,
and the two constants only they use
transcribe_fw.py 236 argument parsing, model loading, diarized
output writing, main
All 8 top-level definitions verified AST-identical to HEAD, and an AST scan
confirms neither module references a name it does not bind - which is what
caught that the two progress constants and the hhmmss import had to travel
with the loop. transcribe_fw.py no longer imports time.
The progress tests now patch _transcribe_progress, including its sys.stderr
and time references, since that is where the loop resolves them.
_resolve_device_and_compute stayed behind, so its test still points at
transcribe_fw.
Verified: --help prints the full usage and a missing input returns exit 2,
the code transcribe.sh branches on; 32 tests pass at 100% line and branch on
both modules; 275 pass in the directory. Over-cap files 34 -> 33.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY