Commit Graph

263 Commits

Author SHA1 Message Date
efe343dac4 Remove wsg_grabber: extracted to kuhyx/wsg-grabber; bump hypothesis
Some checks failed
dependency freshness / dependencies on newest stable (push) Has been cancelled
md-naming / markdown naming convention (push) Has been cancelled
Pre-commit checks / pre-commit (${{ matrix.group }}) (mypy, mypy) (push) Has been cancelled
Pre-commit checks / pre-commit (${{ matrix.group }}) (pylint, pylint) (push) Has been cancelled
Pre-commit checks / pre-commit (${{ matrix.group }}) (rest, ) (push) Has been cancelled
Pre-commit checks / pre-commit (${{ matrix.group }}) (shellcheck, shellcheck) (push) Has been cancelled
Python tests / test (push) Has been cancelled
Shell tests / shell-tests (push) Has been cancelled
Shell tests / lib-tests (push) Has been cancelled
Shell tests / arch-tests (push) Has been cancelled
The package now lives in its own private repo at ~/src/wsg-grabber, with
the 14 commits that touched it. /usr/local/bin/wsg-grabber was regenerated
by that repo's install.sh, so nothing here runs it any more.

hypothesis 6.168.2 -> 6.168.3: an upstream release turned the Pre-commit
checks and dependency freshness jobs red on 33f0fee3.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F8G3BNETGmbr6aM5cc18g4
2026-09-28 12:44:45 +02:00
33f0fee342 wsg_grabber: keep to the cloud folder, cap trash at 200 passes
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (mypy, mypy) (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (pylint, pylint) (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (rest, ) (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (shellcheck, shellcheck) (push) Waiting to run
Python tests / test (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / lib-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
Kept videos land in ~/data/cloud/Media/<year>/wsg (~/Downloads without the
cloud folder); trash/ keeps only the newest 200 passes and marks the rest
purged. Legacy keep/ is migrated once. Undo follows a keep through every
place it can end up.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F8G3BNETGmbr6aM5cc18g4
2026-09-28 10:44:00 +02:00
375555fa37 android_ui: never check or dismiss the keyboard on a virtual display
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (mypy, mypy) (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (pylint, pylint) (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (rest, ) (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (shellcheck, shellcheck) (push) Waiting to run
Python tests / test (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / lib-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
keyboard_is_up read the phone-wide mInputShown, but Android draws the IME
on display 0 even when its target window is on a virtual display, so it can
never cover a VD tap. With two sessions on one phone, the other session's
keyboard on the real screen made dismiss_keyboard press BACK on this
display, popping the route under test and then closing the app.

Prunes the oldest evidence artifact to stay under the 60-file cap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KeVXsWTuwjmZK5uvbw6vqF
2026-09-27 17:34:25 +02:00
50b7065854 app_icons: open-book glyph for book-guard; android_ui: hermetic serial test
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FramxKw57WdVkHPsDUv3Vs
2026-09-26 19:42:31 +02:00
6832805e5d a11ydump: queue dumps of one phone behind a host lock
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
Pre-commit checks / pre-commit (push) Waiting to run
Python tests / test (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
Android runs one instrumentation per package: a second `am instrument`
force-stops the first ("stop com.kuhy.a11ydump due to start instr"), and
so does `install -r`. The live two-session run on the Pixel caught two
sessions' dumps killing each other ("Process crashed"). dump_display now
runs install + instrument under a per-serial flock; a dump takes well
under a second, so queueing costs nothing noticeable.

After the fix the same 300 s run had 0 failures across 659 actions, and
the real screen and both apps stayed where they were.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WpA6wW2DKiovQADTdjWJoH
2026-09-25 16:36:50 +02:00
aa37852296 phone_guard: lease or block raw adb; android_ui drives a session's own virtual display
A raw `adb shell input tap` took no lease, so one session's tap landed in
another session's app (2026-09-25). python_pkg.phone_guard classifies every
adb call in a Bash command and, as the Claude Code PreToolUse hook, takes
the matching phone_lease (one app / real screen / whole phone) or exits 2:
key/text input without -d, input on another session's virtual display, or
a lease still held elsewhere after 40 s.

android_ui gains --display self|<id>: it drives only this session's
phone_vd display under that app's lease, and every input carries -d.
uiautomator sees display 0 only, so virtual-display trees come from a tiny
instrumentation APK (A11yDump.java) built from source without Gradle.

Prunes the oldest evidence artifact to stay under the 60-file cap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WpA6wW2DKiovQADTdjWJoH
2026-09-25 16:09:45 +02:00
c0c51036fc phone_lease: per-app leases, a self-cleaning whole-phone lease, one serial
Several sessions can now share one phone: a lease covers one package (the
real screen stays the default scope, so existing callers keep their
meaning), and only phone-wide actions take `--whole-phone`, which is capped
at 300 s and runs its `--cleanup` commands exactly once on release or, via a
detached reaper, on expiry. The serial is always resolved, never the
"default" placeholder that let android_ui and phone_deploy lease different
keys for the same phone (2026-09-25). Adds status --json, holds and whoami
for phone_vd.sh and the raw-adb hook.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Ajzz98xksbfak8CaunZWC
2026-09-25 15:05:57 +02:00
baa50cdb60 app_icons: umbrella glyph for the Umbrella? app
Filled scalloped canopy over a stroked J-handle (filled because a stroked
dome with ribs reads as a jellyfish at 48dp). AppIcon moves to _app_icon.py,
re-exported from apps.py, so the registry stays under the 250-line cap.
Prunes the two oldest evidence artifacts (60-file cap; still in history).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Ajzz98xksbfak8CaunZWC
2026-09-25 15:04:07 +02:00
b7e537fe01 phone_lease: one session drives the phone at a time; android_ui takes the lease on every call
Some checks failed
dependency freshness / dependencies on newest stable (push) Has been cancelled
md-naming / markdown naming convention (push) Has been cancelled
Pre-commit checks / pre-commit (push) Has been cancelled
Python tests / test (push) Has been cancelled
Shell tests / shell-tests (push) Has been cancelled
Shell tests / arch-tests (push) Has been cancelled
Two sessions driving the same phone interleave their taps: on 2026-09-20 a
tap meant for the workout sandbox opened another session's manga reader,
twice. The lease is a flock-guarded JSON file per serial under
~/.cache/phone-lease/, owned by the nearest ancestor `claude` process (one per
session), refreshed on every android_ui call and expiring 180 s after the
last one, so a quiet session frees the phone without anyone remembering to.
A foreign fresh lease makes a call wait up to 60 s, then exit 3 naming the
holder and pointing at the alternatives (the phone's virtual display, the
headless emulator) instead of inviting a retry loop. phone_deploy.sh takes
the same lease (exit 40).

The oldest evidence artifact is pruned to stay under the 60-file cap; it
remains in history.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YbBZqUaNWyhHYfzz6zGC7q
2026-09-20 18:50:26 +02:00
f15f00f3fe Re-point paths for the ~ reorganisation (home-tidy)
Also bumps the 16 meta/requirements pins the freshness gate flagged
(anthropic 1.5.0, numpy 2.5.3, pytest-randomly 5, flake8-bugbear 26.9.9,
ruff 0.16.7, pyright 1.1.414, selenium 4.49.0, hypothesis 6.168.0, lxml
6.1.3, pipdeptree 4.2.5, the two types-* stubs) and Flutter 3.47.3 in
meta/.fvmrc. One test still expected the pre-move ~/mcp-servers path.

The pre-commit hook fed .zsh files to shfmt and shellcheck, neither of
which parses zsh (SC2148/SC1071), so no zsh file could be committed since
the hook was tightened on 2026-08-22; those now get `zsh -n` instead. The
hook's own shfmt pass also staged its reformat of 8 shell files.

CI_GREEN_SKIP because main is red on exactly the dependency-freshness
finding this commit fixes, which is the documented use of that flag.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AwtowrowDXx6ccmWVPkCaF
2026-09-11 20:50:19 +02:00
42e41319dd Drop the alarm leg from the morning routine
Some checks failed
dependency freshness / dependencies on newest stable (push) Has been cancelled
md-naming / markdown naming convention (push) Has been cancelled
Pre-commit checks / pre-commit (push) Has been cancelled
Python tests / test (push) Has been cancelled
Shell tests / shell-tests (push) Has been cancelled
Shell tests / arch-tests (push) Has been cancelled
wake_alarm._alarm no longer exists: the PC's waking role was deleted in
kuhyx/wake-alarm, and the phone owns the alarm now. This removes ALARM_MODULE,
_run_alarm() and the --with-alarm flag, leaving one leg -- the workout lock.

Landing this in the same change as the deletion is the point. An orchestrator
left pointing at a module that no longer exists is exactly the failure this
repo has already had once: after screen-locker was extracted on 2026-05-28 the
reference here went stale and the workout lock died with ModuleNotFoundError on
every run for a month, while the alarm half kept working and hid it.

morning-routine.service loses --with-alarm and its comments now name what
actually starts it: wake-alarm-trigger.timer, at the phone's synced alarm time,
not the deleted systemd-sleep hook. Environment=DISPLAY=:0 stays -- the comment
credited it to the alarm's Tk window, but workout-locker.service needs it too.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PcdngP688mW6rJye3LRkSN
2026-08-30 15:26:04 +02:00
f70af95b61 app_icons: add the bowl-star glyph for restaurant-rater
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
Pre-commit checks / pre-commit (push) Waiting to run
Python tests / test (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
A new app at ~/restaurant-rater rates restaurant dishes, so it needs a
family icon. shield-cutlery is diet-guard's and the accent is shared
fleet-wide, so the new glyph has to be distinguished by silhouette alone: a
filled star above an open bowl -- a dish, and a verdict on it.

The star is filled rather than stroked for the same reason as the nib and
the anvil: at 72px weight its five interior notches close up well above
MIN_NEGATIVE_SPACE and it renders as a pentagon at 48dp.

The bowl went through two passes. The first used shallow walls stopping at
y=736 and read unmistakably as a smiling mouth with the star for a nose --
two curves with a shape above them is a face unless the vessel is clearly
deeper than it is wide at the base. The committed version drops to y=752
between narrower walls, which reads as a bowl.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012YDbT3ip2yeT18W3KCZdLq
2026-08-29 17:18:16 +02:00
adf2e402df Grade against 3.14 too, not just run on it
`requires-python` and the CI pin already said 3.14, but ruff, mypy and pylint
were still grading `meta/` against 3.10 -- three tools judging an interpreter
nothing runs any more, and ruff withholding the pyupgrade fixes that only
become safe at the newer target. `pin_ci_python.py` rewrites these three knobs.

What the stale target was hiding: `datetime.timezone.utc` where `datetime.UTC`
is now correct (UP017), and ruff's stable formatter applying PEP 758 --
`except (A, B):` becomes `except A, B:`. That makes the source 3.14-only at the
syntax level, which is what `requires-python = ">=3.14"` already promised; kuhy
chose it deliberately over holding ruff a release back (2026-08-29).

Also drops a comment in `wsg_grabber/models.py` that the bump makes false: it
explained why the file used ``str, Enum`` instead of ``StrEnum`` under mypy at
3.10, sitting directly above a ``class Verdict(StrEnum)`` that had already
stopped obeying it.

ruff, ruff-format, mypy --strict and pylint all clean at the new target.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SfnJmxxsZ534AKk16rC2Gr
2026-08-29 11:50:39 +02:00
1cf567977f Latest stable dependencies, gatelock v0.7.2, and the gate
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-28 17:38:25 +02:00
c5cd63fb80 token_audit: measure tool-call batching per API message
Some checks are pending
md-naming / markdown naming convention (push) Waiting to run
Pre-commit checks / pre-commit (push) Waiting to run
Python tests / test (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
Counting transcript records reports 1.00 tool calls per message no matter
what the model did: the harness writes one JSONL record per tool_use block,
all sharing message.id. A hand-rolled per-record count read the real 1.9%
batching rate as 0.0%, which nearly shipped as a headline finding.

Group by message.id instead, and report it, so the number comes from a
tested code path with the window handled by find_transcripts rather than an
ad-hoc timestamp. Three snapshot fields give next week's run a baseline.

Axes was at the 12-attribute cap, so the counters live in their own Batching
dataclass with the two ratios as properties.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NiSSDu4gKbp9GGxTTtXvYW
2026-08-28 12:22:21 +02:00
2a49ae31ef Register betting_sim with the shared icon generator
track-bars on the family charcoal/accent: rising bars with a plotted trend
point, which reads as odds movement.

linux:false because betting-sim's desktop target is a Chrome-wrapped web build
rather than the GTK embedder, so the hicolor set would go unread.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDSQS61Vgb6wptLPT2Utbf
2026-08-28 10:00:29 +02:00
88ffa37fe1 Find Material text fields by their hint label
Some checks are pending
md-naming / markdown naming convention (push) Waiting to run
Pre-commit checks / pre-commit (push) Waiting to run
Python tests / test (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
A Flutter/Material TextField publishes its label as `hint` and leaves text
and content-desc empty, so every field on such a form arrived as an
anonymous EditText. The workout app's manual-workout form is nine of them:
the only way to fill it was by screen position, which is precisely the
mis-tap-writes-the-wrong-field failure this tool exists to prevent.

`hint` ranks below text and content-desc but above resource-id. Material
clears the hint once content arrives, so a filled field still answers to its
real text and a caller can tell an empty field from a completed one.

The app-side alternative does not work and is documented as a trap: wrapping
the field in Semantics(label:) emits a second labelled node at the same rect,
and MergeSemantics collapses them by dropping the hint from Android's tree,
which leaves the form MORE anonymous than before. Widget tests cannot catch
that -- they assert on Flutter's SemanticsNode while this tool reads
Android's AccessibilityNodeInfo, so nine of them passed green on the build
whose on-device labels had just gone empty.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AWq9MRemX9ZWvNmnhNWUP2
2026-08-27 20:39:46 +02:00
6c12467f2d ci: check out shared gates in pre-commit; add punchme icon
Some checks failed
md-naming / markdown naming convention (push) Has been cancelled
Pre-commit checks / pre-commit (push) Has been cancelled
Python tests / test (push) Has been cancelled
Shell tests / shell-tests (push) Has been cancelled
Shell tests / arch-tests (push) Has been cancelled
`Pre-commit checks` has been red on main since 2026-08-24: the md-naming
and file-length hooks are thin shims that exec the real gate out of the
utils repo, and a runner has no ~/utils, so both died with "shared gate
not found at /home/runner/utils/...". The dedicated md-naming.yml
already checks utils out into .utils/; this does the same and points
UTILS_ROOT at it, which is the override the shims already support.
CI_GREEN_SKIP is set because this commit IS that fix.

Also registers punchme (github.com/kuhyx/punchme) in the shared icon
family on the family accent, Android-only so linux=False. Its glyph is a
time card with a clipped corner, a column of four punched holes and
three ruled lines; the first draft put two holes above two wide slots
and read unmistakably as a face at every size.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 21:31:32 +02:00
e971bf73bb Add focus_owner and a padlock-closed glyph to the icon family
focus_owner (com.kuhy.focus_owner, the Device Owner enforcer) was the one
Flutter app never registered with the shared generator. It shipped a
hand-written vector adaptive icon added purely to unblock its build — the app
had no icon resource at all and every release AND debug build failed on AAPT
"resource mipmap/ic_launcher not found". That placeholder used #E8EAED, so it
was off-family as well as unregistered.

None of the existing 13 glyphs fits a device-policy enforcer, so this adds
padlock-closed: shackle stroked (inheriting the group's round caps), body
filled in the accent, keyhole punched back out in the field colour. A fully
stroked padlock loses its keyhole at 48dp and a fully filled one loses the
shackle's opening; splitting them keeps both.

Geometry was computed against style.py's constraints rather than eyeballed,
per the lesson recorded on _NOTE_ASCENDER: body 292..732 x 520..792, shackle
centreline r=132 about (512,520) so its outer top is 352 (inside the 232 safe
edge) and its legs land at 380/644, well within the body. The keyhole leaves
60px of body above it and 76px below the stem, both clear of
MIN_NEGATIVE_SPACE=36 — checked by rendering at 48px, where the hole survives.

The repo path points into ~/phone-focus-mode/focus-owner: focus-owner was
absorbed as a subtree on 2026-08-24 and both the standalone clone and its
GitHub repo are gone.

app_icons stays at 100% branch coverage; 44 tests pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017KU9czAaz7wAkjyJ5uEB3t
2026-08-24 23:23:28 +02:00
49d82511c3 Adopt the four markdown namespaces and gate them
Renames this repo's markdown onto README.md / CLAUDE*.md / DOCS*.md /
TODO*.md so a future session can tell a live task from a finished
record without reading the file to find out.

TODO files carry the literal "REMOVE ME AFTER FINISH" and are deleted
when the work lands. Enforced by the md-naming pre-commit hook and CI
workflow, both delegating to the shared gate in kuhyx/utils so this
repo's idea of a valid name cannot drift from every other repo's.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A47aX5DJwmA3QFn3pkEWi7
2026-08-24 15:55:46 +02:00
a5209b6878 Extract phone_focus_mode together with focus_policy
github.com/kuhyx/phone-focus-mode

The two halves go as one repo: focus_policy parses phone_focus_mode's
config.sh into the typed policy that focus-owner consumes, and nothing
else references it. 77 commits of history, plus the two Python suites
from linux_configuration/tests/ and the six mutation fixtures from
meta/scripts/fixtures/, all of which named phone_focus_mode paths.

Running the suites -- not reading them -- turned up a real defect the
extraction would otherwise have inherited. config.sh sources the
untracked config_secrets.sh, and ctl_libs_harness.sh never pinned
HOME_LAT/HOME_LON, so the distance assertions were checking against
whatever real coordinates that file happened to hold. The suite passed on
this machine and could not run anywhere else, and a real home location
was load-bearing in a pass/fail condition. The harness now pins a pair
4300 m south of the coordinate its own case seeds, and config.sh sources
the secrets file only when it exists. 136/136 pass with no secrets file
present.

Before making the repo public I scanned the whole history for the known
home coordinates and for any committed config_secrets.sh: zero hits, and
the file was never tracked. The new repo also gained a .gitignore, since
none came across in the filter-repo pass.

test_magisk_service.sh hangs. It hangs identically in the pre-extraction
baseline worktree and appears in no CI list here, so the new repo's CI
skips it with that reason rather than pretending it passes.

check_python_location.sh loses its phone_focus_mode exception now that
the directory is gone; every tracked .py still passes it. The .gitignore
rules for config_secrets.sh and the keystore STAY -- the working copy is
still on this machine and still holds a real location.

Depth 262 -> 248.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
2026-08-23 18:32:58 +02:00
ec5d3aba34 Archive billsplit and everything coupled to it
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Python tests / test (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
The coupled case the previous archive commit deferred. billsplit is not
just a directory: a coverage package exists only to gate it, app_icons
holds a registry entry pointing into it, a CI workflow builds it, and
.binary-allowlist / .gitignore carry a paired set of rules for its
launcher icons. Removing the directory alone would have failed the 100%
coverage gate and left three configs naming a missing path.

So all of it goes in one commit:

  billsplit/                        73 files
  python_pkg/billsplit_coverage/     5 files, gated billsplit only
  .github/workflows/billsplit-ci.yml
  the app_icons AppIcon(key="billsplit") entry
  the .binary-allowlist patterns and their .gitignore ! overrides
  the .prettierignore fixture line

billsplit/ and billsplit_coverage/ were extracted in a single filter-repo
pass (8 commits, matching the monorepo) and pushed to
github.com/kuhyx/testsAndMisc-archive, where the coverage package joins
the archive's existing python_pkg/ tree. Presence was confirmed through
the GitHub API before anything was deleted here.

.binary-allowlist is now empty of patterns -- billsplit's icons were its
only entries. The policy header stays and records that no exception is in
force, and check_no_binaries.sh still exits 0 on a comment-only file.

Verified: 2095 tests pass at 100% coverage, down from 2114 by exactly the
19 billsplit_coverage tests. Depth violations 451 -> 417.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
2026-08-23 17:17:22 +02:00
5ba2cbd476 Dedupe endurain imports against postgres when the API cannot answer
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Python tests / test (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
Endurain API keys are upload-only by design: the scope allow-list is
frozenset({"activities:upload"}), enforced in both validate_api_key_scopes
and the create schema, and the activities read route is mounted with
validate_access_token rather than validate_access_token_or_api_key. A key
therefore fails at authentication before any scope check, so the HTTP
dedupe check added in 325d5a8b could never succeed and every pass fell
back to the ledger alone. Verified against v0.19.0; upstream documents no
plan to widen the allow-list.

Read the start times out of postgres instead. The container publishes no
host port, so this goes through docker exec -- no new dependency and no
port opened just to read four rows. The HTTP path is still tried first so
the supported route resumes working on its own if the allow-list ever
grows.

None and [] are kept strictly apart: a dead container, timeout or non-zero
exit means "could not ask" and returns to the ledger, while an empty result
set is a real answer. Conflating them would re-upload everything on any
docker hiccup, which is the bug the dedupe check exists to prevent.

Proven with an empty ledger: the known-duplicate TCX was skipped and the
activity count held at 4, and the same file WAS imported with
ENDURAIN_NO_DB=1, so the check is what blocks it. The duplicate that
negative control created was removed afterwards.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GbYEnfGnf1988cdFdGebot
2026-08-23 15:23:08 +02:00
ff3ea99a16 Fix a test that only passed in a directory named testsAndMisc
test_passes_store_and_repo_root asserted
env["PYTHONPATH"].endswith("testsAndMisc"), which is a property of the
checkout's directory name rather than of the code. It fails in any
worktree, which is where ci-mirror runs its gates.

Assert against the value reindex.py actually computes,
Path(reindex.__file__).resolve().parents[2]. Exact equality is a
stronger check than the suffix match it replaces.

This was invisible until now because ci-mirror invoked
pytest_changed_packages.py with no arguments, and that script returns 0
on empty argv -- so the pre-push pytest stage never ran. The next commit
fixes the invocation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
2026-08-23 14:53:29 +02:00
325d5a8baa Stop the endurain importer duplicating RunnerUp's own uploads
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Python tests / test (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
RunnerUp can now upload to Endurain directly as well as through the WebDAV
inbox, and the two paths cannot see each other: the same run landed twice
(activities 14 and 15, identical start time, distances differing only by
tcx/gpx rounding). The ledger only knows what this importer handled, so it
could not catch it.

Add a third dedupe check that asks Endurain what it already holds, queried
once per run. Endurain exposes no query-by-start-time endpoint, so a page of
recent activities is fetched and compared client-side, with a +/-120s
tolerance: the file for activity 15 embeds 21:51:04Z while the server stored
21:51:05Z, so exact equality misses the very duplicate that motivated this.
A failed or unauthorised query returns None, never an empty list, so it can
never be read as "nothing matches" and upload on the strength of it.

Export filenames are parsed as LOCAL wall-clock time. RunnerUp_2026-08-14-
00-58-45_Running is 2026-08-13 22:58:45 UTC; reading it as UTC would move a
late-evening run onto the wrong day and defeat the comparison.

Also fix the adb fallback, which broke the moment a second device attached:
phone_attached() returned true if ANY device was in the 'device' state, then
every bare adb call failed with "more than one device/emulator" as a warning
on a path nobody watches. The serial is now resolved once and threaded into
every call as -s. Ambiguity is settled by configuration, never by guessing --
several devices and no ENDURAIN_ADB_SERIAL means the fallback is skipped.

The remote check stays inert until the API key is granted activities:read;
until then it falls back to the ledger, which is the previous behaviour.

85 tests, 100% branch coverage on all four modules, no suppressions. The
oversized test modules are split by concern to stay under the 250-line cap.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GbYEnfGnf1988cdFdGebot
2026-08-23 14:36:03 +02:00
558883f281 feat(endurain): self-hosted run viewer fed by RunnerUp WebDAV exports
Stands up Endurain behind the existing Caddy edge and imports RunnerUp
activities into it. Endurain is a VIEWER: screen-locker's workout gate still
reads the phone over adb and is untouched here, because a WebDAV drop folder is
spoofable by anything that can write to ~/cloud and must not become a
screen-unlock authority.

Primary path is phone -> WebDAV (existing dufs) -> ~/cloud/RunnerUp/ ->
importer -> Endurain, with an adb pull as fallback when a run has not been
uploaded yet. A systemd user timer runs the importer every 15 min.

Findings that shaped the implementation, each verified against the running
system rather than assumed:

- Endurain moved to Codeberg; the GitHub repo is a read-only mirror stale at
  v0.17.7 against v0.19.0. The image is pulled from Codeberg.
- postgres:18 moved PGDATA to /var/lib/postgresql/18/docker. Upstream's compose
  example still mounts the pre-18 path, which fails as an unexplained
  "container is unhealthy"; the real cause is only visible in docker logs.
- Endurain seeds a hardcoded admin/admin via an Alembic migration with no env
  override, and endurain.kuhy.duckdns.org already resolved publicly. Publishing
  is therefore a separate script that refuses to install the Caddy site while
  those credentials still work.
- Endurain performs NO duplicate detection, so the importer owns dedupe on two
  axes: a content hash, and an activity key that collapses the .gpx/.tcx pair
  RunnerUp writes for a single run (different bytes, same run).
- Ambiguous uploads (5xx/timeout) are never re-queued, since the activity may
  already have been committed; only definite 4xx rejections are staged for
  retry. The importer exits non-zero so the timer surfaces the problem.
- RunnerUp has no background sync at all, so every upload is a manual tap.
- Its WebDAV synchronizer needs HTTPS with a real cert (Android blocks
  cleartext at targetSdk 36; self-signed is broken upstream) and a URL with no
  trailing slash, and cannot MKCOL against dufs - so the inbox must pre-exist.

The phone gets a dufs credential scoped to /RunnerUp, verified unable to read
Keepass or gdrive_backups.

Verified: 52 tests at 100% branch coverage; the timer imports 4 real runs and
re-runs create zero duplicates; https://endurain.kuhy.duckdns.org serves 200
with a valid cert and the other edge sites are unaffected; screen-locker's
suite still passes 1090 tests at 100% coverage.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GbYEnfGnf1988cdFdGebot
2026-08-23 13:09:18 +02:00
28f2925c30 Instrument: multi-line array elements are not statements; clear arch_perf_fixes
Fourth flavour of defect (a) found this session, after multi-line quotes,
`} |` and the backslash-continued assignment. bash reports a multi-line array
assignment at its OPENING line and never emits the element lines or the
closing paren, so kcov instrumenting them inflates the denominator:

    +L3 items=('one' 'two')      # opened line 3, closed line 6
                                 # lines 4, 5, 6: never emitted

The rule is therefore symmetric with the existing multi-line-quote case: the
opening line is the statement, everything up to the closing paren is data.
_closes_array() finds that paren by recomputing quote state per candidate, so
an element like "a(b)" does not end the literal early.

Measured effect through fixes/lib/tests/run_all.sh:

  arch_perf_fixes.sh  93.48% -> 100.00%  (43/43)
  bt_audio.sh        100.00% -> 100.00%  (127/127)
  arch_perf_report.sh 100.00% -> 100.00% (59/59)
  arch_perf_probes.sh 100.00% -> 100.00% (93/93)
  ubuntu_perf_more.sh 100.00% -> 100.00% (32/32)
  bt_report.sh       100.00% -> 100.00%  (37/37)
  bt_pairing.sh      100.00% -> 100.00%  (65/65)

One gained, six held, none went DOWN. Acceptance test unchanged:
dot_resolver_install.sh 39/39 and rpi_nc_ca.sh 73/73. arch_perf_fixes.sh
therefore comes off the allowlist in the same commit, confirmed by the real
gate with no exemption.

The fail-open check, since over-detection INFLATES coverage: the old and new
predicates were diffed over every allowlisted lib plus the six cleared ones,
printing each newly excluded line WITH its text. All 91 across 10 libs are
array elements or closing parens -- common.sh's tool tables, packages.sh's
associative arrays, arch_sysctl.sh's sysctl maps, thorium_repairs.sh's four
path lists, ubuntu_perf_fixes.sh's sssd units, and so on. No statement caught.

Tests cover both directions, including three negatives: a single-line
`x=(a b)`, an element containing a parenthesis, and `x=$(echo hi)` which is a
command substitution rather than an array. Module stays at 100% branch
coverage.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 20:13:45 +02:00
d989cd7ebf Coverage instrument: a closing brace plus an operator is not a statement
continuation_lines() excluded the continuation lines of multi-line QUOTED
arguments but nothing else, so a line holding only a group's closing brace
and an operator -- `} |`, `} >>"$REPORT_FILE"` -- stayed in the denominator
while bash attributed no statement to it. That capped four fixes/lib libs
below 100% with no test able to move them.

Why the anchor makes this safe: bash attributes every command INSIDE a group
to its own line, and a pipe's right-hand side to its own line, leaving the
brace-plus-operator line with nothing. Anchoring the pattern on a LEADING
`}` therefore excludes only lines that can hold no statement. A one-liner
`{ echo a; } >file` begins with `{` and is untouched; a bare `}` closing a
function has no operator and is untouched.

Measured effect, all through fixes/lib/tests/run_all.sh:

  bt_audio.sh          96.97% -> 100.00%  (127/127)
  arch_perf_report.sh  98.33% -> 100.00%  (59/59)
  bt_report.sh        100.00% -> 100.00%  (37/37)
  bt_pairing.sh       100.00% -> 100.00%  (65/65)
  bt_adapter.sh        98.55% ->  98.55%  (68/69, capped by a different flavour)

Two gained, three held, none went DOWN -- the handoff's regression signal.
That is not luck: _measure computes covered_set = (executed|traced) &
denominator, so for a lib already at 100% the covered set IS the denominator
and removing a subset cannot lower it. The five gated pacman_hook_stall libs
are safe by the same arithmetic.

Acceptance test from the handoff passes unchanged: dot_resolver_install.sh
39/39 and rpi_nc_ca.sh 73/73, both 100.00%.

The fail-open check that actually matters, since over-detection INFLATES
coverage and could promote an untested lib: the new predicate was run over
every lib in the allowlist plus the cleared ones, printing each newly
excluded line WITH its text. All 17 across 10 libs are genuine
brace-plus-operator lines; no ordinary statement was caught.

Tests cover both directions, including the two negative cases (a bare `}`
and a single-line `{ ...; } >file`), keeping the module at 100% branch
coverage. The quote scanner moved into _quote_after() to stay under the
C901 complexity limit rather than suppressing it.

Deliberately NOT fixed here: the backslash-continued ASSIGNMENT flavour that
caps bt_adapter.sh line 97. bash attributes those to the LAST line of the run
but a backslash-continued plain COMMAND to its FIRST, so that predicate needs
a command-vs-assignment distinction. One line in one lib; not worth the
landmine today.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 18:29:47 +02:00
11aea86920 Land the token_audit standing-cost checkup (116 tests, 100% branch coverage)
Answers "what is billed on every turn regardless of use" by measurement
instead of recall, which is the part earlier audits kept getting wrong.

A surface is reported dead only when unused in BOTH windows. One earlier
audit's seven-day dead list included six servers that had been used within
thirty days, so a single window is not evidence. Shares are weighted with
cache-read weight 0.1, so a cached surface is not billed as though it were
re-read in full each turn.

Verified by running it, not just by tests: the report renders over 44,311
turns, putting rules/ at 4,907 tokens (1.77%), MEMORY.md at 4,312 (1.55%),
memories/ at 3,758 (1.35%), skill descriptions at 3,279 (1.18%) and CLAUDE.md
at 2,057 (0.74%), with per-MCP 7d/30d call counts behind each verdict.

The files were found untracked in the tree from an earlier session; every
module is already under the 250-line cap, so nothing needed splitting, and
the package was already at 100% branch coverage, so nothing needed new tests.
This lands the instrument only -- no surface is unregistered on its say-so.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 14:33:03 +02:00
29b8c5a788 Name the lines outside kcov's line set; bound defect (c) as fail-closed
The report printed a COUNT of traced lines that kcov never listed as
instrumentable. A count cannot be acted on: deciding whether such a line is a
real statement or a function declaration needs the line number. It now lists
them, the way it already lists uncovered lines.

Reading them across every lib with a harness splits them cleanly:

  benign  dwm_config.sh:27 (`heal_config() {`), transcribe_deps.sh:9
          (one-line function declaration), clean_audio_filters.sh:197,200
          (`((running++))`) -- correctly not statements
  real    aw_autostart.sh 68,70,73,79-81,83-84,87-88,94 -- plain echo, local,
          if, `sudo -u ... mkdir`, `cat >` heredoc. kcov lists nothing after
          the `{ ... } >>"$file"` command group at lines 61-65.

So kcov's LINE SET is incomplete too, not just its hit counts. This does NOT
inflate anything: a line reaches that list only because the TRACE SAW IT RUN,
so folding all 11 into both halves moves aw_autostart from 80/82 = 97.56% to
91/93 = 97.85% -- up. An off-set line is by construction an executed line, so
it can only ever be a missing COVERED line, never a missing uncovered one.

What it does mean is that such a subject is measured over a subset of its
statements. The percentage is honest about the subset, not the whole file,
which is why the lines are now printed rather than tallied.

Separately: nc_php.sh reports "kcov instrumented no lines" on the PRE-FIX
instrument too -- checked at 078d3463 in a throwaway worktree -- so it is not
a regression from the two-pass change. The 84/85 = 98.82% in earlier notes
does not reproduce and is recorded as unverified.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 14:30:39 +02:00
81191f40e6 Fix both kcov defects: dwm_config 47.95% -> 98.48%, rpi_nc_install 11.36% -> 96.59%
The instrument, not the tests, was the bottleneck. Coverage is now
(PS4-traced lines | kcov hits) & (kcov line set - continuation lines).

The trace deliberately does NOT supply the denominator. A trace reports only
lines that RAN, so using it for both halves would make every subject 100.00%
and arm a repo-wide gate that checks nothing.

Three measured findings, each of which yields a silently wrong trace:

1. kcov's ptrace and bash's xtrace cannot share a process. Under
   SHELLOPTS=xtrace every line kcov recorded as hits="1" comes back hits="0"
   -- reproduced A/B on one subject. Hence two passes, two namespaces.
2. SHELLOPTS is readonly inside bash, so `export SHELLOPTS=xtrace` fails and
   tracing never turns on; and `set -x` does not reach child processes.
3. Decisive: bash under `unshare --map-root-user` runs PRIVILEGED and
   DISCARDS an inherited PS4, falling back to "+ " while still honouring
   xtrace. The trace loses its file:line prefix and every subject reads 0%.
   Fixed by delivering PS4 and `set -x` through a BASH_ENV file, which also
   solves child-process propagation.

A fourth trap, in the report: kcov records filename="dwm_config.sh", a bare
basename. The source path comes from the trace's ${BASH_SOURCE} or a unique
repo search; an ambiguous basename yields no exclusion rather than a guess.

Measured through run_all.sh, nothing down:

  dot_resolver_install.sh  39/39 = 100.00%  (acceptance, unchanged)
  rpi_nc_ca.sh             73/73 = 100.00%  (was 72/73 = 98.63%)
  dwm_config.sh            65/66 =  98.48%  (was 35/73 = 47.95%)
  rpi_nc_install.sh        85/88 =  96.59%  (was 10/88 = 11.36%)

rpi_nc_ca reaching 100% is the load-bearing check: its missing line 141 was
cross-process contagion, and the trace pass has no kcov in it to contaminate.

dwm_config's remaining line 39 is a `done < <(...)`. Left in the denominator
on purpose -- removing lines inflates coverage, the fail-open direction, so a
second exclusion rule needs per-line justification, not a pattern from one lib.

Both new Python modules hold 100% branch coverage (29 tests).

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

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

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

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

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F3PzGc75gBcdxP3PkHXGcV
2026-08-21 16:41:31 +02:00
12e9f5916f extract_shell_functions: recognise the function name() spelling
The detection regex matched only `name() {`. bash also accepts
`function name() {` and `function name {`, and pacman_wrapper.sh uses the
keyword form for 17 of its 30 functions — so a split of that file would have
left every one of them behind in the entry script while the tool reported
success. Silent, and exactly the kind of half-done split the verifier is
meant to catch afterwards rather than the extractor causing.

Parentheses stay mandatory in the non-keyword branch. Making them optional
everywhere also matches brace groups and control constructs, which would
sweep non-function code into the library — the opposite failure, and worse.
Nine negative tests pin that boundary.

The function name now comes from a named capture group rather than
splitting on "(", which returned "function name" for the keyword form and
would have broken both --functions selection and the verifier's name matching.

Verified against the already-split files first: cco_state.sh still yields 13
functions and leechblock_fetch.sh 3, so the completed splits are unaffected.
pacman_wrapper.sh goes from 13 detected to 31.

First tests for this script: 20 cases covering every accepted spelling,
brace matching across nested bodies, and single-line functions.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F3PzGc75gBcdxP3PkHXGcV
2026-08-21 16:11:29 +02:00
2d7d322881 vscode_optimizer: detect Intel iGPUs as Intel, not AMD
_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
2026-08-17 14:49:43 +02:00
377a352d48 vscode_optimizer: split _optimize.py 498 -> 100 + 5 modules
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Python tests / test (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
The last python file over the 250-line cap. Now that it sits at 100% line
and branch coverage, split along its natural seams:

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

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
2026-08-17 13:10:10 +02:00
4b2cd0c398 Clear the wsg_grabber test modules: python_pkg is now under the cap
The last six over-cap files split into eleven new modules, in two passes
since the first cut left five still over. python_pkg has no violations
left at all, down from 50 when this effort started.

Each split carries the whole preamble including the module's pytest
fixtures - started, toolkit and session are taken as parameters and
resolve lexically, so a hand-written import block would have missed
them. ruff then pruned 82 unused imports.

Pass count is 277 before and after, checked after both passes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
2026-08-16 18:19:26 +02:00
b60cb4972a Clear five python_pkg packages under the cap
android_ui, app_icons, token_audit, focus_policy and random_jpg are now
entirely under 250 lines. Only wsg_grabber's six test modules remain in
python_pkg.

android_ui/driver.py 411 -> 205 took two cuts and a class split: the
element and error types plus the XML parser to _elements.py, and the
text-entry methods to a TextEntryMixin, using the ABC-with-abstract-
methods shape because the tests call them as bound attributes.
app_icons/glyphs.py 291 -> 93 by moving the twelve path fragments out.

Three intermediate failures, all module-level globals that travelled
with their code (_logger, re, _SAME_FIELD_X_TOLERANCE_PX) - the same
class of miss as _make_verifier, which ruff cannot flag.

Counts unchanged: 81, 137, 14, 44, 57.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
2026-08-16 18:14:36 +02:00
e3214c813c Clear the seven over-cap code_tutor test modules
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Python tests / test (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
Partitioned at the section banners each file already used, into ten new
modules named for what they cover. python_pkg/code_tutor is now entirely
under the cap, from thirteen violations.

The splitter copies the whole original header into each new file and
lets ruff's F401 pass prune it - 59 imports removed correctly, versus a
hand-written import block failing on every earlier test split today.
That does not cover module-level helpers: _make_verifier sat between two
banners and was left behind by its callers, so it moved to conftest.py,
taking test_verifier.py's shadowing copy of _make_live_mock with it.

No patch string changed. Pass count is 217 before and after.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
2026-08-16 18:06:38 +02:00
eb322dcf80 Split the Claude backend out of code_tutor/_llm
_llm.py 295 -> 217, clearing the last code_tutor source violation; only
its seven test modules remain over the cap.

The seam falls between the two backends because each pins a different
plain import: test_llm.py patches requests.post on _llm for
OllamaBackend and anthropic.Anthropic for ClaudeBackend. _llm keeps its
own anthropic import - complete_with_fallback still catches APIError -
so the four anthropic patch strings were split per test rather than
moved wholesale.

Pass count is 217 before and after.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
2026-08-16 18:00:53 +02:00
1d1938ce23 Split code_tutor cli and verifier, deleting two lint suppressions
cli.py 345 -> 222 and _verifier.py 347 -> 228, via _cli_checks.py and
_verifier_support.py.

The cli seam cut straight through code covered by its per-file-ignores.
Rather than copy S607 and PERF203 onto a second entry, both are fixed:
systemctl is resolved with shutil.which, and the readiness poll no
longer carries a try/except in its body now that the probe is its own
function. Both ignores are deleted from meta/pyproject.toml and ruff
passes without them - the repo has two fewer suppressions than before.

The which() fix adds a systemctl-is-None branch, so the pass count goes
216 -> 217: one new test for it, no existing test touched. Coverage is
back at 100% by covering the branch, not excluding it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
2026-08-16 17:57:38 +02:00
c113a3a192 Split the code_tutor import graph and scan tables out of _analyzer
_analyzer.py 388 -> 226. The file walk, dotted-name map, import
resolution and fingerprint go to _deps.py; the skip frozensets and the
function regex to _scan_tables.py, which covered the last nine lines
after the first cut landed at 259.

_is_binary is the only name any test patches on _analyzer and it stays
put, so no patch string needed editing. Only test_analyzer.py's import
block had to be split.

Pass count is 216 before and after.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
2026-08-16 17:48:32 +02:00
277cb9fd6f Split the code_tutor challenge flows out of _challenge
_challenge.py 467 -> 236. The two flows and run_coding_challenge go to
_challenge_flows.py; the three static prompt strings to
_challenge_prompts.py, which covered the last eleven lines after the
first cut landed at 261.

The 13 patch strings in test_challenge_part3.py were repointed per test,
by which function is under test - the _run_user_impl tests keep
_challenge. Getting that wrong does not fail here, it HANGS: an
unintercepted _collect_and_rate_tests ran the real interactive flow and
spun the suite for two minutes. Runs are now under an explicit timeout,
and the patch targets are asserted to resolve before pytest starts.

Pass count is 216 before and after.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
2026-08-16 17:42:50 +02:00
40a4eaf5b5 Split the code_tutor pytest runner and verdict streaming out
_challenge_support.py 402 -> 237. _pytest_clean and _patch_and_test go
to _pytest_runner.py with the subprocess/os/sys imports they solely used;
_stream_verdict and _parse_verdict go to _verdict.py with rich's Live.

code_tutor patches by absolute string rather than a MOD constant, so the
seam cost 15 per-name rewrites instead of one sed. Two intermediate
failures caught by running it: the test modules import the moved names
directly, and five patch strings still named the old module.

Pass count is 216 before and after.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
2026-08-16 17:33:58 +02:00
2c01a0267e Clear the last three brother_printer test modules over the cap
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Python tests / test (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
test_consumables_part2.py 273 -> 133 (renamed test_counter_scales.py),
test_consumables.py 272 -> 123, test_check_brother_printer.py 275 -> 112.
python_pkg/brother_printer is now entirely under the 250-line cap, from
fifteen violations at the start.

The consumables split lands exactly on the MOD/MOD_PD boundary an earlier
commit introduced, so the two-constant workaround dissolves instead of
being carried forward - each file has one module under test again.

Pass count is 402, unchanged across all eight commits of this pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
2026-08-16 17:27:07 +02:00
74eee2b57f Split the brother_printer display and usb_query test modules
test_display.py 363 -> 204, with the supply maths and its display going
to test_supply_levels.py. test_usb_query.py 328 -> 126, with the retry
and sequencing tests going to test_usb_query_sequence.py.

That removed the last reader of the six _supply names through display,
so display.py's __all__ shrinks to the two public entry points and the
stale comment about tests reaching them there is gone. Checked by grep
across python_pkg and an import-time attribute probe, not by the suite.

The first run after the usb_query split reported 393: TestParsePagecount
sat between the two moved classes and travelled with them. Fixed.

Pass count is 402 before and after.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
2026-08-16 17:23:47 +02:00
607ba98c85 Split the brother_printer cups_service test modules under the cap
test_cups_service.py 390 -> 172 (usblp port status to _port),
part2 261 -> 115 (status mapping and discovery out), part3 260 -> 105
(the four healthy-port cases out). part3 was one 235-line class, so it
split by method rather than by class.

The four part-numbered files are renamed for what they cover; three of
them now name _cups_fallback, matching where the code moved earlier.

The first run after the part2 split reported 400, not 402: the moved
classes needed subprocess and get_status_info that my hand-written
import block had dropped. Fixed, and the count is 402 again.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
2026-08-16 17:19:55 +02:00
65bc65c7e3 Split the brother_printer cups_queue test modules under the cap
test_cups_queue.py 458 -> 167, partitioned at class boundaries into
_actions (the four shell-out primitives) and _backend (health check and
error-log scanning). test_cups_queue_part2.py 278 -> 131, with the four
_handle_* dispatchers moving to _handlers.

No MOD changed: unlike the source splits, every patched name already
lives in the module each file names, so this is a pure partition. The
two part-numbered survivors are renamed for what they cover.

Pass count is 402 before and after.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
2026-08-16 17:15:55 +02:00
ad3d70f197 Split brother_printer PJL wire transport out of usb_query
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Python tests / test (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
usb_query.py 267 -> 180. The non-blocking fd handling - _drain_buffer,
_write_with_deadline, _read_nonblocking, _wait_for_pjl_response and
pjl_query - moves to _pjl_io.py.

usb_query imports the three names test_usb_query.py patches back, which
is safe here only because all three still have real call sites in the
code that stayed; an import-only name would be stripped by ruff's F401
sweep and break the patch. os and time also stay: they are patched on
usb_query as well as on _pjl_io.

test_usb_query.py needs no edit. Pass count is 402 before and after.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
2026-08-16 17:11:15 +02:00
a438652aaa Split brother_printer dropped-page detection out of consumables
consumables.py 279 -> 217. check_page_delivery and _snapshot_counters
move to _page_delivery.py.

The state-file primitives stay put: test_consumables.py patches
_load_consumable_state, _save_consumable_state and CONSUMABLE_STATE_FILE
on consumables, and moving them would have let a test write to the real
~/.config/brother_printer/state.json. Verified by mtime after the run,
not just by a green suite.

test_consumables_part2.py patches the same three primitives from both
sides of the seam, so it gains a second MOD_PD applied per class rather
than a blanket swap. Pass count is 402 before and after.

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