diff --git a/docs/history/DEVELOPMENT_LOG.md b/docs/history/DEVELOPMENT_LOG.md index ce136e1..0795163 100644 --- a/docs/history/DEVELOPMENT_LOG.md +++ b/docs/history/DEVELOPMENT_LOG.md @@ -32,6 +32,90 @@ written and stay true. It is exempt from review for the same reason a receipt is ## Entries +### 2026-08-20 (evening) — Two release-blockers, and the trap that was reported + +Kaspa reported one bug — *"on the settings page, if you go to app lock you can't +unnavigate out of it"* — and asked for biometrics-or-PIN, back arrows, unified +code, error handling, notifications, and blind spots covered. Three read-only +explorations of settings navigation, the app lock, and the reminder pipeline +turned up considerably more than the report, including two release-blockers +nobody had noticed. + +**Batch 12 · P0 — "Ended" recorded the start of a period.** The period-end +check-in asks *"Is your period over?"* and offers **Ended** / **Still going**. +`ReminderWorker` attached writes to buttons *by position* — index 0 to "started", +index 1 to "not yet" — for every kind of reminder, so "Ended" inserted a new +period starting today, in the middle of the one it was asking about, and "Still +going" filed a censoring observation against a forecast that had already +arrived. It corrupted the health record and every forecast built on it. The +handler had no test at all, which is how it survived; it owns the only two writes +reachable from a locked phone. A button is now one thing carrying both halves, +`ENDED` closes the period through `setPeriodEnd`, and `STILL_GOING` deliberately +writes nothing. + +**Batch 11 · P0 — Change PIN never checked the current PIN.** The screen fired +the check and advanced to "Choose a PIN" in the same breath, without waiting for +the answer. Any four digits reached the replacement screen and overwrote the PIN, +so anyone holding the unlocked phone could lock the owner out of her own history +permanently — under the no-recovery policy the only way back is to erase +everything. Fixed at both altitudes: the step advances inside the verified +callback, and the ViewModel refuses a replacement no successful check authorised. + +**The reported trap had three causes, all real.** The bottom nav saved and +restored per-tab stacks, so tapping Settings from App lock restored +`[settings, settings/lock]` and landed back on App lock — and the saved stack +survived visiting other tabs, pinning the Settings tab there for the rest of the +process. No screen in the app had a back arrow at all: a grep for `TopAppBar`, +`navigationIcon`, `BackHandler` and `popBackStack` across `app/`, `core/` and +`domain/` returned nothing, and App lock had a headline styled like a bar without +being one. And setting a PIN could still lose the race the log recorded as fixed +in the app-lock entry: the fix lived in the scope the race destroys. + +Settings is now a nested graph, leaving it never saves its stack, re-tapping a +tab returns it to its root, and one `SettingsSubpage` carries the bar for all +three children. App lock's steps are remembered state rather than destinations, +so its back is step-aware and the arrow and the gesture agree. `setPin` unlocks +before it writes, which makes the bad pair unobservable. + +**`ExportHost` has almost certainly never been visible.** It was emitted as a +sibling *before* the `Scaffold`, inside a `Box`, where later siblings draw on top +— and the Scaffold paints an opaque container. It is the Scaffold's top bar now. +The export's on-device checks were never run either, which is why nobody noticed. + +**The lock now fails towards opening.** `hasPin` had no `catch` and is collected +on the startup path in two places that cannot handle a throw, so a corrupt +`app_lock` file crashed the app before any UI existed. It reads as *no lock* now, +and the store has a corruption handler — without one, a single bad write leaves +the lock unable to be set *and* the "Forgot your PIN?" erase unable to run. +Separately, a wrong PIN whose counter could not be signed used to cost nothing; +the unwritten counter is held in memory and read back when it is the longer wait. + +**And the app can now stop asking.** §30's stopping rule was unreachable: the +check-in count was reset by a flow whose first emission always passes, in a +coordinator started in every process — including the one WorkManager spawns for +the reminder, moments before it reads the count. The count now carries the row id +of the period it was asked about, so a new period starts it over by arithmetic +and the reset chain is deleted. + +Compose UI tests run on the JVM under Robolectric now; nothing in this project +could assert a navigation behaviour before. Every fix was proved with +`scripts/prove-guard.sh` — one red each, except removing the change-PIN +write-site guard, which reddens three because that is the whole of its surface. + +**16 issues filed** across two new milestones: `Batch 11 — Settings and the App +Lock` (#60-#67) and `Batch 12 — Reminders that tell the truth` (#68-#75). Five +are closed. + +**Next action:** #71 — a denied notification permission is ignored, the toggle +stays on, and the check-in counter still advances on reminders that were never +posted, so the app can "stop asking" having never asked once. Then #72, where +changing the reminder time does not move it (verified against WorkManager's +bytecode: `UPDATE` keeps the old `lastEnqueueTime`). + +**Blockers:** none. Note that a flaky 5-second test budget was raised to 30s +after one red in a parallel run — each PIN in that test costs a real +210,000-iteration derivation. + ### 2026-08-20 (later) — Batch 09: the learning loop, measured Started from a question — *how well does this app learn a period?* — and