# Claude QA Coverage — Period

```
Status: Current
Owner: _null
Last reviewed: 2026-08-18
Governs: what each QA pass actually reached
Review trigger: Any QA round run
```

> Pass by pass, what was reached and what was not. The point of this file is the
> **Blocked** and **Not run** rows: a pass left out of a report reads exactly
> like a pass that succeeded, and that is how untested code ships believing it
> was tested.

## Round 2 — 2026-08-18 at `19edf4c`, partial

Batch 03 built every screen the app has, and each was driven by hand on the
emulator as it landed rather than in one pass at the end. Same environment:
`PeriodQA`, API 36, Pixel 6 profile, debug build.

| Pass | Result | Notes |
| --- | --- | --- |
| A — First run | **Pass** | Clean install reaches onboarding; all seven screens walked; the flow produces a forecast and a relaunch goes straight to Today. |
| B — Core loop | **Pass** | Two-tap logging confirmed on the device: "Started period" → "Yes — today" → "Logged ✓". Editing, ending, and spotting reclassification all exercised. |
| C — Failure paths | **Partial** | Duplicate-date logging, an end date before a start, and a future date in the picker all handled. Airplane mode, denied permissions and a process killed mid-write still untried. |
| D — Persistence | **Partial** | Onboarding completion, records and settings all survive force-stop and relaunch. Reboot and update-over-install untried. |
| E — Forecast under hard histories | **Partial** | The §51 cases pass as unit tests against both engines, and the not-yet path was driven through the UI. The variable and outlier histories have still not been entered by hand. |
| F — Notification privacy | **Not run** | Nothing sends a notification yet — Batch 05. The onboarding *choice* was verified: Discreet is selected before the user touches anything. |
| G — Accessibility | **Partial** | Every calendar day, the confidence indicator and the hero countdown carry content descriptions, checked by reading the view hierarchy. **The calendar was verified in actual greyscale** and all five marks remain distinguishable. TalkBack itself, and font scaling, still untried. |
| H — Data ownership | **Partial** | Delete-all covered by an instrumented test. Export, app lock and artifact inspection do not exist yet. |

### What driving it found that tests did not

Three defects, none of which any unit test would have caught:

1. **Dark mode was broken for the whole of Batch 01.** `PeriodTheme` never
   wrapped its content in a `Surface`, so text without an explicit colour
   inherited black and the app background never painted. Light mode looked
   correct by accident.
2. **"Period ended" appeared to do nothing.** The logic was right — a period
   ending today still includes today — but the screen was identical afterwards,
   so the button read as broken.
3. **Today's underline collided with the spotting dot**, on the one day that was
   both. Invisible in the colour screenshot, obvious in greyscale.

The pattern is now four rounds old and worth stating as a rule: **the defects in
this project are found by opening it, not by reading it.**

## Round 1 — 2026-08-18 at `adc5075`, partial

Not a full round. Batch 01 produced the first build, and this covered the two
passes that build could support. Recorded as partial rather than left out,
because a pass omitted from a report reads exactly like a pass that succeeded.

**Environment:** emulator `PeriodQA`, API 36 (`sdk_gphone64_x86_64`), Pixel 6
profile, debug build.

| Pass | Result | Notes |
| --- | --- | --- |
| A — First run | **Pass** | Clean install, cold start, empty state reads correctly ("No forecast yet", "No periods logged yet"), all four tabs reachable. No notification permission prompt yet — none is requested until Batch 05. |
| B — Core loop | **Pass** | Logged a period; cycle day, countdown, window and confidence appeared. Edited the start date twice; forecast moved from 15 Sep to 13 Sep and the record was marked `edited`. Deletion covered by instrumented test rather than by hand. |
| C — Failure paths | **Partial** | Only one path exercised, and it found a crash — see below. Airplane mode, denied permissions and a killed process mid-entry were not tried. |
| D — Persistence and migration | **Partial** | Covered by an instrumented test that closes and reopens a file-backed database, which is what a force-stop does. A real force-stop, a reboot and an update-over-install were not tried. |
| E — Forecast under hard histories | **Not run** | The §51 cases pass as unit tests; none has been driven through the UI, which is what this pass is for. |
| F — Notification privacy on a lock screen | **Not run** | Nothing sends a notification yet — Batch 05. |
| G — Accessibility | **Not run** | TalkBack, font scaling and greyscale legibility untried. The working surface is not the designed screen, so this is worth deferring to Batch 03 rather than testing a screen that is about to be replaced. |
| H — Data ownership and leakage | **Partial** | Delete-all covered by an instrumented test. Export, app lock and the built-artifact inspection do not exist yet. |

### What pass C found

Tapping **Started today** twice on the same day killed the app:

```text
FATAL EXCEPTION: main
android.database.sqlite.SQLiteConstraintException: UNIQUE constraint failed:
period_records.startDate
```

Not filed as an issue: it was found and fixed inside the same batch, before any
build left this repository, and a tracker issue closed by the commit that
introduced the code would be bookkeeping rather than a record. It is written
down here, in the commit that fixed it, and in
[`../architecture/README.md`](../architecture/README.md), with four regression
tests pinning the behaviour.

The interesting part is not the crash. It is that **the first thing tried by
hand broke immediately**, on the app's primary button, with 70 unit tests
green — which is the argument for pass C existing at all.

### What lint found, that no pass would have

Wiring the boundary guard into `./gradlew check` ran Android lint for the first
time, on `f5e9fbe`:

```text
NewApi: java.time.LocalDate#ofInstant requires API 34 (minSdk is 26)
NewApi: java.time.LocalDate#EPOCH     requires API 34 (minSdk is 26)
```

Both on the recalculation path — a crash on every device below Android 14.
Invisible to the unit tests and invisible to an API 36 emulator. Fixed, and
worth recording as a standing gap below: **this project has no test on a device
at its own `minSdk`.**

## Standing gaps

Things no round has ever covered, carried forward until they are. This list
existing is not a failure; it not existing while the gaps do is.

- **TalkBack has never actually been run.** Content descriptions are written and
  were checked by reading the view hierarchy, which proves they exist and not
  that they make sense in sequence. Pass G is not complete until somebody
  navigates a screen with their eyes shut.
- **Font scaling untried.** The hero number is 72sp; at the largest accessibility
  scale it may not fit beside anything.
- **No device at `minSdk`.** Everything so far ran on API 36. The two NewApi
  bugs above would have crashed on Android 8 through 13 and nothing except lint
  could see them. Lint is a good guard and it is not a substitute for one run on
  an old API level.
- **Physical-device coverage is undecided.** Passes F and G need a real device
  with a lock screen and TalkBack; which device that is has not been chosen, and
  an emulator is not a substitute for either.
- **Long-horizon accuracy** — whether predictions measurably improve at 3, 6 and
  12 confirmed cycles — cannot be reached by a QA round at all. It needs either
  a simulated history harness or real elapsed time, and until one exists the
  product's headline claim is tested only at the unit level.
