docs: mark M7 done

This commit is contained in:
2026-09-22 10:24:12 +02:00
parent cb35cc9c95
commit 8dec91fd4c
+85 -10
View File
@@ -10,15 +10,18 @@ Status legend: ✅ done · 🚧 in progress · ⬜ not started
## Current state (one line)
✅ **M6 done.** The Timers tab is real: several timers at once, a keypad and
saved presets that set one in a couple of taps, pause/reset/"+1 min" from the
row, the live pill *and* a notification whose countdown the system draws, a
foreground service alive exactly while something is active, and expiry that
rings through M3's audio path — waiting its turn when an alarm is already
ringing rather than talking over it. Every countdown is anchored on the device's
uptime, so moving the system clock cannot move a timer in either direction. Two
tabs are still empty states: **M7** (stopwatch — start/stop/reset, laps with
splits, and the app-wide big-readout typography settled at last) is next.
✅ **M7 done.** The Stopwatch tab is real: start, pause and reset, a readout to
hundredths whose fraction is drawn smaller than its seconds, laps with their
split and their running total newest-first with the fastest and slowest marked,
the lap in progress as its own counting row, and a silent `specialUse`
foreground service whose Lap and Pause work with the app closed — its ticking
drawn by the system, so it costs no battery to keep accurate. A stopwatch that
meets a reboot comes back **paused at what it banked**, with every lap intact,
rather than inventing the time it lost; a clock change cannot move it at all.
M0's promise that the big-readout typography would be settled against a working
stopwatch is kept, and every readout in the app now sits in fixed columns. One
tab is still an empty state: **M8** (world clock — the zone list, ICU names,
offsets and the analog face) is next.
---
@@ -329,13 +332,85 @@ Elapsed-realtime anchored.
§5.10 #11 opens a second DataStore over a live file in the same scope, which
DataStore refuses by design — as `SettingsPrefsTest` itself documents.
### ⬜ M7 — Stopwatch
### ✅ M7 — Stopwatch
Start/stop/reset, laps with splits and cumulative times, best/worst lap emphasis.
Foreground service so it survives backgrounding; laps persist across process
death. This is where the big-readout typography gets settled for the whole app.
The post-reboot repair is **not** here — M3 took it (`ARCHITECTURE.md` §5);
M7 owns only how the paused result is presented.
✅ 968 JVM tests, 125 of them new, and **still no Robolectric** — kept now for
a third screen and a third service. Everything the tab decides is a pure
object (`StopwatchFormat`, `StopwatchReadings`, `LapStats`, `StopwatchLaps`,
`StopwatchNotificationPolicy`, `StopwatchRows`) or a method on the engine or
the ViewModel, tested over the **real** `StopwatchRepositoryImpl` across the
real `appendLap` transaction and a real DataStore under a `@TempDir`.
`StopwatchService` holds no policy and **no state at all** — not even the
timers' one `alreadyAlerted` flag, because nothing here ever alerts — and it
has no `delay`. Two new `ArchitectureRulesTest` rules and two extended; the
four new instrumentation tests bring the compiled total to 36.
The decisions worth knowing: the foreground service is **`specialUse`**, not
`systemExempted`, because a stopwatch is not alarm functionality — it
schedules nothing, wakes nothing and rings nothing, and claiming the type
would have the app tell the platform something untrue; it holds **no wake
lock, ever**, since the elapsed value is derived from a persisted monotonic
anchor at read time; the notification is the **platform chronometer counting
up**, with its base `now - elapsed` **derived at post time and never stored**,
which is how the run record keeps its "no wall-clock field whatsoever" rule;
its channel is `IMPORTANCE_LOW` and silent, because nothing here ever
finishes, so there is no alert-once rule and no session window anywhere in the
path; the readout is hundredths and it is **two strings**, so the
fastest-changing glyphs are not the loudest ones; best and worst need **three**
laps, ties go to the earliest, and all-equal means neither; laps are capped at
**999**, guarded in the engine over one `COUNT(*)` so appending never
materialises the rows; and the tab ticks at ~60 Hz **only while running**, so
a paused or idle stopwatch costs nothing.
Three things the checklist did not name were changed anyway, and are recorded
in `ARCHITECTURE.md`: **the live pill's stopwatch actions now go through
`StopwatchEngine`** (M6 recorded this as knowingly open), with the pill's own
contract unchanged and `LivePillSelectorTest` passing **unmodified** as the
proof; **`LivePillSelector` was re-pointed at a new pure `StopwatchReadings`**,
the same extraction M6 made for timers, so the pill, the shade and the tab
cannot answer "what does this read right now" three different ways; and
**`ui/theme/Type.kt` stopped being `Typography()`** — tabular figures on the
display, headline and title roles, app-wide, with no call-site change
anywhere, plus `ClockulaReadoutDefaults` naming the three roles a readout uses.
The correctness find: a run that met a reboot mid-lap had `accumulated = 0`
while its laps still recorded forty seconds, so the honest reading is
**floored at the last lap's total** as well as at zero — a readout sitting
below a lap the same run printed is not one anyone can believe, and the laps
are real measurements that are never deleted to tidy the arithmetic. Recorded
in `ARCHITECTURE.md` §5. The review found the other half of it: the floor has
to be **banked, not only displayed** — resuming writes
`accumulated = max(accumulated, lastLapCumulative)`, and pausing and lapping
bank the same floored reading — or the record ticks from below its own last
lap, the shade's free-running chronometer counts while the tab sits frozen,
and the next lap records a cumulative below the previous one. The same review
pass moved the engine's verbs onto **the reading's** mode rather than the
stored row's, so a stale record's Resume button resumes instead of hitting a
silent no-op. `pauseAfterReboot` itself is still untouched.
No schema change (the database stays at v2; `LapDao` gains one `COUNT(*)` and
nothing else), **one** new permission — `FOREGROUND_SERVICE_SPECIAL_USE`, the
one that names what the service actually does — and **no floret-kit change**:
the kit stays at 0.6.0, untouched.
Knowingly open: no lap export, sharing, deletion or editing, and no "previous
run" history; no Lap button on the live pill (it has two controls by design);
no user-configurable lap cap or readout precision; no reordering, grouping or
filtering of laps; no countdown-to-start, split-lap alarm, sound or vibration
from the stopwatch (a build rule enforces the silence); no analog stopwatch
face and no `MaterialShapes` showpiece, which `PLAN.md` §8 reserves for the
world-clock face, the ring screen and timer progress. `StopwatchReading.anchorIsStale`
is still not surfaced, the same answer M6 gave for timers. Of `AlarmClock`'s
intent contract only the in-process hook `ShellNavigation.tabForAction` grew a
second action; M9 owns the rest. The four new instrumentation tests compile in
the gate but have **never run on hardware**, as no device was attached — the
same caveat M5's seven and M6's four carry.
### ⬜ M8 — World clock
IANA zone list with search, ICU-localised city and zone display names, offset and
day-difference relative to home, reorder, home-zone handling. The analog face