pre-commit 4.7.0, pylint 4.1.3 and ruff 0.17.2 across crdt-sync,
earned_time, freedays, gatelock, music_theory and sickdays; numpy 2.5.4 in
music_theory (requirements, pyproject and the mypy/pylint hook deps). The
ruff-pre-commit rev follows to v0.17.2 in those configs and vmbox's, so the
hook lints with the version requirements.txt pins. No Python lockfiles exist.
All six suites at 100% coverage; every package's pre-commit config green.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NBScELfMUiieK9ManFbS3Y
Also fixes a ~5% flake in the hmac round-trip test (freedays and sickdays):
load_key strips whitespace, so a random key edged with a whitespace byte
never compared equal to the raw bytes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xfj1sxKH9VBPqpcAni8Jry
freedays 0.3.0 pinned crdt-sync v0.9.1 exactly, so any consumer that
moved to crdt-sync 0.9.2 (screen-locker) could not resolve both in a
clean venv. No code change.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018RHwVPyiiQMPh4M3JQwQku
ts_core and web_ui depend on `link:../ts_config`. pnpm links the directory
and installs nothing inside it, and a flat config imports its plugins from
the preset's own node_modules, so CI died on "Cannot find package
'@eslint/js'" while the local tree (where ts_config had been installed by
hand) passed. Both workflows now `pnpm install` in ts_config first, and
trigger on ts_config/** too so a preset change re-verifies its consumers.
The same push turned the dependency-freshness job red on five pins that had
aged underneath it (ruff 0.16.8 in four requirements.txt,
flutter_secure_storage 11.2.0 in crdt_sync_flutter); bumped, pubspec.lock is
ignored in that package so nothing else moves.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DnAYyNH1Sm4iJNidfYbrZJ
The three import-error findings were the hook not having crdt-sync in
its own venv; it is now pinned there as in pyproject.toml. The one
source finding is the background sync's documented fail-open net, which
must catch everything by design, so it carries the one suppression.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHCbkJWXHjtTUaqvnHRAqA
The three Python packages pinned ruff twice at different versions:
requirements.txt at 0.16.7, .pre-commit-config.yaml at v0.15.2. Only the
older one ever ran, so `select = ["ALL"]` was enforced two minor versions
behind. Measured before aligning: v0.15.2 reports 0 findings, every 0.16.x
reports CPY001 on each file -- 70 crdt-sync, 23 freedays, 71 gatelock, and
no other rule.
Resolved by adding the notice rather than ignoring the rule, so the gate
keeps its teeth.
Two gatelock modules sat at exactly the 250-line cap, so a one-line header
tipped them to 251. Both are split along a real seam rather than shaved:
_widget_fills.py colour arithmetic over LockConfig, no tkinter import
_surfaces_focus.py selects among existing surfaces, never mutates them
The focus split keeps _surfaces.py's documented containment (it stays the
only module that creates, moves or destroys windows), and the static guard
in test_recovery_invariants.py globs _surfaces*.py, so the new sibling is
covered with no list to extend. widgets.py re-exports the fills helpers,
which keeps gatelock.widgets the single public surface its tests import.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019kMvSMtsVMB3d2uoZ56pbp
sync_quietly exists so a background timer stays green when the pool
cannot be synced. It caught only SyncUnavailableError, which meant the
contract held only because of an upstream class hierarchy: today
FirebaseAuthError happens to subclass RemoteSyncError, and requests'
JSONDecodeError happens to subclass RequestException, so an expired
refresh token or a malformed payload does get wrapped and caught.
That is a coincidence, not a promise to this package -- and it is exactly
the kind that goes quiet the day upstream reorganises its exceptions. The
symptom would be a unit going red every 15 minutes, which is the outcome
this function's own docstring says the design exists to prevent.
It now also catches anything unexpected and logs it with a traceback, so
a real bug is loud in the journal while the timer stays green. The
narrow handler stays first and stays at debug level: offline is normal
on a laptop and must not fill the journal.
Two tests, neither of which the old suite could have failed on -- the
existing one monkeypatched get_client to raise the one exception already
known to be handled. One raises FirebaseAuthError *mid-sync* (a token
expiring during the tick rather than at client construction, which is the
realistic case per seed_session.py's own docstring), the other raises a
RuntimeError that no hierarchy covers.
111 tests, 100% branch coverage.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two things the pool needed to be real rather than merely correct.
**freedays no longer depends on gatelock.** It imported
gatelock.log_integrity for ~15 lines of HMAC, but importing gatelock runs
its __init__, which imports tkinter for the lock-window machinery. So a
headless policy library had acquired a hard dependency on a GUI toolkit,
and `import freedays` failed anywhere without Tk:
ImportError: libtk8.6.so: cannot open shared object file
A vmbox run caught it. The host masked it completely -- Tk is installed
here for the lockers -- and so did CI, whose own comment claimed "no
display needed" while the job would have needed libtk. freedays/_hmac.py
now does the signing with hashlib and hmac.
The format is deliberately unchanged (HMAC-SHA256 over compact
sorted-key JSON), so nothing has to be re-signed: all 8 entries already
in the real audit trail, written by gatelock's implementation, verify
against the new one. That was checked, not assumed.
This also dissolves the version diamond that shaped the app rollout:
freedays now pins only crdt-sync, so it can never conflict with whatever
gatelock tag an app carries.
**The pool now syncs on a schedule.** A `sync-quiet` subcommand plus a
systemd user timer every 15 minutes, because "it syncs" was otherwise
only true if you typed `freedays sync` yourself -- a day marked on the
phone would sit there. sync-quiet exits 0 when there is no network or no
credential, on purpose: a unit that goes red every time the laptop is
offline is a unit you learn to ignore, and a real bug still reaches the
journal through the library's logging.
install.sh gives freedays its own venv rather than borrowing an app's --
the pool is shared by five apps, so binding its CLI to one of them would
make "is freedays installed?" depend on something unrelated, and
uninstalling that app would take the command with it.
Verified in vmbox first (systemd), then on this host: the timer is
enabled and scheduled, and the service *finishes* rather than failing
when there is no remote.
seed_session.py gains freedays, without which the one command that can
give the pool a Firebase session would reject it outright (--app is
choices-validated) and the pool would stay local-only with nothing
saying why. That file was already exactly at the 250-line cap, so the
app list and its rationale move to tool/_seeded_apps.py.
freedays: 109 tests, 100% branch coverage. crdt-sync: 433 pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Five apps (diet-guard, wake-alarm, screen-locker, leetcode-guard,
home-guard) each enforce something daily, and none of them had a way to
say "not today". screen-locker was the only one with any notion of a day
off, and it had two overlapping ones: a budgeted sick day designed to be
hard to take, and an undocumented scheduled_skips.json that no other app
could see.
This is the second thing generalised, not the first. Free days are
pre-declared, carry no justification, allow consecutive runs, and are
capped only by an annual budget -- 35 per calendar year, resetting Jan 1.
Sick days stay in screen-locker exactly as they are: their rolling
windows (1/7d, 3/30d, 10/90d) exist precisely to forbid the consecutive
runs a holiday is made of, so merging the two policies would have had to
ruin one of them.
One global pool: a free day is free for every app at once, so there is
nothing per-app to mark and nothing per-app to forget.
The integration is one call, `freedays.is_free_day()`, which reads a
single local JSON file and never the network -- a gate fires from a
systemd timer and a hung sync must not be able to hold one open or shut.
It returns False for anything it cannot parse, so an unreadable pool
means the normal rules apply rather than everything switching off.
Two design points worth keeping:
- Un-marking flips a `state` field rather than tombstoning the record.
crdt-sync deletes are monotonic, so a tombstoned date could never be
marked free again.
- `consumed` is a sticky flag. Releasing a day that has not arrived
refunds it; releasing today does not, because those hours were already
unguarded. It is OR-ed against the stored value on every write so two
devices either side of midnight cannot hand a spent day back.
Timezone is local, decided in _day.today() and nowhere else. The apps
disagreed about this before: screen-locker's lock chain computes the day
in UTC while add_scheduled_skip.py uses local time, so a skip added near
midnight could name a date the chain never matched.
94 tests, 100% branch coverage, no suppressions.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>