Commit Graph

182 Commits

Author SHA1 Message Date
cdee896d43 Apply sync tombstones across the device union, not per device log
The previous commit dropped tombstoned records while reading each device
log, which was not enough: _merge_device_records unions across devices
keeping the highest HLC, so a record tombstoned on one device and still
live on another was simply absent from one dict and won the merge from
the other -- ingested exactly as before.

Collect tombstoned ids across every device log, then suppress by id
after the union. crdt-sync makes deletion monotonic inside its own merge
for this reason; this reader rolls its own merge, so it has to honour
the same rule itself.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018M5gKyYzxWG6QkCaj1wWfk
2026-08-21 13:38:52 +02:00
590f606856 Honour crdt-sync tombstones when ingesting synced workouts
_records_matching read every record's payload and never checked
record.deleted, so a tombstoned workout was ingested like a live one.
Deleting a workout in the phone app therefore looked like it worked and
came back on the next 15-minute sync, silently re-crediting it.

crdt-sync deletion is monotonic (a tombstone cannot be resurrected by
merging an older, non-deleted copy), so this is the only durable way to
retract a synced workout across devices. Skip tombstoned records and say
so at info, since "this stopped counting" is exactly the kind of thing
that must not happen silently.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018M5gKyYzxWG6QkCaj1wWfk
2026-08-21 13:35:53 +02:00
426b9372be Opt in to preempting a weaker gatelock holder
gatelock just made preempt_weaker_holder opt-in (it defaulted to True,
which let wake_alarm evict this armed lock). screen_locker is the app
that actually needs it: on 2026-08-21 diet_guard held the global grab
from 07:04 to 09:07+ while this locker retried against it, which is the
contention behind the reported lockout.

Production only -- demo mode uses a local grab and never contends.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018M5gKyYzxWG6QkCaj1wWfk
2026-08-21 13:31:57 +02:00
390e916922 Finish the RunnerUp sport-allowlist removal; isolate the instance lock in tests
Some checks are pending
pre-commit / pre-commit (push) Waiting to run
Tests / test (3.10) (push) Waiting to run
Tests / test (3.11) (push) Waiting to run
Tests / test (3.12) (push) Waiting to run
Tests / lock-popup-safety (push) Waiting to run
release-apk / release (APK) (push) Waiting to run
A prior commit (6a7a7d8) accidentally captured only the _constants.py
half of an already-in-progress, uncommitted change removing
RUNNERUP_ACCEPTED_SPORTS -- leaving main broken (import error) since
_runnerup_verification.py and its 15 test files still referenced the
now-undefined constant. Committing the matching half: RunnerUp
entries are now judged purely by distance/duration, any sport
qualifies, matching how every other workout type is already treated.

Also: the new single-instance lock was not test-isolated, so every
ScreenLocker built across the suite contended for the same real
$XDG_RUNTIME_DIR/gatelock file -- only the first test won it, the
rest silently exited. Added to conftest.py's isolated-state table
(split into tests/_isolated_state.py to stay under the 250-line cap)
and added dedicated coverage for screen_locker/_instance.py.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018M5gKyYzxWG6QkCaj1wWfk
2026-08-21 13:17:26 +02:00
957e4566c2 Bump gatelock pin in pre-commit's pylint hooks to v0.7.1
Missed alongside the main dependency bump -- pre-commit's pylint
hooks pin gatelock separately via additional_dependencies, so they
were still resolving wait_for_turn against the old v0.4.0 tag.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018M5gKyYzxWG6QkCaj1wWfk
2026-08-21 13:09:34 +02:00
6a7a7d8933 Queue behind stronger lockers, single-instance guard, fix budget-crash in fit check
Root-caused the 2026-08-21 stuck-lock incident: a manual workout
unlocked correctly, but the mechanism behind "locked again seconds
later" was a stray workout-locker.service re-arm racing a second
process into building a competing lock window -- not a leetcode-guard
conflict as first suspected (its timer wasn't due until 13:00 that
day). Separately, diet_guard held the global grab for ~3 minutes
against a newly-arriving, higher-ranked screen_locker that morning,
which just retried and logged instead of making progress.

- _build_lock_window now calls gatelock.wait_for_turn before arming,
  so a stronger incumbent is queued behind instead of raced.
- New screen_locker/_instance.py (flock-based, mirrors
  leetcode_guard/diet_guard's own single-instance guards) stops a
  second process from ever building a competing window in the first
  place.
- Bumped to gatelock v0.7.1, which adds weaker-holder preemption
  (SIGTERM, reusing the holder's own clean close() path) so a lower-
  ranked incumbent can no longer block a stronger claim for minutes.
- Fixed a live crash in scripts/verify_screen_fits.py: it read the
  real log.json, so today's exhausted manual-workout budget (itself
  caused by the incident's duplicate entry) made the fit-check crash
  on an unset _mw_error_label. Points at a path that can never exist
  instead, so the measurement no longer depends on live data.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018M5gKyYzxWG6QkCaj1wWfk
2026-08-21 13:04:54 +02:00
c1c73d43ba Stop the relaxed-day popup from re-firing every 30 minutes after Skip
Some checks failed
pre-commit / pre-commit (push) Has been cancelled
Tests / test (3.10) (push) Has been cancelled
Tests / test (3.11) (push) Has been cancelled
Tests / test (3.12) (push) Has been cancelled
Tests / lock-popup-safety (push) Has been cancelled
release-apk / release (APK) (push) Has been cancelled
workout-locker.timer (added in e5db254) re-runs the full lock-decision
chain every 30 minutes, but is_relaxed_day() is a pure date check with
no memory of an earlier same-day dismissal. Clicking "Skip — No
Penalty" only closed the window without persisting anything, so the
next tick re-derived reason=relaxed_day and rebuilt the same popup.

Now the skip button writes a relaxed_day_skip marker to the log
(excluded from weekly counting and sync, same treatment as heat_skip),
and the startup chain checks for it before re-entering the relaxed-day
branch. Also renames workout_log.json to log.json throughout the repo,
tests, docs, and Dart comments, since the fix touches that file anyway.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qH2hJ6mvCJd3ZXpAJSaF7
2026-08-18 21:55:59 +02:00
e5db25468d Re-arm the locker, and make a disarmed enforcer impossible to miss
Some checks are pending
pre-commit / pre-commit (push) Waiting to run
Tests / test (3.10) (push) Waiting to run
Tests / test (3.11) (push) Waiting to run
Tests / test (3.12) (push) Waiting to run
Tests / lock-popup-safety (push) Waiting to run
release-apk / release (APK) (push) Waiting to run
The screen locker stopped locking on 2026-08-04 and nobody could tell. Not a
logic bug: every predicate worked the whole time. systemd logged

  timers.target: Found ordering cycle: early-bird-workout-check.timer/start
    after graphical-session.target/start after basic.target/start after
    timers.target/start - after early-bird-workout-check.timer
  timers.target: Job early-bird-workout-check.timer/start deleted to break
    ordering cycle starting with timers.target/start

and left the timer disabled. A timer that is WantedBy=timers.target must not be
ordered After= a target that itself comes after timers.target; systemd resolves
that loop by deleting the job, which is an absence, and absences page nobody.

That timer is the ONLY thing that closes the early-bird window. Each morning a
run inside the window banked early_bird_pending.json, logged "will re-check at
08:30", and exited 0. The re-check never came, so the marker stood all day,
every day, for thirteen days.

It stayed invisible because logging.basicConfig was called only in the
--sync-only branch, never for --production -- the mode systemd actually runs.
Journal proof: zero INFO lines from workout-locker.service since June. Every
early exit logged at INFO into a logger with no handler. Meanwhile the healthy
15-minute sync kept the system looking alive.

The 2026-08-15 Python split is NOT implicated: it was pure code motion, and
basicConfig already sat in that branch before it.

Fixes:

* Drop the After= that closes the cycle; Persistent=true so a machine asleep at
  08:30 still re-checks after resume.
* Add workout-locker.timer (*:00,30). The service is WantedBy=
  graphical-session.target, i.e. one decision per login, so the 08-16 -> 08-17
  day and ISO-week rollover inside one long session was never re-evaluated.
* arm.sh: install units, enable timers, verify -- one command, because
  re-arming was a four-command sequence that was easy to half-do, and
  `systemctl enable` returns 0 while systemd deletes the timer job.
* check_enforcement_armed.py asks systemd what it actually scheduled (enabled
  AND has a next trigger) and fails loudly otherwise. Wired as ExecStartPost on
  workout-sync.service, so a disarmed locker marks that unit failed every 15
  minutes instead of scrolling past. install_systemd.sh now fails on it too.

Logging (the reason this went unnoticed, so treated as a defect in its own
right):

* basicConfig for every mode, never one branch.
* Every enforcement-abandoning exit records a decision instead of an invisible
  INFO: one greppable line -- DECISION lock=no reason=early_bird_window_active
  weekly=0/5 -- at WARNING, since per CLAUDE.md an INFO explanation of a
  non-action is a silent failure with extra steps. The enforced case is logged
  too: "no line at all" was the outage's whole signature.
* A durable JSONL trail outside the repo (~/.local/share/screen_locker/), so
  "when did this last actually enforce?" survives journal rotation. --status
  and --sync-only record lock=n/a; "did not decide" must never look like
  "decided not to lock".
* check_silent_failures.py now also catches sys.exit(0) that explains nothing.
  Verified both directions: it flags 3/3 exits in the pre-fix _auto_upgrade.py
  and passes the repo clean.

Sync noise: four records replayed as WARNINGs every cycle (~384/day), which is
how the actionable lines got tuned out. manual_workout:2026-08-15 is an empty
stub ({date, kind, type}) -- no workout to recover, and that day's credit was
already counted under a different sync_record_id, so no credit was ever lost.
The three "collided" records are dedup working. Both now report at info; a
WARNING from this logger again means something real. 4 -> 0 per cycle.

Verified on the live system: the periodic timer fired at 16:45 and the journal
shows `DECISION lock=no reason=scheduled_skip_day weekly=0/5` -- the line that
did not exist for thirteen days. Both timers report enabled and scheduled, and
no ordering cycle recurs. Today is a scheduled skip so the machine was not
locked mid-session; enforcement resumes tomorrow 08:30. 1007 tests, 100%
coverage, all hooks green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F2b6ZjBcdrkSYnMujx4mWd
2026-08-17 16:47:25 +02:00
b1ee78be64 Add dopamine-ux session prompts to prompts/
Some checks are pending
pre-commit / pre-commit (push) Waiting to run
Tests / test (3.10) (push) Waiting to run
Tests / test (3.11) (push) Waiting to run
Tests / test (3.12) (push) Waiting to run
Tests / lock-popup-safety (push) Waiting to run
release-apk / release (APK) (push) Waiting to run
Copy of the canonical prompts in ~/utils/dopamine-ux/, distributed here so the
repo can be opened and the prompt run directly. Derived from a survey of this
repo plus "The Dopamine Architecture" (london-post.co.uk).

Each prompt states its own completion protocol: when finished, the session
deletes this copy in the implementation commit and logs a DONE row in
~/utils/dopamine-ux/00-INDEX.md. Partial completion is never recorded as done.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FdApz7EZ1zDfvtvvi1idDv
2026-08-16 21:12:52 +02:00
22294ec541 Enforce the 250-line cap for real, and on Dart
Some checks are pending
pre-commit / pre-commit (push) Waiting to run
Tests / test (3.10) (push) Waiting to run
Tests / test (3.11) (push) Waiting to run
Tests / test (3.12) (push) Waiting to run
Tests / lock-popup-safety (push) Waiting to run
release-apk / release (APK) (push) Waiting to run
The gate this whole refactor was aimed at had two holes:

- `MAX_LINES` was 400, while `refresh_refactor_todo.py` reported against 250.
  The hook and the TODO generator disagreed about what "over the cap" meant.
- `types_or` was [python, shell], so the hook had never inspected a single
  Dart file. Every file in the refactor — workout_screen.dart at 633 lines
  included — was exempt from the check it was being split to satisfy.

A violation also exited 1 while printing nothing, so a developer got a failed
commit with no indication of which file or by how much. It now names the file
and the overage on stderr.

Verified by planting a 260-line Dart file: the hook exits 1 and reports it.
Boundary checked at 250 (pass) / 251 (fail).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EveB813BQ8abAP68VSqZ5Y
2026-08-15 22:42:57 +02:00
a7ca5bfbb4 Split settings_screen.dart (503 -> 211) — the file-length gate now passes
Last of the ten. The ~190-line `build()` was the bulk, so it became five
private StatelessWidgets rather than list-building methods, per the note in
the todo that a previous attempt to slice them out as a raw widget-list getter
produced unbalanced brackets.

  settings_screen.dart                    211  state, lifecycle, setState paths
  settings_screen_sections.dart           190  SYNC, OFFLINE BACKUP, app bar
  settings_screen_exercise_sections.dart  161  WEIGHTS, TARGET REPS, THRESHOLDS
  settings_screen_widget.dart              89  the public widget + 11 seams
  settings_screen_actions.dart             86  setState-free actions

Every conditional in the new widgets is the one `build()` already had, in the
same shape: no defaulted parameters, no `??` fallbacks, no convenience null
checks. That was deliberate — the earlier workout_screen pass showed a single
added `if (x == null) return` guard is enough to drop the 100% gate, because
it is a line no existing test reaches.

`_openSyncSettings` split the way `_finishWorkout` did: the navigation half is
setState-free and moved to the actions extension, the post-return progression
pull keeps the setState and stays on the class.

`bash ~/utils/scripts/check_file_length.sh --all` now exits 0. 402 tests pass
with a byte-identical name set, dart analyze clean, coverage 100%
(2765/2765).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EveB813BQ8abAP68VSqZ5Y
2026-08-15 22:36:23 +02:00
b6414c7f1c Split home_screen.dart (260) and manual_workout_screen.dart (313)
Both by pattern B, both cut along the same line: whatever does not call
setState can live in a part extension, whatever does must stay on the class.

home_screen.dart 260 -> 228
  home_screen_navigation.dart (42) takes `_openSyncSettings` and
  `_openWorkout` — the two routes the screen pushes, each ending in the same
  `unawaited(_load())` reload. `_load` and `_refreshSyncStatus` stay: both
  setState.

manual_workout_screen.dart 313 -> 245
  manual_workout_form_parts.dart (83) takes the form's non-interactive
  building blocks — `_budgetBanner`, `_section`, `_text` — plus the
  `_int`/`_str` field readers and the `_draft()` assembler they feed. The
  sport dropdown and RPE slider stay on the class; both setState on change.

402 tests pass, dart analyze clean, coverage still 100%.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EveB813BQ8abAP68VSqZ5Y
2026-08-15 22:30:27 +02:00
ac7a7afa08 Extract WorkoutSyncSession from workout_sync_service.dart (322 -> 218)
The one file on the list that could not be fixed by moving code, so the
approach was agreed with the user first: extract a real collaborator rather
than change the public surface.

`push`, `pushManual` and `syncNow` were three near-identical bodies — same
configured-or-bail guard, same GitHubClient/Firebase/MirrorStore construction,
same getFileText/mergeLogs/syncLog round-trip, same catch-everything-and-report
handler, same close-both finally. They differed in the record they merged in
(one, one, none) and two strings. That shared body is now
WorkoutSyncSession.run(), and the three methods are thin wrappers holding only
their own guard and their own wording.

Why a collaborator and not a `part` extension: extension methods dispatch
statically. Four fakes in _home_test_fixtures.dart override `syncNow`, and
manual_workout_screen_test.dart overrides `pushManual` and
`readMergedManualPayloads` — with those in an extension the real
implementations would run while the fakes sat unused, which is how four tests
hung on pumpAndSettle in an earlier attempt. All four remain ordinary methods
on WorkoutSyncService, so every override still binds.

`successReason` and `failurePrefix` are passed in rather than derived from one
another, so the exact strings the tests assert on ('pushed' / 'push failed',
'synced' / 'sync failed') stay under the callers' control.

Unlike the part files, this is a real library with its own lcov SF: entry, so
it has to carry its own coverage — it does. The total drops 2751 -> 2728
covered lines, which is the duplication the extraction removed.

402 tests pass; dart analyze clean; coverage 100%.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EveB813BQ8abAP68VSqZ5Y
2026-08-15 22:28:02 +02:00
1e4719b4de Split workout_screen_test.dart (362) into three
Last of the four test files. `_pump`, `_tapReal` and `_longPressReal` were
locals inside `main()`; they and the top-level `_completeSaved` / `_wrap` /
`_exercises` moved into _workout_screen_test_fixtures.dart.

  workout_breaks_test.dart              201  breaks, long-press, finish,
                                             thresholds
  workout_screen_test.dart              141  render, buttons, restore
  _workout_screen_test_fixtures.dart     73  the six hoisted helpers

Two mistakes worth naming, both caught by the test count rather than by a
failing assertion — a file that will not compile reports as
"loading <file>" and silently drops its tests:

- `_exercises` is referenced from a doc comment and a test body, so leaving it
  private in the fixtures broke the load. It is `testExercises` now.
- The first cut landed inside the runAsync explanation comment that sits
  between the last render test and the first interaction test, taking the
  `setUp` closing brace with it. The comment explains why the interaction
  tests need runAsync, so it moved to the breaks file where those tests are.

402 tests pass; name set gains only the new file's reporter lifecycle records.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EveB813BQ8abAP68VSqZ5Y
2026-08-15 22:20:56 +02:00
e2721b5a29 Split home_screen_test.dart (383) into three
The file's four post-`main()` declarations were the blocker: FailingSyncService,
OkSyncService, HangingSyncService and CountingSyncService all
`extends WorkoutSyncService` and override `syncNow`. They moved to
_home_test_fixtures.dart together with the `_pump` / `_wrap` locals, so both
halves bind to one definition of each.

  home_screen_test.dart      185  navigation, resume, start-workout
  home_sync_card_test.dart   172  the sync-status card and its Retry tick
  _home_test_fixtures.dart    66  pumpHome, wrapHome, the four fakes

Worth recording for the workout_sync_service.dart work still to come: these
four fakes are exactly why `syncNow` cannot move into a `part` extension.
Extension methods dispatch statically, so a subclass override of an extension
member never runs — the real implementation would, and the tests would hang
rather than fail.

402 tests pass; the name set gains only the new file's reporter lifecycle
records.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EveB813BQ8abAP68VSqZ5Y
2026-08-15 22:16:57 +02:00
786d261d9e Split history_screen_test.dart (386) into three
This is the file the previous session's splitter could not handle: `_pump`,
`_wrap`, `_seed` (and `_syncMock`) were local functions declared inside
`main()`. A textual split copies the preamble into both halves, and the second
half's references then resolve against the wrong closure — which is why the
file was left on the list rather than split badly.

Hoisted all four to top level in _history_test_fixtures.dart, renamed to
pumpHistory / wrapHistory / seedSession / historySyncMock, then split by
subject:

  history_chart_test.dart        221  chart, drill-down, PC-synced rows
  history_screen_test.dart       117  app bar, list, calendar, tile states
  _history_test_fixtures.dart     99  the four hoisted helpers

None of the four closed over `main()` state, so hoisting is a pure move; the
runAsync/sqflite-ffi comment on `seedSession` travels with it, since that is
the non-obvious part a future reader needs.

402 tests pass. The name set gains only the new file's `(setUpAll)` /
`(tearDownAll)` reporter records.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EveB813BQ8abAP68VSqZ5Y
2026-08-15 22:14:16 +02:00
a4b189ee3b Split progression_sync_service_test.dart (426) into three
Same two-step shape as the workout-sync split, done in one commit because the
fixtures here are smaller and have a single consumer each.

  _progression_test_fixtures.dart   101  FakeStore, ExplodingStore, seedRemote,
                                         hlcAt, markSynced
  progression_sync_push_test.dart   213  pushProgression, active session, result
  progression_sync_service_test.dart 155  paths, pullProgression

`_ExplodingStore` was the file's one post-`main()` declaration — the thing
that kept it outside the splitter's envelope — and is now a fixture.
`markSynced` was a local function inside `main()`, so it moved too rather than
being copied into both halves where the two could drift.

402 tests still pass. The test-name set gains exactly two entries,
`(setUpAll)` and `(tearDownAll)` for the new file; those are framework
lifecycle records in the JSON reporter, not tests, and the +402 count is
unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EveB813BQ8abAP68VSqZ5Y
2026-08-15 22:12:04 +02:00
1e72b61826 Split workout_sync_service_test.dart (740) into four
The largest file on the list. Now that the shared helpers live in
_sync_test_fixtures.dart, the two post-`main()` group functions were already
self-contained and became their own files rather than needing the splitter:

  workout_sync_service_test.dart          195  push / pushManual
  workout_sync_read_test.dart             238  readMerged*, token recovery
  workout_sync_manual_survival_test.dart  174  Firebase-connect survival
  workout_sync_cutover_test.dart           92  dual-write cutover

Each new file gets its own `TestWidgetsFlutterBinding.ensureInitialized()`.
That was previously supplied once by the shared `main()`, and
`installFakeSecureStorage` touches the test binary messenger — without it the
files would fail to load, which shows up as a dropped test count rather than
a red assertion.

402 tests pass and the test-name set is byte-identical to the baseline, so
nothing was lost, duplicated or silently skipped in the move.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EveB813BQ8abAP68VSqZ5Y
2026-08-15 22:09:54 +02:00
0c49039378 Move the workout-sync test helpers into _sync_test_fixtures.dart
Groundwork for splitting workout_sync_service_test.dart (740). The file's
shared helpers had to leave it first: `_cutoverTests` and
`_manualWorkoutSurvivalTests` are declared after `main()` and both reach back
into preamble helpers, so any split would have stranded one side of the file
from the fixtures it calls.

Moved and made public (a sibling import cannot see `_`-private names):
`_response`→syncResponse, `_fileContaining`→fileContaining, `_PutCall`→PutCall,
`_mockGitHub`→mockGitHub, `_session`→workoutSession, `_logPut`→logPut,
`_service`→syncService, `_fakeFirebase`→fakeFirebase.

`_manual` was a *local* function inside `main()` — the exact shape that broke
history_screen_test.dart last time, where both halves of a split get the
preamble copy but the second half's references resolve wrong. It is now the
top-level `manualRecord` in the fixtures file, so it survives the split.

Pure move: no test added, removed, renamed or reordered. 402 tests pass and
the test-name set diffs byte-identical against the pre-change baseline.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EveB813BQ8abAP68VSqZ5Y
2026-08-15 22:07:36 +02:00
78db299086 Split workout_screen.dart (633) into seven part files
The largest lib/ file on the 250-line list and the one never attempted. It
has no private widget classes to lift out (pattern A did not apply), so the
cut is by responsibility:

  workout_screen.dart        250  state fields, lifecycle, setState methods
  workout_screen_session.dart 182  save/restore, break maths, threshold write
  workout_screen_finish.dart  110  the write-everything finish step
  workout_screen_body.dart    105  exercise list + break banner
  workout_screen_dialogs.dart  73  finish/reset confirmations
  workout_screen_appbar.dart   68  clock + Reset/Finish actions
  workout_screen_breaks.dart   67  rest timers, threshold edits

Two constraints shaped it. `setState` is `@protected` and unreachable from an
extension, so methods that call it either stay in the class or split into a
class-side wrapper plus a setState-free body — that is why `_finishWorkout`
keeps only its three lines and delegates to `_persistFinishedWorkout`. The
breaks extension needs real state writes, so the class exposes one named
`_applyBreakState` shim rather than scattering the seam.

The extensions are named `_Foo`, not `Foo`: extending the private
`_WorkoutScreenState` from a public extension trips
`library_private_types_in_public_api`.

Coverage caught one real regression mid-way. Restructuring the threshold
update as a map-returning helper with an `if (s == null) return` guard added
a branch no test exercises and dropped the gate to 2753/2754. Rewritten as
`_writeThresholds`, mutating in place with the original `if (s != null)`
shape, so the extraction stays a pure move.

dart analyze clean; 402 tests pass with a byte-identical test-name set
(captured via --reporter json and diffed, since a count alone cannot tell a
lost test from a duplicated one); flutter coverage back to 100%.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EveB813BQ8abAP68VSqZ5Y
2026-08-15 22:04:11 +02:00
99302c2be7 Add todo_finish_dart_250_prompt.md and the Dart test splitter
A self-contained prompt for a fresh session to finish the last 10 files over
the 250-line cap, all Dart. It carries the parts that are expensive to
rediscover:

- scripts/split_dart_test.py, which works, plus the envelope it works in:
  one pass from the pristine file, no post-main() declarations, no local
  function declarations in the preamble. All four remaining test files sit
  outside that envelope, and the fix (hoist shared fakes into a
  _fixtures.dart sibling first, as its own commit) is written down.
- The three Dart constraints each of which cost a debugging cycle here:
  setState is @protected so it cannot be called from an extension; extension
  methods dispatch statically so a method a test fake overrides must stay in
  the class (this hung four tests on pumpAndSettle rather than failing an
  assertion); static members need qualifying inside an extension.
- 402 as the test-count invariant, and the note that dart analyze is a no-op
  on test/ so the count is the only signal there.
- That the local gatelock (0.5.0) is ahead of the pin (0.4.0), so the popup
  gate's failure here is environment drift and must NOT be "fixed" in code.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EveB813BQ8abAP68VSqZ5Y
2026-08-15 21:49:15 +02:00
96f6e5f8cb Refresh the refactor todo: 41 -> 10 files over the cap
Regenerated from the gate, so the resume state matches the tree: 6,753 lines
over the cap at the start of this pass, 1,828 left.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EveB813BQ8abAP68VSqZ5Y
2026-08-15 21:46:16 +02:00
2bb84f6d1b Split progression_sync_service.dart (433) into four
progression_sync_service.dart          client, record IO, JSON mapping
  progression_sync_service_push.dart     push local progression
  progression_sync_service_pull.dart     pull remote progression
  progression_sync_service_session.dart  the shared active session

Extensions are safe here: no test fake subclasses ProgressionSyncService, so
static dispatch cannot swallow an override the way it did for
WorkoutSyncService. Static members need qualifying from inside an extension
(ProgressionSyncService._stateToJson, .pathForExercise).

402 Dart tests; dart analyze clean; coverage gate passes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EveB813BQ8abAP68VSqZ5Y
2026-08-15 21:44:43 +02:00
9e4c45dfa3 Split manual_workout.dart (410) into four part files
manual_workout.dart             constants, the draft, buildEntry
  manual_workout_validation.dart  duration and per-sport rules
  manual_workout_sync.dart        sync payload + record id
  manual_workout_budget.dart      rolling budget accounting

This file is a cross-language contract: its JSON keys must match the Python
side's build_sync_payload / manual_sync_record_id. Nothing was retyped, and
both sides' fixtures still pass -- 16 Python wire-format tests and all 402
Dart tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EveB813BQ8abAP68VSqZ5Y
2026-08-15 21:41:59 +02:00
5977475021 Split the private widgets out of three more Dart screens
exercise_tile.dart        319 -> 144  + exercise_tile_rows.dart
  github_mirror_screen.dart 441 -> 247  + github_mirror_screen_widgets.dart
  home_screen.dart          383 -> 260  + home_screen_cards.dart

Same `part` pattern as history_screen: the private StatelessWidgets move, the
State stays. home_screen lands 10 over -- its remaining methods all call
setState, which an extension cannot, and its build() is one cohesive tree.

402 Dart tests; dart analyze clean; coverage gate passes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EveB813BQ8abAP68VSqZ5Y
2026-08-15 21:39:57 +02:00
9184a6bc33 Split backup_service.dart; partly split workout_sync_service.dart
backup_service.dart (261 -> 175) + backup_service_session.dart
  workout_sync_service.dart (415 -> 322) + workout_sync_service_read.dart

An extension method is dispatched STATICALLY, so a method a test fake
overrides cannot move to one: `_FakeSync extends WorkoutSyncService` overrides
pushManual and readMergedManualPayloads, and with those in an extension the
real implementations ran instead of the fakes -- four manual-workout screen
tests hung on pumpAndSettle rather than failing an assertion. Both are back in
the class; only the private _readMergedPayloads/_fetchPayloads/_mergeRecords
helpers moved.

That leaves workout_sync_service.dart at 322: its public surface is what
callers and fakes bind to, so going lower means changing that surface, not
moving text. Static members also need qualifying inside an extension
(BackupService._baseDir).

402 Dart tests; dart analyze clean; flutter coverage gate passes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EveB813BQ8abAP68VSqZ5Y
2026-08-15 21:38:42 +02:00
3d992ef71b Split manual_workout_test.dart (350) into three by group
402 Dart tests before and after. The cross-language wire-format group moves
with its siblings and still pins the same literal keys the Python side emits.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EveB813BQ8abAP68VSqZ5Y
2026-08-15 21:32:58 +02:00
3a587d6cf9 Split github_mirror_screen_test.dart (470) into four
402 Dart tests before and after.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EveB813BQ8abAP68VSqZ5Y
2026-08-15 21:29:34 +02:00
8293dd88d0 Split settings_screen_test.dart (480) into four by testWidgets
This file is a flat list of testWidgets() calls with no group(), so the split
points are the test boundaries themselves. 402 Dart tests before and after.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EveB813BQ8abAP68VSqZ5Y
2026-08-15 21:28:40 +02:00
f74687064e Split storage_service_test.dart (759) into six by group
Every top-level group() lands in exactly one file, each reusing the original's
imports and setUp/setUpAll preamble. 402 Dart tests before and after.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EveB813BQ8abAP68VSqZ5Y
2026-08-15 21:28:02 +02:00
40a2f28734 Split DESIGN_AUDIT_TODO.md into one file per audited surface
341 lines -> a 19-line index plus three topic files under docs/design-audit/:
the Python/Tkinter lock UI, the Flutter workout app, and the shared gatelock
library. Findings are unchanged -- same 6 entries, same three section headers,
verified by counting both before and after.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EveB813BQ8abAP68VSqZ5Y
2026-08-15 21:27:19 +02:00
338ef5ed9b Split settings_screen.dart's widgets into part files (771 -> 503)
settings_screen_rows.dart        section header, reps row, weight row
  settings_screen_thresholds.dart  stepper button, threshold card and row

Still over the cap: the remaining 503 lines are the State class, whose ~190
line build() would have to be broken up to go further. Two routes were tried
and reverted rather than shipped half-working:

- an extension holding the async actions -- rejected by the analyzer, because
  setState is @protected and cannot be called from an extension. Worth
  recording: it rules out moving any State method that calls setState.
- an extension exposing the SYNC/OFFLINE BACKUP rows as a widget-list getter.
  That one is viable (those rows never call setState) but slicing a balanced
  chunk out of a nested list literal is not a mechanical move, and this screen
  ships to the phone.

402 Dart tests pass; coverage 100%; dart analyze clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EveB813BQ8abAP68VSqZ5Y
2026-08-15 21:15:55 +02:00
d418994f6f Split storage_service.dart (781) into part extensions
storage_service.dart            the class: open, settings, active session
  storage_service_schema.dart     create / migrate / seed
  storage_service_exercises.dart  read+write a single exercise's row
  storage_service_sessions.dart   apply progression, record sessions
  storage_service_backup.dart     backup, restore, sync markers

Dart cannot continue a class body across files, so unlike history_screen these
are `extension`s in `part` files. Library privacy still applies inside a part,
so they reach `_db` and `_getSetting` unchanged and every caller still writes
`StorageService.instance.foo()` -- no public API change, nothing for
public_member_api_docs to demand.

Doing this exposed a bug in scripts/check_silent_failures_dart.py: its bare
`on Foo {` pattern (the catch clause form) also matched
`extension StorageServiceBackup on StorageService {`, so it reported the
declarations as swallowed exceptions. It now requires the try block's closing
`}` first. Verified both directions on a fixture: a real silent
`} on FormatException {` is still reported, the extension is not.

402 Dart tests still pass; flutter coverage still 100%; dart analyze clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EveB813BQ8abAP68VSqZ5Y
2026-08-15 21:12:08 +02:00
38e8364306 Split history_screen.dart (897) into four part files
history_screen.dart          the State: load, pick, build
  history_screen_charts.dart   picker, stats card, streak row, chart widget
  history_screen_painter.dart  the CustomPainter
  history_screen_tiles.dart    the three session tiles
  history_screen_data.dart     pure transforms over the loaded rows

`part` files rather than separate libraries, for two reasons. Dart privacy is
library-scoped, so these `_`-prefixed widgets cannot be imported across a
library boundary -- making them public would be a real API change and
public_member_api_docs would then demand docs for every one. And a `part` does
not get its own SF: entry in lcov, so the coverage gate's "every lib file must
appear" guard has nothing new to miss.

The data helpers become top-level functions taking `rows` explicitly, since
they only ever read it -- which is also what let them leave the State class.

402 Dart tests still pass; flutter coverage still 100% (2717/2717);
dart analyze clean under very_good_analysis.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EveB813BQ8abAP68VSqZ5Y
2026-08-15 21:06:09 +02:00
b8cf634fa6 Record why the popup gate fails on a drifted gatelock
The RecoveryLoop call in HostileLock matches gatelock 0.4.0, which is what
requirements.txt pins and what the pylint hook installs. gatelock 0.5.0
regroups the five arguments into a RecoveryCollaborators object, so running
this gate against a locally-installed 0.5.0 fails with

    RecoveryLoop.__init__() takes 3 positional arguments but 6 were given

That is environment drift, not a defect in the check -- against the pinned
version the gate runs and passes both halves. Comment says so, and says how to
resync, so the next reader does not "fix" the code to match whatever happens to
be installed. (I nearly did; against the pin that edit would have broken CI.)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EveB813BQ8abAP68VSqZ5Y
2026-08-15 21:00:49 +02:00
76a4c6a0a7 Finish the Python side: scripts, conftest, and the last test modules
verify_lock_popup_safety.py (318) -> _xvfb_reexec.py      shared re-exec
                                       _popup_form_check.py HostileLock, ticks,
                                                            the real-form check
  verify_screen_fits.py (266)       -> uses _xvfb_reexec too
  conftest.py (329)                 -> _tk_mocks.py         the tk mock + the
                                                            module patch list
                                       _opt_in_fixtures.py  requestable fixtures
  three test modules                -> _part4/_part5/_part15 siblings

conftest keeps every autouse fixture. pytest applies autouse fixtures from
conftest only, so one moved to a sibling would silently stop running and take
its safety guard with it -- verified by asserting the fixtures are still
listed, not just that the suite is green. The five near-identical _isolate_*
fixtures collapse into one table-driven _isolate_state_files, which also picks
up the _shutdown_sick_state binding an earlier split created.

scripts/ becomes a real package so the shared helpers are importable. Both
gates now run as `python3 -m scripts.<name>`, including CI -- the path form put
scripts/ on sys.path instead of the repo root, and the alternative was a
try/except ImportError that this repo's no-silent-failures hook rightly
rejects. Each gate was run in its exact invocation form.

Note: verify_lock_popup_safety still fails on
`RecoveryLoop.__init__() takes 3 positional arguments but 6 were given`. That
is pre-existing -- confirmed by running it at HEAD in a clean worktree before
touching it -- a gatelock API drift, not this refactor. Left as found.

978 tests at 100% branch coverage. Gate: 41 -> 22 files, all remaining ones
Dart plus DESIGN_AUDIT_TODO.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EveB813BQ8abAP68VSqZ5Y
2026-08-15 20:57:29 +02:00
8bc981da4e Split _runnerup_verification, _ui_widgets and _sick_dialog
_runnerup_verification.py (312) -> _runnerup_tcx.py       pull + parse TCX
                                     _runnerup_backfill.py  fill the log for a
                                                            date or a week
  _ui_widgets.py (287)            -> _ui_form_fields.py     labelled fields,
                                                            focus, disable_paste
  _sick_dialog.py (278)           -> _sick_commitment.py    next-day commitment

_ui_form_fields also takes disable_paste, since the fields are what use it;
_ui_widgets re-exports it for _manual_workout_widgets and _sick_dialog.

Two things had to move with the code rather than being left behind:

- conftest's _TK_MODULES gains _ui_form_fields, or its tkinter calls reach a
  real display instead of the mock.
- pyproject's S314 waiver follows the ET.parse call it covers into
  _runnerup_tcx.py. Same approved suppression, new home -- not a new one.

Test count 977 -> 978: _ui_form_fields is a new lock-surface module, so
test_no_popup_widgets safety-checks it too. 100% branch coverage held; pylint
back at 8.70 after fixing a genuinely undefined WidgetGroup annotation the
split exposed. `screen_lock --status` unchanged. Gate: 41 -> 28 files.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EveB813BQ8abAP68VSqZ5Y
2026-08-15 20:37:15 +02:00
094a1fe5ed Split _workout_sync.py and _sick_tracker.py
_workout_sync.py (332) -> _sync_client.py   token, Firebase/GitHub client
                            _sync_records.py  payload predicates + log decoding
  _sick_tracker.py (317) -> _sick_budget.py   SickHistory + rolling budget

This one was not a pure move until the fixtures followed it. Three autouse
test guards patch names by module, and all three pointed at _workout_sync:

  isolate_sync_token      SYNC_TOKEN_FILE -> now also on _sync_client
  _no_real_firebase_config CONFIG_FILE    -> now on _sync_client
  _firebase_config helper  (7 call sites) -> now passed _sync_client

With them stale, sync_client built a REAL client: the suite went from 14s to
over 120s and test_workout_sync_part5 asserted against live workout data
pulled from the actual backend. Fixed, the same tests run in 0.7s. Both
modules keep SYNC_TOKEN_FILE so either patch point works, and every relocated
guard target is asserted to exist.

977 tests pass at 100% branch coverage; pylint back to 8.70 after fixing the
findings the split introduced (a dropped `return None` in _parse_iso and
leftover dead imports). `screen_lock --status` unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EveB813BQ8abAP68VSqZ5Y
2026-08-15 20:30:42 +02:00
f5c6c2fb9d Split _manual_workout_dialog.py and _manual_workout.py
_manual_workout_dialog.py (357) -> _manual_workout_sport_fields.py
                                     ManualWorkoutSportFieldsMixin
                                     (+ _mw_scrollable_form moved into the
                                      existing widgets mixin, where the
                                      generic scaffold belongs)
  _manual_workout.py (355)        -> _manual_workout_types.py: the sport
                                     constants, ManualWorkoutDraft, and the
                                     rolling budget accounting

A painting mixin cannot simply move: conftest's mock_tk fixture patches `tk`
per-module through the _TK_MODULES list, so the new module had to be
registered there or its tkinter calls reach a real display. That is what the
two dialog tests caught.

Test count 976 -> 977: _manual_workout_sport_fields is a new lock-surface
module, so test_no_popup_widgets now safety-checks it too. Coverage stays
100%, pylint holds at 8.70/10, `screen_lock --status` unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EveB813BQ8abAP68VSqZ5Y
2026-08-15 20:16:33 +02:00
bdc405c8c9 Split _shutdown, _phone_verification and _status_data
Six new modules, all composed or re-exported so no caller changes:

  _shutdown.py (384)          -> _shutdown_sick_state.py  SickDayStateMixin
                                 _wake_alarm.py           WakeAlarmMixin
  _phone_verification.py (380)-> _adb_transport.py        AdbTransportMixin
                                 _http_workout_fetch.py   HttpWorkoutFetchMixin
  _status_data.py (370)       -> _status_types.py         the 7 dataclasses
                                 _status_projection.py    shutdown projection
                                                          + format_summary_line

Moving a name means moving where it is *read*, so 30-odd mock patch targets
move with it. Most importantly conftest.py's autouse _block_real_network
fixture: ruff correctly dropped the now-unused `socket` import from
_phone_verification, which silently removed the guard's target and would have
let subnet probes reach the real network. It now blocks socket in both modules
that actually own the probes, and both targets are asserted to exist.

976 tests pass at 100% branch coverage; pylint 8.70 -> 8.71; `screen_lock
--status` renders the real week unchanged. File-length gate: 41 -> 35.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EveB813BQ8abAP68VSqZ5Y
2026-08-15 20:11:28 +02:00
43a8b1d0f6 Split _compliance_state.py and status_view.py
_compliance_state.py was 399 lines and status_view.py 396.

_compliance_state.py becomes three acyclic modules:

  _compliance_predicates.py  the five leaf predicates + the gatelock
                             log_integrity imports
  _compliance_trace.py       PredicateResult / AutoUpgradeOpportunity /
                             LockExplanation, _stage, and
                             describe_auto_upgrade_opportunity
  _compliance_state.py       explain_lock_decision, importing both

Two-module shapes were all cyclic here: _compliance_state must export the
engine while owning the predicates it calls. Three modules break that with no
deferred import, no forwarder and no loss of the typed signature; every name
is re-exported, so _status_data.py and _log_mixin.py are untouched.

Because the hmac helpers now live in _compliance_predicates, 29 patch targets
across 5 test modules move with them -- patching a name where it is no longer
read would silently stop taking effect.

status_view.py's seven _section_* render methods become StatusSectionsMixin in
_status_sections.py, composed into StatusWindow alongside its existing mixins.

Test count goes 975 -> 976: test_no_popup_widgets parametrizes over
PACKAGE_DIR.glob("*.py"), so _status_sections.py is now covered by the
no-popup safety check too. Coverage stays 100%, pylint holds at 8.70/10, and
`screen_lock --status` renders the real week unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EveB813BQ8abAP68VSqZ5Y
2026-08-15 20:01:12 +02:00
0714c2264d Split _ui_flows.py into sick-day and lockout flow mixins
_ui_flows.py was 370 lines. The sick-day flow and the lockout/phone-penalty
flow are both self-contained, so each moves to a sibling module:

  _ui_flows_sick.py     SickDayFlowMixin   (justification, countdown, debt)
  _ui_flows_lockout.py  LockoutFlowMixin   (lockout timer, no-phone penalty)

UIFlowsMixin composes both rather than exposing them as separate mixins, so
neither ScreenLocker nor scripts/verify_screen_fits.py changes its bases --
and ScreenLocker's ancestor count (already at pylint's max-parents limit)
stays where it was.

Pure move: 975 tests still pass at 100% branch coverage, pylint holds at
8.70/10, and `screen_lock --status` renders the real week unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EveB813BQ8abAP68VSqZ5Y
2026-08-15 19:56:16 +02:00
3476dd3afd Split the last 8 test modules; scope pylint's test-inherent checks
Some checks are pending
pre-commit / pre-commit (push) Waiting to run
Tests / test (3.10) (push) Waiting to run
Tests / test (3.11) (push) Waiting to run
Tests / test (3.12) (push) Waiting to run
Tests / lock-popup-safety (push) Waiting to run
release-apk / release (APK) (push) Waiting to run
Splits test_screen_lock_coverage_part2 via a shared _unlock_helpers.setup_unlock
factory rather than duplicating the fixture, and finishes the remaining
runnerup/manual-workout/status-view test modules under the 250-line cap.

Also splits the pylint hook in two. protected-access, unused-argument and
duplicate-code are how pytest and unit-testing private mixins work, not
defects, and a commit made entirely of test files could never clear the 8.0
batch score with them counted. Source keeps the full rule set — verified that
an injected protected-access is still reported there.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EveB813BQ8abAP68VSqZ5Y
2026-08-15 19:40:32 +02:00
dd8296fd3f Split 12 more test modules under the 250-line cap
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EveB813BQ8abAP68VSqZ5Y
2026-08-15 19:34:59 +02:00
5c27769642 Split 10 more test modules under the 250-line cap
Some checks are pending
pre-commit / pre-commit (push) Waiting to run
Tests / test (3.10) (push) Waiting to run
Tests / test (3.11) (push) Waiting to run
Tests / test (3.12) (push) Waiting to run
Tests / lock-popup-safety (push) Waiting to run
release-apk / release (APK) (push) Waiting to run
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-15 19:22:00 +02:00
4bdd180cb1 Add todo_fix_pylint_10_prompt.md: raise pylint to a 10.00 gate
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-15 19:18:39 +02:00
a0d78bc719 Split 13 test modules under the cap; document their test methods
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-15 19:16:18 +02:00
ee8f8bcf3a Add refresh_refactor_todo.py: self-correcting violations table
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-15 19:05:31 +02:00
4b4695e23a Split screen_lock.py under the 250-line cap
Some checks are pending
pre-commit / pre-commit (push) Waiting to run
Tests / test (3.10) (push) Waiting to run
Tests / test (3.11) (push) Waiting to run
Tests / test (3.12) (push) Waiting to run
Tests / lock-popup-safety (push) Waiting to run
release-apk / release (APK) (push) Waiting to run
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-15 19:01:18 +02:00
c63a02f9eb Add sync-status card and duration-threshold fixture
Some checks are pending
pre-commit / pre-commit (push) Waiting to run
Tests / test (3.10) (push) Waiting to run
Tests / test (3.11) (push) Waiting to run
Tests / test (3.12) (push) Waiting to run
Tests / lock-popup-safety (push) Waiting to run
release-apk / release (APK) (push) Waiting to run
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-15 18:41:31 +02:00