CLAUDE.md becomes a symlink to AGENTS.md, matching the generic-agent
naming other tools expect. .gitignore picks up the CocoIndex Code
cache directory.
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
Records the user's two standing decisions -- 100% line coverage always,
and keep splitting blind without a phone deploy -- and reorders the queue
so the monitor.sh coverage backfill comes first.
kcov works, so the brief now leads with how to measure and with the two
ways kcov fails silently: it must be handed the test script directly, and
per-line detail exists only in cov.xml.
Also corrects the previous brief where measurement contradicted it. The
claim that focus_policy's tests would catch a bad config.sh split is
false -- the pytest hook is types:[python], ci-mirror only covers changed
packages, and those tests use synthetic tmp_path fixtures rather than the
real file. A parse-fingerprint baseline replaces it. The
extract_shell_functions duplication warning is dropped, since the tool
was fixed. Ten over-cap files the brief has never covered are listed
explicitly as out of scope rather than left ambiguous.
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
The user has decided all three of the questions the previous brief left
open: fix the _VENDOR_KW Intel bug, split the phone_focus_mode scripts
despite the deployment coupling, and restructure the three parked files.
Records them as instructions rather than open questions, with the specific
facts each needs.
For the phone splits, accepting the risk does not mean skipping the copy
lists: deploy.sh has two hardcoded per-file lists and never deploys lib/, so
the brief now spells out that both must grow and that the seam is a sibling
file. dns_enforcer.sh and lib/monitor.sh are called out first because both
have runnable tests, so those splits can be proved the way tether_enforcer
was. config.sh is flagged separately - focus_policy/loader.py parses it.
For the restructuring, notes that verify_shell_split.sh no longer applies
once code stops being a verbatim move, so each of the three needs a test or
a real run instead.
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
Python is done: every .py is under the cap. Over-cap 37 -> 25.
Rewrites the brief around what this session learned rather than restating
the old method: the two scripts that now do the mechanical half
(extract_shell_functions.py, verify_shell_split.sh, both validated before
use), the SC2034 writer/reader rule that decides where a shell seam can
fall, and the deployment triage - deploy.sh copies a hardcoded per-file list
and never deploys lib/, which rules out an entry+lib seam for 8 scripts.
Records the three parked files with the reason each resisted, so the next
session does not repeat four failed attempts on libre_translate, and carries
forward the vscode_optimizer _VENDOR_KW bug as pending a user decision.
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
transcribe_fw.py is 302 lines against the cap and was untested. Tests land
first, in their own commit, so the split that follows is verified against
tests that predate the seam.
32 tests: argument parsing and its FW_* environment defaults, device and
compute resolution, the progress line, the transcription loop, model
loading, diarized output writing, and main()'s three exit paths.
faster_whisper is reached only through _try_import, so patching that seam
covers every path without the package installed. _format_progress_line is
tested directly rather than through the loop - it is pure, so the percentage
clamp, the ETA, and the suppression of an ETA longer than a day are each
assertable without a model.
The last coverage gap was _try_import itself, which every other test
patches; it is now exercised directly in both directions.
Split across three test files because a single pipeline module reached 321
lines - over the cap this work exists to clear. 275 tests pass in the
directory, up from 243 by exactly the 32 added.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
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
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
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
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
Documented as "kept for backwards import compatibility", it had zero
production callers - only its own two tests. Removed the function, those
two tests, and the pathlib.Path import that only its signature used.
_SEC_PER_DAY stays: two live callers remain at _usage_report_run.py:135
and :181, so only the function was dead, not the constant.
Verified: usage_report.py --help exits 0 (the unit tests stub subprocesses,
so only a real run proves the import graph), 202 tests pass, and
_usage_report_run.py holds 100% line+branch coverage after removing both
the code and the tests that covered it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
Rewrites NEXT_SESSION_PROMPT.md against what is actually left: 37 files
(29 shell, 4 python, 2 kotlin, 1 dart) rather than the 40 it described,
with the system-maintenance python now finished.
Also drops it from 260 lines to 155, so the brief is no longer itself
over the cap it tracks. Over-cap 37 -> 36.
Carries forward the five traps that cost real time this session -
especially that a monkeypatch aimed at the wrong module made main() call
the real _run_since, read the machine's atop logs and hang the suite
rather than fail - and records the two shell blockers with the specific
thing that breaks and the order to do it in.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
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
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
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
36 tests, no production change - the module is byte-identical to HEAD.
Lands separately from the split that follows, because tests written in
the same commit as a split are written against the post-split shape.
This one mattered more than the last: the uncovered 61% was almost
exactly the code a split would extract - the subprocess plumbing and the
native atop_agg path. Splitting that first would have proved nothing.
_atop_agg_binary had no tests at all; all five outcomes are now covered
(fresh cache, no compiler, make raising, make exiting 0 with no binary,
successful build + chmod). The fresh-cache test asserts by installing a
subprocess.run that fails if called. Every subprocess is faked - no test
spawns atop, make or cc.
Also covers _run's two error paths, _iter_atop_lines with and without a
stdout pipe, the native N/W TSV rows including malformed ones,
_parse_name's six paths, _parse_prm, _window_from_epochs' median
interval, _fold_pid_aggregates, and aggregate_atop end to end.
219 stmts / 62 branches, 0 missed. Directory: 143 -> 179 passing. No
suppressions - two mypy exit-return findings were fixed by typing the
fakes' __exit__ as None rather than silencing them.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
The eslint commit added must() to DropZone.test.tsx, which pushed that
file from exactly 250 lines to 256 - so it removed lint errors while
silently adding an over-cap file. Moving the helper to
src/test/domQueries.ts drops it to 229 and takes the session's over-cap
count to its intended 41 -> 39.
src/test/ is the right home: coverage excludes that path, which is why
the fixtures already live there. PuzzleResult.test.tsx now uses the same
helper instead of its own inline instanceof guard.
145 tests, coverage 100% with totals unchanged at 579/217/98/530, eslint
still at the 4 known remaining errors.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
Removes the last fixable non-null assertion. GamePhase goes from a string
union to a tagged one whose "done" arm carries `result: GameResult`, so
narrowing on phase.kind yields a non-null result and `fileResult!`
disappears. GameResult - previously an untagged union nothing consumed -
is now tagged, so the render picks a screen from the discriminant instead
of inferring it from which of two nullable states was set.
Three earlier attempts at this site were reverted: a null guard returning
null, a widened ReactElement | null return, and a tagged GameResult with
phase left as a string. Each either added a branch no test could reach -
dropping the 100% gate - or left the value nullable in the fall-through.
The invariant lived in the phase, so the fix had to as well.
Coverage stays 100%. Statements 585 -> 579 and lines 536 -> 530, exactly
the two useState declarations and their setters that the collapse
removed; branches unchanged at 217, so nothing new went untested. All 10
App tests pass unmodified - they drive the app through mocked onDone
callbacks, so both done arms are exercised through the new union.
eslint 5 -> 4 (45 -> 4 for the session). The remaining four are reported
in the previous commit's evidence. usePuzzleGameLoop's Map.get assertion
is not among the fixable ones: `?? Infinity` was tried and measured at
219 branches / 99.54%, since the default side is unreachable.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
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
The previous commit's message claimed "no re-export barrel". That was
wrong: usage_report.py still did
from _usage_report_render import _fmt_h, _render_report
and after the split _fmt_h lives in _usage_report_format - the render
module only re-imports it for _methodology_section. So the import reached
the owner through an intermediary, which is the exact pattern the brief
says was vetoed last session. Now each name comes from its own module.
Also untracks bucket_catch/packages/frontend/tsconfig.tsbuildinfo and
gitignores *.tsbuildinfo. It is a generated incremental-build cache
tracked in a repo that blocks binaries, and it has been dirty in every
commit this session, forcing pre-commit to stash each time.
143 tests pass; usage_report.py still generates a real report.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
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
43 tests, no production change - the module is byte-identical to HEAD.
This lands separately from the split that follows because a split
verified by tests written in the same commit proves nothing: the tests
would have been written against the post-split shape.
Covers every function, and specifically the branches a happy-path test
misses: zero-length window and cpu_count() -> None in _cpu_table,
non-positive RSS and the CPU/pid tie-break in _dedupe_ram, the sibling
truncation boundary in _ram_table, zero total_samples in _gpu_table,
OSError and unparseable MemTotal in _host_profile, and the single-sample
unknown-interval path in _methodology_section and _render_report.
_host_profile is the only impure function; its /proc reads go through a
monkeypatched Path.open and nvidia-smi is stubbed at the _run seam, so
no test touches the disk or forks.
117 stmts / 28 branches, 0 missed. linux_configuration/tests: 100 -> 143
passing. No suppressions: two ANN401s were removed by typing the helper
kwargs explicitly rather than silencing them, and a mypy ignore went away
with a fallback branch no caller reached.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
windows/ (18 files), macos/ (28), ios/ (40) and linux/ (10) were flutter
create scaffolding that nothing in this repo builds: billsplit-ci.yml runs
pub get, dart format, analyze, test and the Python coverage gate, with no
flutter build step for any platform, and no script or doc invokes one
either. 96 tracked files, 4188 lines.
This also removes win32_window.cpp (288 lines), the last over-cap C++
file. It could not be split honestly - flutter regenerates it - so
deleting the target it belongs to is the honest way off the list.
Over-cap: 41 -> 40, and the cpp category is now empty.
Unpicked all three references: the .binary-allowlist icon entries and the
.gitignore un-ignore rules that existed only to let those icons past the
binary block, plus the README claim that the repo ships without platform
runner folders (false while 96 were tracked, true again now).
The four dirs are now gitignored so `flutter create` regenerates them
locally without re-tracking them. Verified by actually running
`flutter create . --platforms=linux,windows,macos,ios`: git add -An then
refuses all four as ignored. analyze/test/format stay green, 60 tests.
analysis_options.yaml is deliberately untouched - flutter analyze rewrites
the platform excludes back on every run, so removing them is not durable.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
flutter analyze --fatal-infos exited 1 at 6713ef3 with four
unnecessary_unawaited issues in lib/ui/home_screen.dart: Flutter now
annotates Navigator.push with @awaitNotRequired, which makes the
unawaited() wrappers redundant. Removed all four wrappers and the
dart:async import they were the only user of.
dart format --set-exit-if-changed was separately red on three test files
left unformatted by a68313f. One trailing blank line each.
Both are steps in .github/workflows/billsplit-ci.yml, so billsplit CI was
failing on main for reasons unrelated to any current work. Fixing them
first means the platform-directory deletion that follows can be verified
green-to-green.
flutter test: 60 passing before and after - the unchanged count is what
proves the unawaited() removals did not shift navigation timing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
The previous brief pointed at kcd2_dice_solver; those four files and twelve
more are done, so it was stale. The new one reflects what the user asked for
after seeing the stopping report: write the missing tests instead of stopping
at the coverage wall, change the source-text tests where a split needs it, and
delete the Flutter Windows scaffolding rather than working around it.
Also spells out the deployed-copy blocker in plain terms, since the previous
brief's phrasing did not land: install_leechblock.sh is copied to
/usr/local/share/digital_wellbeing/ and pacman_wrapper.sh:831 runs THAT copy on
every pacman invocation, so an entry+lib/ split breaks pacman on the live
machine. Verified the root-owned copy is still there.
Records the billsplit/windows/ deletion as a four-part job, not a git rm --
.gitignore carries two un-ignore rules for it and analysis_options.yaml
excludes it -- and flags macos/ and ios/ as the same question to ask rather
than assume.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY