steam-backlog-enforcer/DOCS-next-session.md
Krzysztof kuhy Rudnicki 965abcaeaa 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:54:34 +02:00

166 lines
8.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Next session: manual-pick rules, then finish the 250-line cap
Paste this whole file as the first prompt of a fresh session. Everything needed
is below; you should not need the previous session's transcript.
Repo: `~/steam-backlog-enforcer`. Work on `main`. HEAD is `0f25fba`.
---
## Task 1 (do this first): change the manual-pick rules
kuhy hit this and wants it changed:
```
./run.sh pick-manual 3164500 -> Error: you already have 3 manual pick(s).
./run.sh abandon-pick 495890 -> Grace period EXPIRED ... closed 3.7 day(s) ago.
./run.sh abandon-pick 618510 -> Grace period EXPIRED ... closed 3.3 day(s) ago.
```
He is stuck: three picks locked in, and the 4-day grace window closed on two of
them, so nothing can be abandoned and no new pick can be added.
**The change he asked for, verbatim:**
"allow to abandon pick at any time but reduce number of manual picks to max of 2"
So:
1. **Abandon at any time** — drop the grace-window restriction entirely.
- `_manual_pick_lifecycle.py:73` `can_abandon_manual_pick()` currently returns
`remaining is not None and remaining > 0`. It should return whether the pick
exists at all, regardless of elapsed time.
- `_manual_pick_lifecycle.py:87` `abandon_manual_pick()` has the matching
guard; the docstring says "if still inside its grace window" — update it.
- `MANUAL_GRACE_DAYS = 4` is declared in **two** places:
`_actions.py:33` and `_manual_pick_lifecycle.py:40`. Decide whether the
constant still has a purpose after this change (see "open question" below)
and do not leave two divergent copies.
- `manual_pick_grace_remaining()` (`_manual_pick_lifecycle.py:47`) also feeds
`status_payload()` at line 162 as `grace_days_left`, and `run.sh status`
prints "undoable for N more day(s)". If grace no longer gates anything,
that display is now a lie — either drop it or restate it.
2. **Max picks 3 → 2** — `config.py:63`, `max_manual_picks: int = 3`.
- kuhy's `~/.config/steam_backlog_enforcer/config.json` does **not** set this
key, so it takes the default; changing the default is sufficient and no
config migration is needed. (Verified.)
**Open question — ask kuhy before implementing, it changes the design:**
He currently has **3** picks locked and the new max is **2**. What should happen
to the existing over-cap state? Options: leave the 3 in place and simply refuse
new picks until he abandons down to 2 (least surprising); or force him to
abandon one now. `manual_pick_slots_left()` (`_actions.py:89`) returns
`max_picks - len(picks)` and will go negative — check every caller handles that,
particularly `main/picks.py:109`.
**Verify by running, not just tests** (repo CLAUDE.md requires this):
```
./run.sh status # read-only, safe
./run.sh abandon-pick 495890 # MUTATES STATE - confirm with kuhy first
```
`./run.sh` with **no args defaults to `done`**, which completes the current game
and reassigns. Never run it bare. `status` and `list` are the read-only ones.
---
## Task 2: finish the 250-line cap campaign (user-approved, in progress)
Three files remain. The gate is `bash scripts/check_file_length.sh --all`
(**trust the exit code only** — `| tail` and `grep -c` both report misleadingly).
```
_stats.py: 523 lines (over by 273)
scanning.py: 521 lines (over by 271)
_web_dataset.py: 436 lines (over by 186)
```
Everything else in `pre-commit run --all-files` already passes, so these three
are the only thing between the repo and a working `git push`.
### The method that works: leaf-set extraction ONLY
Extract functions that call **nothing else in their own module**. That keeps
every new import one-way, so no cycle is possible. Splitting on any other
boundary produced circular imports three separate times, and one of them broke
`./run.sh` while kuhy was using it.
Helper scripts from the last session (outside the repo, so they don't dirty git):
`/tmp/claude-1000/-home-kuhy/0f40b913-131e-43b9-b353-23de8c80a8c8/scratchpad/`
- `leaf_split.py` — run with just a module name to LIST its leaves; add
`--new <mod> --doc "..." --names ... --apply` to perform the split.
- `missing_names.py` — reports names a module uses but never binds. Run it on
`steam_backlog_enforcer/*.py` after every split; this catches the fallout
immediately instead of via 30 confusing test failures.
- `retarget.py`, `add_getattr.py` — of limited value, see the warning below.
If `/tmp` has been cleared, `leaf_split.py` is ~90 lines and easy to rewrite:
parse the module, find `FunctionDef`s whose bodies call no sibling function,
move those plus a copy of the module header into a new file, and add a
top-level `from ... import (...)` back in the original.
### Traps that cost the last session hours
- **`_web_dataset` is the hard one.** Its dataclasses (`WebGame`,
`WebStateInfo`, `WebDataset`, `PaceVsHLTB`, `DefaultSummary`, `WebDefaults`)
are used by both the parent and the helpers, so they must go to their own
**leaf model module** that both import — not stay in the parent. Getting this
wrong is what broke `run.sh`.
- **`ruff-format` silently deletes bottom-of-file re-export imports** (the
comment above them survives, so it looks intact). Put re-exports at the top,
or serve them through a module `__getattr__`.
- **A module `__getattr__` re-export** needs: `importlib.import_module` (a call,
so no `PLC0415`), and a `_Reexport: TypeAlias = Any` return annotation —
`object` fails mypy at the call sites, bare `Any` fails ruff `ANN401`.
Working examples already in the tree: `_actions.py`, `_whitelist.py`,
`hltb.py`, `_cmd_done.py`.
- **Patch-target rule**: the target is the module whose **source file contains
the function under test** — not where a helper is defined, not where the test
imports from. Fix these **per file from the pytest failure list**. Six
attempts at bulk regex retargeting had a net effect of −11 tests; per-file
diagnosis worked every single time.
- **`@dataclass` decorators**: when copying a module header, include decorator
lines in the boundary or you strand a bare `@dataclass` (fails with a
confusing `'function' object has no attribute '__mro__'`).
- **Stale `.mypy_cache`** after moving symbols → `AssertionError: Cannot find
component`. `rm -rf .mypy_cache`; it is not a real error.
### Non-negotiables
- **Use `.ci-mirror-venv/bin/python` for pytest**, not system python3 — the
latter has mcp 2.0 against a pinned mcp<2, so `test_mcp*.py` cannot collect
and coverage reads ~97% instead of 100%.
- Baseline to preserve: **1347 tests, 100.00% coverage**.
- **Verify with the coverage run before committing**, not `--no-cov`. Last
session committed 3 failures after misreading a `--no-cov` run as green.
- Never `--no-verify`. Never add `# noqa` without asking.
- **The service auto-restarts from this tree** (`Restart=always`,
`PYTHONPATH` pointed here). After any edit that could break imports, run
`PYTHONPATH=. python3 -c "import steam_backlog_enforcer.main"` before moving
on, and check `systemctl show steam-backlog-enforcer -p NRestarts --value`.
- **Test runs can write to real config.** Two leaks were found and sealed last
session (`exception_audit.log`, `owned_app_ids_cache.json`), both caused by a
path constant moving to a new module while `tests/conftest.py` kept patching
the old one. After any split that moves a module-level path constant, md5sum
`~/.config/steam_backlog_enforcer` before and after a full suite run.
### When the cap is clear
`git push` (13 commits are waiting, including another session's `2ad8752` for
`install.sh`). The pre-push ci-mirror runs `pre-commit --all-files` over the
whole tree, which is why the cap has to be fully clear first.
---
## Context: what the last session actually fixed (already committed, verified)
`21fe2f7` + `919ecdf` — the enforcer was launching Steam **as root**, so Steam
answered with a `zenity` modal reading "Cannot run as root user" on kuhy's
screen. Root cause: `game_install._get_real_user()` read only `SUDO_USER`/`USER`,
neither of which systemd sets, so it returned `None` and every
`geteuid() == 0 and real_user` guard fell through to running as root. It also
caused a retry storm — 1656 attempts in 30 minutes at 1.5 GB RSS. Both fixed and
verified in production (`sudo -u kuhy env DISPLAY=:0 ... steam`, every Steam
process at uid 1000, 0 retries, ~244 MB).
Do not re-litigate that fix; it is done. Tasks 1 and 2 above are what remains.