docs: log the encryption decision, the crash it uncovered, and two false greens

Step 6 of WORK_CYCLE. Next action is #34, the app lock — the recovery
decision is settled and the design is on the issue.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
null 2026-08-19 03:09:26 -05:00
parent 17cddb2a61
commit 42a2d543ad
1 changed files with 77 additions and 0 deletions

View File

@ -32,6 +32,83 @@ written and stay true. It is exempt from review for the same reason a receipt is
## Entries
### 2026-08-19 — Encryption, declined for a reason; and the crash it uncovered
The question was whether the cycle database could be encrypted at rest. It can.
It is not, and the reasoning is now in `SECURITY.md` under *Deliberately out of
scope* rather than in anybody's head.
**The architecture answers it.** `ReminderCoordinator` collects Room flows at
every process start — including processes WorkManager starts after a reboot with
no Activity — and `ReminderWorker` reads the forecast on a schedule. So a
database key would have to be readable with no user present, which is the same
availability condition Android's own file-based encryption key already has. A
key anything running as this app can reach unattended defends a **file**, not a
**process**. Against every adversary `SECURITY.md` lists — someone holding an
unlocked phone, someone reading a lock screen, a rooted device — it adds
nothing. What it would add is narrow and real: a file copied off the device and
read elsewhere, a phone with no screen lock, and crypto-shred on delete.
Costed rather than argued: about +1 MB downloaded and +2 MB installed on a 2 MB
app, and a new class of total loss, since a Keystore key can be lost in the
field, platform backup is off, and no telemetry would tell us. The reversing
condition is written down — a key bound to the app lock's secret, once export
gives the user a copy they control.
**Looking for the encryption seam found a live defect instead.** The
application scope was built with `SupervisorJob` and no
`CoroutineExceptionHandler`, and two Room flows are collected on it.
`SupervisorJob` stops a failing child cancelling its siblings; it does not stop
the exception, which reaches the thread's default handler and ends the process —
headless, at every boot, with nothing on screen to explain it. Both ViewModels
already had a handler. The one scope that runs with nobody watching did not. The
trigger is not hypothetical: `repository.forecast` runs the prediction engine
inside the flow, and `Prediction`'s init block enforces its window invariants
with `require`. Filed as #45, fixed in `424a513`, closed with its evidence.
**`prove-guard.sh` told me it had caught something it had not.** The first proof
ran `./gradlew :app:test --tests …`. `:app:test` is AGP's lifecycle task and
takes no `--tests` option, so Gradle failed in 544 ms with `Unknown
command-line option`, the mutation was never compiled, no test ran — and the
script reported *"the guard caught it"*. It decides from the exit code and
cannot tell a broken test from a broken command.
Pulling that thread: **the three documented boundary-guard proofs in
`architecture/README.md` have never exited 0.** Gradle prints no test-style
summary for those tasks, so `prove-guard` counts log lines, and its default
pattern also matches `FAILURE:` and `BUILD FAILED` — one caught violation reads
as three, exit 3, "not a pass". They now carry a `PROVE_GUARD_FAIL_PATTERN` and
were re-run at 0, 0, 0. Both lessons are `GUARDS.md` §8, which is exactly that
document's review trigger.
**And a false claim that was one drawing away from shipping.**
`BRAND_GUIDE.md` carried an artwork brief reading *"explaining encrypted local
data storage"* — on the single subject where a padlock reads as a promise.
Replaced with what is true and is the better story anyway: the data never
leaves, so there is no server to breach and nothing to sell. `SECURITY_CHECKLIST`
gained the row that would have caught it. Nothing shipped ever claimed it; grep
over `app`, `core` and `domain` comes back empty.
Also registered the Forgejo webhook (#9), verified with a signed replay that
returned `applied:true` — so a `P0` now raises an alert immediately instead of
waiting for the next sweep.
**What this proved:** 207 tests, 0 failures, 0 skipped; `checkModuleBoundaries`,
`checkNoHealthLogging` and `checkPermissions` all exit 0, the last against a
freshly built release manifest; removing the new handler fails exactly one test,
and removing either `catch` fails exactly its own.
**Next action:** #34, the biometric and PIN app lock. The recovery decision is
settled — none — and the full design is on the issue. Start with `:core:security`
and remember both rows it needs in the root `build.gradle.kts`, since
`checkNoHealthLogging` reads a hardcoded module list and a missing row fails
silently in the one module that holds key material.
**Blockers:** none. #34 needs `androidx.biometric`, which fails `checkPermissions`
until `USE_BIOMETRIC` and `USE_FINGERPRINT` are added to the allowlist with their
reasons, and needs `MainActivity` to become a `FragmentActivity` — both known,
neither blocking.
### 2026-08-19 — Eleven screenshots, and the day the seeding chose a bad hero
Privacy: Period Tracker was the only Privacy LLC product without a page on