diff --git a/docs/superpowers/evidence/book-guard-bar-block-2026-10-02.json b/docs/superpowers/evidence/book-guard-bar-block-2026-10-02.json new file mode 100644 index 00000000..f141e4d8 --- /dev/null +++ b/docs/superpowers/evidence/book-guard-bar-block-2026-10-02.json @@ -0,0 +1,49 @@ +{ + "intent": "Make book-guard openable from the i3 bar the same way screen-locker is: a [book_guard] block shows this month's paper-book pace, and a click opens the book-guard desktop window. Move the shared pins to gatelock 0.9.0 (the new shared morning-session reader) and today's newest stable releases.", + "scope": [ + "linux_configuration/i3blocks/book_guard.sh - new block script", + "linux_configuration/i3blocks/config - [book_guard] entry next to [screen_locker]", + "linux_configuration/tests/test_i3blocks_book_guard.sh - behaviour test", + "meta/requirements.txt - gatelock 0.9.0 and newest-stable pins", + "Non-goal: the stale tracked linux_configuration/i3/config (it already lacks the workout-locker and leetcode-guard exec lines); the live ~/.config/i3/config got the book-guard exec line." + ], + "changes": [ + "book_guard.sh reads the state.json book-guard publishes into ~/data/cloud/Reading on every inbox pass and gate run (no server, one jq pass): green on pace, yellow behind but unlocked (escaped / free day / unlocked tag), red when locked, orange when missing or older than 45 min, red with DISARMED when book-guard.timer is inactive. A click runs book-guard-desktop via setsid, which opens another window if it is already serving.", + "meta/requirements.txt: gatelock 0.8.2 -> 0.9.0; anthropic 1.11.0, cryptography 50.0.2, flake8-bugbear 26.9.30, geopandas 1.2.0, howlongtobeatpy 1.0.24, pyflakes 4.0.1, selenium 4.50.0, mypy 2.4.0, pylint 4.1.1, ruff 0.16.10." + ], + "verification": [ + { + "command": "bash linux_configuration/tests/test_i3blocks_book_guard.sh", + "result": "pass", + "evidence": "on pace / locked / escaped / free day / other / stale / disarmed / missing / broken / click all asserted; systemctl and the launcher stubbed on PATH." + }, + { + "command": "bash linux_configuration/tests/test_i3blocks_efficiency.sh", + "result": "pass", + "evidence": "All i3blocks efficiency regression tests passed." + }, + { + "command": "~/.config/i3blocks/book_guard.sh (live state.json)", + "result": "pass", + "evidence": "'๐Ÿ“– 0/300 ยท 10p behind (escaped)' #F1FA8C; block live in the bar after i3-msg reload." + }, + { + "command": ".venv/bin/python -m pytest python_pkg/code_tutor (the only anthropic/geopandas/selenium/cryptography importer)", + "result": "pass", + "evidence": "217 passed on anthropic 1.11.0." + }, + { + "command": "pre-commit run --files ", + "result": "pass", + "evidence": "all hooks passed, including dependency-freshness." + } + ], + "risks": [ + "cryptography 50.0.2 stays outside pyopenssl/mitmproxy's declared ranges, as 50.0.1 already was.", + "A click with book-guard-desktop missing does nothing (errors are silenced so the bar never breaks)." + ], + "rollback": [ + "git revert this commit and remove ~/.config/i3blocks/book_guard.sh; i3-msg reload.", + "Confirm the bar still renders [screen_locker] and the other blocks." + ] +} diff --git a/docs/superpowers/evidence/python-tests-path-filter-symlink-2026-08-23.json b/docs/superpowers/evidence/python-tests-path-filter-symlink-2026-08-23.json deleted file mode 100644 index f2efc3f6..00000000 --- a/docs/superpowers/evidence/python-tests-path-filter-symlink-2026-08-23.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "intent": "Make the Python tests workflow actually trigger when the dependency list changes, so a requirements fix cannot silently skip the workflow it is meant to repair.", - "scope": [ - ".github/workflows/python-tests.yml (push and pull_request path filters)", - "Non-goal: changing the symlink layout, which the repo documents deliberately" - ], - "changes": [ - "Added meta/requirements.txt alongside requirements.txt in both path filters. The root file is a symlink into meta/, and GitHub matches filters against the path as git records it, so editing the real file did not match the symlink's name." - ], - "verification": [ - { - "command": "Observed on commit 32de2215, which changed meta/requirements.txt", - "result": "fail", - "evidence": "Dependency Graph, Shell tests and Pre-commit checks all ran; Python tests did NOT trigger at all, leaving the last Python tests result on the older red commit 5ba2cbd4" - }, - { - "command": "python3 -c \"import yaml; yaml.safe_load(open('.github/workflows/python-tests.yml'))\"", - "result": "pass", - "evidence": "YAML valid; both the push and pull_request filter lists were patched (2 occurrences)" - }, - { - "command": "gh workflow run 'Python tests' --ref main", - "result": "pass", - "evidence": "Dispatched run 32645075080 to confirm the dependency fix itself, since the filter bug prevented an automatic run" - } - ], - "risks": [ - "The other root symlinks (pyproject.toml, run.sh, lint_python.sh, .fvmrc) are not referenced by any workflow path filter today, so they carry the same latent trap if one is ever added." - ], - "rollback": [ - "Remove the meta/requirements.txt lines from both filters.", - "After rollback, a change to meta/requirements.txt will once again skip this workflow." - ] -} diff --git a/linux_configuration/i3blocks/book_guard.sh b/linux_configuration/i3blocks/book_guard.sh new file mode 100755 index 00000000..b276746f --- /dev/null +++ b/linux_configuration/i3blocks/book_guard.sh @@ -0,0 +1,87 @@ +#!/bin/bash + +# ============================================================================ +# book-guard status for i3blocks: this month's paper-book pace. +# +# Sibling of screen_locker.sh, and opened the same way: a click opens the +# book-guard desktop window (book-guard-desktop serves the web app on :8773 +# and, if it is already running, just opens another window onto it). +# +# Reads the state.json book-guard publishes into the Reading folder on every +# inbox pass (every 15 min) and every gate run -- no server needed. Like the +# screen-locker block it is never blank and never silently green: a missing +# or stale snapshot, or a disarmed timer, is shown as such. +# ============================================================================ + +set -euo pipefail + +readonly STATE="${BOOK_GUARD_STATE:-$HOME/data/cloud/Reading/state.json}" +readonly LAUNCHER="${BOOK_GUARD_DESKTOP:-$HOME/.local/bin/book-guard-desktop}" +# Three missed inbox passes: past this the numbers describe a different hour. +readonly STALE_SECONDS=2700 + +# Dracula-ish palette, matching the other blocks in this bar. +readonly RED='#FF5555' +readonly YELLOW='#F1FA8C' +readonly GREEN='#50FA7B' +readonly ORANGE='#FFB86C' + +emit() { + # full_text, short_text, color โ€” the three lines i3blocks reads. + printf '%s\n%s\n%s\n' "$1" "$2" "$3" +} + +# A click only opens the window; nothing here can clear a lock. +if [[ ${BLOCK_BUTTON:-0} -ne 0 ]]; then + setsid -f "$LAUNCHER" >/dev/null 2>&1 || true +fi + +if ! command -v jq >/dev/null 2>&1; then + emit '๐Ÿ“– no jq' '๐Ÿ“– ?' "$ORANGE" + exit 0 +fi + +if [[ ! -r $STATE ]] || ! jq -e '.pace' "$STATE" >/dev/null 2>&1; then + emit '๐Ÿ“– no status' '๐Ÿ“– ?' "$ORANGE" + exit 0 +fi + +# One jq pass, tab-separated: locked, pages, target, behind, reason. +IFS=$'\t' read -r locked pages target behind reason < <( + jq -r '[.locked, .pace.pages, .pace.target, .pace.behind, .reason] | @tsv' "$STATE" +) + +text="๐Ÿ“– ${pages}/${target}" +short="๐Ÿ“– ${pages}" +if [[ $locked == 'true' ]]; then + text="$text ยท LOCKED, ${behind}p behind" + color=$RED +elif ((behind > 0)); then + # Behind but unlocked: the bar has room for a tag, not the full reason. + case $reason in + *escape*) why='escaped' ;; + *free\ day*) why='free day' ;; + *) why='unlocked' ;; + esac + text="$text ยท ${behind}p behind (${why})" + color=$YELLOW +else + text="$text โœ“" + color=$GREEN +fi + +printf -v now '%(%s)T' -1 +age=$((now - $(stat -c %Y "$STATE"))) +if ((age > STALE_SECONDS)); then + text="$text (stale $((age / 60))m)" + color=$ORANGE +fi + +# Arming beats pace, as in the screen-locker block: a gate that cannot fire +# is the worse problem, and it is the one that hides. +if ! systemctl --user is-active --quiet book-guard.timer; then + text="$text โš  DISARMED" + color=$RED +fi + +emit "$text" "$short" "$color" diff --git a/linux_configuration/i3blocks/config b/linux_configuration/i3blocks/config index 5c8c5fb5..2cfc71c3 100644 --- a/linux_configuration/i3blocks/config +++ b/linux_configuration/i3blocks/config @@ -59,6 +59,10 @@ interval=30 command=~/.config/i3blocks/screen_locker.sh interval=30 +[book_guard] +command=~/.config/i3blocks/book_guard.sh +interval=30 + [shutdown_countdown] command=~/.config/i3blocks/shutdown_countdown.sh interval=60 diff --git a/linux_configuration/tests/test_i3blocks_book_guard.sh b/linux_configuration/tests/test_i3blocks_book_guard.sh new file mode 100755 index 00000000..cb5ef1e5 --- /dev/null +++ b/linux_configuration/tests/test_i3blocks_book_guard.sh @@ -0,0 +1,118 @@ +#!/bin/bash +# Behaviour tests for the i3blocks book_guard block: pace text, colours, every +# failure state and the click. systemctl and the desktop launcher are stubbed, +# and the state file is a fixture, so nothing reads the real Reading folder. +set -euo pipefail + +SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) +REPO_DIR=$(cd -- "$SCRIPT_DIR/.." && pwd) +BLOCK="$REPO_DIR/i3blocks/book_guard.sh" +TMP_DIR=$(mktemp -d) + +cleanup() { + rm -rf "$TMP_DIR" +} +trap cleanup EXIT + +fail() { + printf 'FAIL: %s\n' "$1" >&2 + exit 1 +} + +assert_eq() { + local expected=$1 + local actual=$2 + local context=$3 + if [[ "$expected" != "$actual" ]]; then + fail "$context (expected '$expected', actual '$actual')" + fi +} + +printf '{}' | jq -e . >/dev/null 2>&1 || + fail 'a working jq is required for the book_guard tests (pacman -S jq)' + +# Stub systemctl: the timer is active unless TIMER_DOWN is set. +BIN_DIR="$TMP_DIR/bin" +mkdir -p "$BIN_DIR" +cat >"$BIN_DIR/systemctl" <<'STUB' +#!/bin/bash +[[ -z ${TIMER_DOWN:-} ]] +STUB +cat >"$BIN_DIR/launcher" <>"$TMP_DIR/clicks" +STUB +chmod +x "$BIN_DIR/systemctl" "$BIN_DIR/launcher" + +STATE="$TMP_DIR/state.json" + +# state +state() { + jq -n --argjson locked "$1" --argjson pages "$2" --argjson behind "$3" \ + --arg reason "$4" \ + '{locked: $locked, reason: $reason, + pace: {target: 300, pages: $pages, behind: $behind}}' >"$STATE" +} + +run_block() { + PATH="$BIN_DIR:$PATH" BOOK_GUARD_STATE="$STATE" \ + BOOK_GUARD_DESKTOP="$BIN_DIR/launcher" bash "$BLOCK" +} + +line() { + sed -n "${1}p" <<<"$2" +} + +printf 'Checking book_guard block states...\n' + +state false 120 0 'on pace' +out=$(run_block) +assert_eq '๐Ÿ“– 120/300 โœ“' "$(line 1 "$out")" 'on pace text' +assert_eq '๐Ÿ“– 120' "$(line 2 "$out")" 'short text' +assert_eq '#50FA7B' "$(line 3 "$out")" 'on pace is green' + +state true 40 12 'behind the pace line' +out=$(run_block) +assert_eq '๐Ÿ“– 40/300 ยท LOCKED, 12p behind' "$(line 1 "$out")" 'locked text' +assert_eq '#FF5555' "$(line 3 "$out")" 'locked is red' + +state false 0 10 "today's lock was skipped with the escape hatch" +assert_eq '๐Ÿ“– 0/300 ยท 10p behind (escaped)' "$(line 1 "$(run_block)")" 'escaped tag' +state false 0 10 'today is a free day' +assert_eq '๐Ÿ“– 0/300 ยท 10p behind (free day)' "$(line 1 "$(run_block)")" 'free-day tag' +state false 0 10 'before the start date' +out=$(run_block) +assert_eq '๐Ÿ“– 0/300 ยท 10p behind (unlocked)' "$(line 1 "$out")" 'other tag' +assert_eq '#F1FA8C' "$(line 3 "$out")" 'behind but unlocked is yellow' + +printf 'Checking failure states are never blank or green...\n' + +state false 120 0 'on pace' +touch -d '-2 hours' "$STATE" +out=$(run_block) +assert_eq '๐Ÿ“– 120/300 โœ“ (stale 120m)' "$(line 1 "$out")" 'stale text' +assert_eq '#FFB86C' "$(line 3 "$out")" 'stale is orange' + +state false 120 0 'on pace' +out=$(TIMER_DOWN=1 run_block) +assert_eq '๐Ÿ“– 120/300 โœ“ โš  DISARMED' "$(line 1 "$out")" 'disarmed text' +assert_eq '#FF5555' "$(line 3 "$out")" 'disarmed is red' + +rm "$STATE" +out=$(run_block) +assert_eq '๐Ÿ“– no status' "$(line 1 "$out")" 'missing state' +assert_eq '#FFB86C' "$(line 3 "$out")" 'missing is orange' +printf '{' >"$STATE" +assert_eq '๐Ÿ“– no status' "$(line 1 "$(run_block)")" 'broken state' + +printf 'Checking a click opens the desktop window...\n' + +state false 120 0 'on pace' +BLOCK_BUTTON=1 run_block >/dev/null +for _ in 1 2 3 4 5 6 7 8 9 10; do + [[ -s "$TMP_DIR/clicks" ]] && break + sleep 0.1 +done +assert_eq 'clicked' "$(cat "$TMP_DIR/clicks" 2>/dev/null)" 'click runs the launcher' + +printf 'book_guard block: all checks passed\n' diff --git a/meta/requirements.txt b/meta/requirements.txt index 27538359..952758dc 100644 --- a/meta/requirements.txt +++ b/meta/requirements.txt @@ -6,7 +6,7 @@ add-trailing-comma==4.0.0 aiohttp==3.14.3 -anthropic==1.8.0 +anthropic==1.11.0 autoflake==2.4.0 autopep8==2.3.2 bandit==1.9.4 @@ -16,7 +16,7 @@ black==26.5.1 bottle==0.13.4 codespell==2.4.3 coverage==7.16.2 -cryptography==50.0.1 +cryptography==50.0.2 darglint==1.8.1 dead==2.1.0 defusedxml==0.7.1 @@ -25,7 +25,7 @@ fixit==2.2.1 flake8==7.4.1 flake8-annotations==3.2.0 flake8-bandit==4.1.1 -flake8-bugbear==26.9.9 +flake8-bugbear==26.9.30 flake8-comprehensions==3.17.0 flake8-docstrings==1.7.0 flake8-eradicate==1.5.0 @@ -35,10 +35,10 @@ flake8-pyi==26.5.0 flake8-pytest-style==2.2.0 flake8-return==1.2.0 flake8-simplify==0.30.0 -gatelock @ git+https://github.com/kuhyx/utils@gatelock-v0.8.2#subdirectory=gatelock +gatelock @ git+https://github.com/kuhyx/utils@gatelock-v0.9.0#subdirectory=gatelock genanki==0.13.1 -geopandas==1.1.4 -howlongtobeatpy==1.0.23 +geopandas==1.2.0 +howlongtobeatpy==1.0.24 hypothesis==6.168.3 importlib-metadata==9.0.1 interrogate==1.7.0 @@ -50,7 +50,7 @@ lxml==6.1.3 markdownify==1.2.3 matplotlib==3.11.2 mccabe==0.7.0 -mypy==2.3.1 +mypy==2.4.0 numpy==2.5.3 opencv-python==5.0.0.93 pillow==12.3.0 @@ -60,9 +60,9 @@ pre-commit==4.6.2 prospector==1.19.1 pycodestyle==2.15.0 pydocstyle==6.3.0 -pyflakes==4.0.0 +pyflakes==4.0.1 pygame==2.6.1 -pylint==4.0.9 +pylint==4.1.1 pyright==1.1.414 pytest==9.1.1 pytest-cov==7.1.0 @@ -77,12 +77,12 @@ radon==6.0.1 reorder-python-imports==3.17.0 requests==2.34.2 rich==15.0.0 -ruff==0.16.9 +ruff==0.16.10 # safety is deliberately not pinned: its only reference in this repo is a # commented-out pre-commit hook, and safety 3.8.1 declares `typer<0.26`, # which would hold the typer this repo actually imports below latest. secretstorage==3.5.0 -selenium==4.49.0 +selenium==4.50.0 typer==0.27.2 types-python-dateutil==2.9.0.20260807 types-PyYAML==6.0.12.20260906