From 439d1312f3890454a13bdd1bc33d01aa4f0479aa Mon Sep 17 00:00:00 2001 From: ApolloF <98666317+ApolloF@users.noreply.github.com> Date: Wed, 21 Oct 2026 17:07:34 +0200 Subject: [PATCH] Remove internal dev notes and references to them --- AGENTS.md | 80 ------------------- CLAUDE.md | 1 - README.md | 2 +- .../data/OldBackupImportTest.kt | 2 +- docs/BACKLOG.md | 29 ++++--- docs/PLAY.md | 2 +- 6 files changed, 17 insertions(+), 99 deletions(-) delete mode 100644 AGENTS.md delete mode 100644 CLAUDE.md diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index 01397b4..0000000 --- a/AGENTS.md +++ /dev/null @@ -1,80 +0,0 @@ -# SteroidTracker - -Offline Android app for dose plans, one-tap logging, reminders and estimated level curves. -Named ProtocolTracker until 0.5.1. The new name is only what people see (launcher label, in-app text, report titles, export file names, docs, release APK name). Everything persisted or identifying keeps the old one, because changing it breaks updates or old files: the package and `applicationId` `com.apollof.protocoltracker`, `tracker.db`, the `settings` DataStore, the backup format `protocoltracker-backup-2`, the bloodwork header `protocoltracker-bloodwork-1`, and the intent actions. `OldBackupImportTest` restores a backup saved by v0.5.1. -Rewrite of the CycleTracker web app (ApolloF/cycletracker), which serves only as a feature reference. - -## Stack -- Kotlin 2.4, Jetpack Compose + Material 3, AGP 9 (built-in Kotlin), Gradle 9, JDK 21 toolchain, JVM target 17. -- minSdk 26, targetSdk 36, compileSdk 37. -- Room (KSP) for data, DataStore for settings, AlarmManager + WorkManager for reminders, Glance for the widget. -- Manual dependency injection (`AppContainer`); no Hilt. -- Local-only data. No accounts, and nothing the user logs leaves the phone. Android cloud backup is disabled; users export JSON backups. foss has no network permission (`FossManifestTest` fails if `INTERNET` or `ACCESS_NETWORK_STATE` enters its merged manifest; a dependency could add it); play gets `INTERNET` from Play Billing only, for the Pro purchase. - -## Commands -Set `JAVA_HOME` to a JDK 21 and `ANDROID_HOME` to the SDK first (on this machine both live under `%LOCALAPPDATA%`). -``` -./gradlew :core:domain:test # fast pure-JVM tests (schedule, PK, import) -./gradlew :core:domain:test testFossDebugUnitTest testPlayDebugUnitTest # all unit tests (domain JVM + Robolectric, both flavours) -./gradlew assembleFossDebug assemblePlayDebug # debug APKs -> app/build/outputs/apk//debug -./gradlew lintFossDebug lintPlayDebug assembleFossRelease bundlePlayRelease # R8-minified foss APK and Play bundle -./gradlew connectedFossDebugAndroidTest # on-device smoke test (emulator running) -``` -Play build switches (Gradle properties, `-P...`): `play.monetization=none|paid_listing|unlock` (default `unlock`, the one-time Pro unlock; foss is always `none`), `play.idSuffix=.play` (default none: both flavours ship as `com.apollof.protocoltracker`), `play.label=...` (Play launcher label, default "ProtocolTracker"). -Kotlin compiles in-process (`gradle.properties`) because the Kotlin daemon locked build dirs on Windows. -If Gradle reports `Unable to delete directory` or `AccessDeniedException` under `build/`, delete that directory (e.g. `rm -rf core/data/build/intermediates/*lint*`) and rerun; it is a local file-lock quirk, not a code error. A test task that fails at once with `java.io.EOFException` read a results store left half-written by a killed test JVM: delete that module's `test-results` folder and rerun. If it keeps happening, run any task with an init script that moves every project's `layout.buildDirectory` to a folder outside the project (e.g. `C:/Users//.ptbuild/`); unit tests work that way too. -When the project is opened through a Google Drive virtual drive, dexing (`assembleDebug`) fails with "this and base files have different roots". Build APKs with an init script that sets `layout.buildDirectory` of every project to a folder on a local disk, and add `-Pkotlin.incremental=false` for release tasks. Run unit tests without it (they need the build folder on the project's drive). - -## Check command -`./gradlew :core:domain:test` -This is the fast subset: the pure-JVM domain tests (all business logic lives there), about 1.5 minutes cold. Needs `JAVA_HOME` (JDK 21) and `ANDROID_HOME` set. The full local unit run, `./gradlew :core:domain:test testFossDebugUnitTest testPlayDebugUnitTest` (adds the Robolectric UI tests in both flavours), took 42 minutes per flavour on this machine, so run it only when you changed `app/` UI code (use `--tests` for the classes you touched), and otherwise leave it to CI. CI (`.github/workflows/android.yml`, job `build`) runs it plus `lintFossDebug lintPlayDebug assembleFossDebug assemblePlayDebug`, and job `smoke` runs the emulator `SmokeTest` in both flavours. Run `lintFossDebug lintPlayDebug assembleFossDebug assemblePlayDebug` yourself before a PR that touches resources, the manifest or build files. Keep it passing. - -## Layout -- `core/domain` — pure Kotlin, no Android. Models (incl. symptom catalog, blood markers and the bloodwork rules shared by the sheet and importers, marker history, draw age and weekly BP averages for the trends, injection sites and their rotation rule), schedule engine (`schedule/`), PK engine, presets, lab units and the per-group level adjustment (`pk/`), unit conversion and display formats (`units/`), logs near a moment (`timeline/`), backup, legacy import and web history import (`io/`), the bloodwork import's text, value and date grammar, marker vocabulary, block reader with its refusal messages, the row pipeline (`RowReader`: units, meaning checks, left-out reasons), the draft and its review (`ImportDraft`, `ImportReview`: choices, duplicates, entries) and the AI prompt (`io/labimport/`). All business logic lives here and is unit tested. -- `core/data` — Room entities/DAOs/mappers, `TrackerRepository` (single write path), `SettingsStore`. -- `app` — Compose UI per screen (`ui/today`, `ui/plan`, `ui/levels`, `ui/journal`, `ui/settings`), symptom, bloodwork and marker sheets and the bloodwork import screen (`ui/health`; a save hands Journal the Bloodwork chip and Undo through `JournalFocus`), shared components (`ui/components`, incl. `TrendChart`, the small time chart for bloodwork, BP and mood), reminders (`reminders/`), widget (`widget/`), `DoseActions` (logging shared by UI, notifications, widget). -- Two product flavours in the `distribution` dimension, with the same data model, schema, backup format, PK engine and features. `foss` (GitHub, F-Droid, Obtainium): every preset, the full wording, `USE_EXACT_ALARM`, no network permission and no proprietary dependency, everything unlocked. `play` (Google Play): the preset allowlist (`PLAY_PRESET_IDS` in `domain/pk/PresetChannel.kt`, presets by INN or compound name), neutral category labels (`app/src/play/res`), no `USE_EXACT_ALARM`, and the Pro unlock (Play Billing, `playImplementation` only; tiers in `domain/entitlement/` `PlayPolicy`; `app/src/play/.../billing/`: `PlayBillingEntitlements` over a `BillingGateway`, the Pro state cached in the "billing" preferences, re-read on every app start and resume, kept when Play cannot be reached, revoked when a successful query no longer lists the purchase; the paywall, trial banner and Settings group in `ProUi.kt`, their wording generated from the policy in `ProCopy.kt`). Each flavour has a small `Distribution` object (`app/src//`) that `AppContainer` reads; feature code never branches on `BuildConfig.FLAVOR`, and asks `FeatureGate` (never a Pro flag) what a feature gives. Never claim "offline" or "no internet" in play text. `docs/MODELS.md` marks each preset's Play column. Until 0.5.0 a `dev` flavor, "ProtocolTracker Dev" with the `.dev` id, carried the features in development; its users moved by backup → restore. `versionName` equals the release tag without the `v` (`VersionNameTest`, CI). -- `docs/MODELS.md` — level model and preset sources. - -## Conventions -- UI copy: plain labels and short instructions. No slogans, motivational or promotional text. Empty states say what is missing and the action. -- Screens hold no business logic: compute in `core/domain`, write via `TrackerRepository`. -- Logged doses carry a snapshot (name, category, PK params, formulation) and the planned amount; never recompute history from the current plan. The one exception: `PresetMigration` replaces snapshot PK params that still equal an older preset's shipped kinetics with the current preset's (a model correction, not a user change). Adjusting a logged amount never changes the plan. A dose field that still shows the plan or the stored amount saves that amount itself (`DoseAdjust.fromField`), never its 4-decimal copy. -- Occurrence keys: `itemId@yyyy-MM-dd/T0830` for exact times (date and planned clock time, stable across time-zone changes; a time edit moves the logs, `Rekey.kt`), `itemId@epochSecond` for every-X-hours doses, `itemId@yyyy-MM-dd/SLOT` for parts of the day (stable when slot clock times change). The DB enforces one log per key. -- Logical day: `SlotTimes.dayStart` (Settings › Times of day; default 4:00, `SettingsStore.DEFAULT_DAY_START`) decides which day "today" is (`slotTimes.dateOf(now, zone)`), which day a log counts for (Journal headers, extras, logged late, interval restarts) and where agenda log windows start (`agendaLogsFrom`). Occurrences keep their calendar `localDate` and key. Never use `now.atZone(zone).toLocalDate()` for "today". -- Skips: a skipped log sits at its planned time (`DoseLog.shownAt`; `normalizedForWrite()` sets `takenAt = scheduledAt` and drops the site on every write). Skips are never "logged late", count apart from taken and missed (`DayStatus`, `daySummary`, `adherenceText`) and print no amount in reports. -- Interval schedules (`EveryNDays` with n > 1, `EveryHours`) with `fromLastDose` restart from the last taken dose. `occurrences()` and the agenda functions take `IntervalAnchors` built from *all* taken plan doses (`TrackerRepository.anchors`/`anchorsNow()`, or `IntervalAnchors.from(allLogs)`), never from a windowed log list. Skipped and unscheduled doses never move the plan. -- Naming: presets use `commonName` + scientific `name`, shown as "Anavar (oxandrolone)"; peptides use the compound name only. Sections: injectable steroids → oral steroids → hormones → SARMs and research compounds → support (by `SupportKind`) → peptides. -- Injectables are planned with `DoseBasis.PER_WEEK`; per-dose amounts come from `PlanItem.dosePerOccurrence()`. -- Adjust level (`LevelAdjustments`): a per-level-group percentage (−90 to +100 %) that scales the estimated curve and its figures for past and planned doses alike. It lives in the settings (`level_adjustments`, so it rides in backups), never in a dose snapshot or the plan; lab results are never scaled. -- Level curves are labelled as estimates. Compounds without reliable data have `pk = null` (logged, not plotted), except compounds with a PK-sheet row, which keep the sheet's curve and say in their source note when its sources are weak. Change preset parameters only with a source note in `docs/MODELS.md` and bump `Presets.VERSION`. -- Exports: JSON backup (`protocoltracker-backup-2`, restorable; since dev.15 it carries the settings as `settings`, `SettingsStore.exportMap`/`importMap`, absent in older backups), HTML and Markdown reports (`domain/io/Report.kt`). -- Privacy policy: it ships in the app (Settings › Privacy policy, `res/raw/privacy_policy.md`, read by `PolicyText`). foss shows `PRIVACY.md` word for word (`FossPrivacyPolicyTest`); play has its own text in `app/src/play/res/raw/` (Pro purchase, billing permissions, no offline claim; `PlayPrivacyPolicyTest`). When data handling or permissions change, update both and the published copies (`docs/PLAY.md`). -- Bloodwork results may carry the lab's range (`refLow`/`refHigh`), a `<`/`>` `qualifier`, and for `other:` keys the printed `name` and `unit`. These fields are `@EncodeDefault(NEVER)` with no `require` (plain results store byte for byte as before; bad values never throw). Flag stored results with `MarkerResult.flag()` (null = no flag; `unclear` = a range but no flag), not `BloodMarker.flag`. Show them with `BloodMarker.formatResult` (keeps the `<`/`>`) and `rangeText(labRange)` marked "(lab)", or `printedText()`/`printedLabRange()` for `other:` keys; censored results are never plotted. -- Injection sites: `DoseLog.site` stores an `InjectionSites` key, never a label (unknown keys are kept and shown as they are). Rotation is per compound from *all* taken logs (`SiteRotation`; skipped ones ignored), never a windowed list. A one-tap (Today check, Log all, reminder Taken) records exactly the site its row showed, one per compound per tap (Today: a card's first pending row, after the site history has loaded; reminder: the compound's latest dose, `SiteRotation.latestDoses`, so the newest log keeps the chain); a picked past day and the widget record none, which ends the suggestion until a site is picked. Writes take a `SiteWrite` (`Keep` for re-logs, so nothing drops a site); edits save with `log.copy`. Shown as the short label in the Log dose sheet, Today rows, reminders and Journal dose lines; reports do not print sites. -- Room schema changes need a migration from version 2 on (version 1 is dropped destructively). Current version 4 (dose log `site`); `MigrationTest` opens real version 2 and 3 databases. -- Accessibility: 48 dp touch targets, content descriptions on icon buttons, status never by colour alone. Nothing clips at 360 dp and 130 % font: put fields side by side with `FieldRow(weights)` (it stacks them when one would wrap), a label and its value or action with `FitRow`, and give one-line text in a fixed share (chips with numbers, segments, figure cells, week cells, titles beside actions) `autoSize` down to `TrackerType.fitMin` instead of `maxLines = 1` alone; `Segmented` turns into radio rows by itself when a label cannot fit. `LargeFontLayoutTest` checks the main screens at that size. -- Styling: use `Tracker.colors`, `TrackerType`, `Spacing` and `Radii` (`ui/theme`), never raw colours or font sizes. Colour schemes live in `ui/theme/Palettes.kt`; `PaletteContrastTest` enforces WCAG AA for text pairs. -- Design review screenshots: `./gradlew :app:testFossDebugUnitTest --tests '*ScreenshotTest' -Pscreenshots.dir=`. The layout pass (`layoutPassFont100/130/200`: every tab, sheet, editor and Settings page at 360 dp, 100 %, 130 % and 200 % font) is the check for clipping. -- Pro tiers (play): `PlayPolicy` is the one table of what is free, limited, on trial or Pro; change a tier there and in `EntitlementTest`. Screens ask `FeatureGate` and show `PaywallSheet` when a limit stops an action. Limits only block adding: plan items already switched on (also restored or imported ones) stay on. Trial starts live in the settings (`trial_started`, so they ride in backups; a restore keeps the earlier start). -- Flavour tests: `src/test` runs for both flavours under `SharedTestApp` (`robolectric.properties`: every preset, foss names, everything unlocked), so it never depends on the flavour. Test a tier with the apps in `GatedApps.kt` (`@Config(application = PlayGatedApp::class)` etc.); flavour-only behaviour (seeding, wording resources, manifest) goes in `src/testFoss` / `src/testPlay` with `@Config(application = ProtocolTrackerApp::class)` when it needs the real container. -- Add or update tests with every behaviour change; run `:core:domain:test testFossDebugUnitTest testPlayDebugUnitTest assembleFossDebug assemblePlayDebug` before committing. -- Times, dates and volumes follow Settings > Units and formats through `DisplayFormat.current` (set by `SettingsStore` on every read). Format with `Formats` (app) or `DisplayFormat`, never a hard-coded `ofPattern`. Reports keep ISO dates and mL. -- Regexes run on ICU on Android, which rejects a `{` or `}` that is not part of a quantifier or `\p{…}` (the JVM accepts it; this crashed Bloodwork in v0.5.0-dev.1–4), and reads `\d`, `\s`, `\w`, `\b` and `(?i)` as Unicode (`\d` matches `٢`, `\s` a no-break space). Build every `Regex` once, as a property of a domain class: `AndroidRegexTest` fails on a regex anywhere else, checks each one for ICU's syntax and reads the import with ICU's character classes; `IcuRegexTest` (androidTest) compiles them all on a device. Text that `\d` matched goes through `BloodworkRules.normalizeChars` (ASCII digits) before `toDouble`. -- Insets: the app is edge-to-edge. Tab screens' Scaffold padding already holds the status bar; sheets end their column with `navigationBarsPadding()`; a sheet whose content can grow scrolls. Robolectric draws no system bars, so check insets on an emulator. -- Before a tag: with an emulator running (`emulator -avd `), run `./gradlew connectedFossDebugAndroidTest` (`SmokeTest` opens every tab, sheet and settings page in the real app and fails on any crash), then install the release APK (`adb install -r`), open every changed screen and read `adb logcat -b crash`. -- Animations follow the Motion setting (`ui/theme/Motion.kt`): screen transitions never animate size; use `Motions.spec` for new animations. Screens change with `Motions.screenEnter`/`screenExit`: a fade-through (the old screen fades out first, then the new one fades in), so two screens never overlap. A back swipe uses `Motions.predictivePopExit` (NavHost `predictivePopExitTransition`; shrink with the finger, slide off after release): Navigation's default drops the screen in one frame on release. Tab screens carry their own bar (`TabFrame` in `AppNav`), so the NavHost never resizes. Today switches days with `Motions.daySwitch`. - -## Definition of done -- The check command passes, with no new warnings. If `app/` UI code changed, the Robolectric tests (`testFossDebugUnitTest testPlayDebugUnitTest`) pass too (CI is enough); `lintFossDebug lintPlayDebug assembleFossDebug assemblePlayDebug` also passes when resources, the manifest or build files changed. -- Tests were added or updated for changed behaviour. -- UI: checked at 360 dp and 130 % font (`LargeFontLayoutTest`, the layout screenshot pass); before a tag, the emulator `SmokeTest` and the release APK on a device. - -## Git workflow -- Remote: GitHub ApolloF/SteroidTracker (the local folder is still named ProtocolTracker). -- Branch, PR, CI (`build`, `smoke`), then merge only when the user says "ship it" (auto-merge, squash). -- Releases: push a `v*` tag; the `release` job in `android.yml` builds the signed APK, checks `versionName` against the tag and publishes the GitHub release. - -## Secrets -- No `.env`. The release keystore and its passwords live outside the repo: locally in `keystore.properties` / `PT_KEYSTORE_*` env vars (gitignored), in CI as the `PT_KEYSTORE_BASE64`, `PT_KEYSTORE_PASSWORD`, `PT_KEY_ALIAS` and `PT_KEY_PASSWORD` repository secrets. `local.properties` (SDK path) is gitignored. -- Never commit real values or `*.jks` / `*.keystore` files. gitleaks runs on every commit. diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 43c994c..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1 +0,0 @@ -@AGENTS.md diff --git a/README.md b/README.md index dc81026..4b694a0 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ Requires JDK 21 and the Android SDK (`ANDROID_HOME`). ``` ./gradlew :core:domain:test testDebugUnitTest assembleDebug ``` -The APK is written to `app/build/outputs/apk/debug/`. See [AGENTS.md](AGENTS.md) for layout and conventions. +The APK is written to `app/build/outputs/apk/debug/`. Release builds are signed in CI when a `v*` tag is pushed and the `PT_KEYSTORE_BASE64`, `PT_KEYSTORE_PASSWORD`, `PT_KEY_ALIAS` and `PT_KEY_PASSWORD` secrets are set. Set `versionName` in `app/build.gradle.kts` to the tag without the `v` first; CI refuses to publish when they differ. A tag with a suffix (e.g. `v0.5.1-beta.1`) is published as a pre-release. Locally, put the same keys in an untracked `keystore.properties`. diff --git a/app/src/test/kotlin/com/apollof/protocoltracker/data/OldBackupImportTest.kt b/app/src/test/kotlin/com/apollof/protocoltracker/data/OldBackupImportTest.kt index 92b02a3..d4863b9 100644 --- a/app/src/test/kotlin/com/apollof/protocoltracker/data/OldBackupImportTest.kt +++ b/app/src/test/kotlin/com/apollof/protocoltracker/data/OldBackupImportTest.kt @@ -35,7 +35,7 @@ class OldBackupImportTest { val restored = repo.exportBackup() assertEquals(backup.items, restored.items) - // PresetMigration may move snapshot kinetics that still equal an older preset to the current one (AGENTS.md). + // PresetMigration may move snapshot kinetics that still equal an older preset to the current one. fun withoutPk(logs: List) = logs.map { it.copy(snapshot = it.snapshot.copy(pk = null)) } assertEquals(withoutPk(backup.logs), withoutPk(restored.logs)) assertEquals(backup.journal, restored.journal) diff --git a/docs/BACKLOG.md b/docs/BACKLOG.md index 9234893..84dce74 100644 --- a/docs/BACKLOG.md +++ b/docs/BACKLOG.md @@ -8,7 +8,7 @@ Written 2026-09-27 at HEAD 8b24b19; trimmed the same day (plan change below); ne ## Handover 2026-10-11: Google Play (read first) -Branch `ApolloF/playstore` = the Play flavour work of PR #11 (`feat/play-flavour`, merged in) plus the release-readiness pass. **Flavours are back** (`foss`, `play`; AGENTS.md › Layout), so the gate below and in §1 is now `:core:domain:test testFossDebugUnitTest testPlayDebugUnitTest lintFossDebug lintPlayDebug assembleFossDebug assemblePlayDebug`, and the on-device check is `connectedFossDebugAndroidTest connectedPlayDebugAndroidTest`. +Branch `ApolloF/playstore` = the Play flavour work of PR #11 (`feat/play-flavour`, merged in) plus the release-readiness pass. **Flavours are back** (`foss`, `play`), so the gate below and in §1 is now `:core:domain:test testFossDebugUnitTest testPlayDebugUnitTest lintFossDebug lintPlayDebug assembleFossDebug assemblePlayDebug`, and the on-device check is `connectedFossDebugAndroidTest connectedPlayDebugAndroidTest`. - PLAY-1 · Settings › Privacy policy shows the policy in the app (Play requires its link or text in the app). foss shows `PRIVACY.md` word for word; play has its own text (`app/src/play/res/raw/privacy_policy.md`: Pro purchase, billing permissions, no offline claim). `PRIVACY.md` no longer lists the network-state permission foss dropped. Tests: `PolicyTextTest`, `FossPrivacyPolicyTest`, `PlayPrivacyPolicyTest`. - PLAY-2 · CI `smoke` runs `SmokeTest` in both flavours. - PLAY-3 · `docs/PLAY.md`: open owner decisions, Play Console setup, App content answers (data safety, content rating, health declaration), listing text, screenshot rules, the per-release steps. @@ -20,7 +20,6 @@ Branch `pol-1-layout-and-tests` (PR, no tag), on v0.5.1. Done: **POL-1** (narrow - `ScreenshotTest.layoutPassFont100/130/200` walk every tab, sheet, editor and Settings page at 360 dp (light and dark at 100 % and 130 %, light at 200 %, the largest Android 14+ font). - Shared layout rules now: `FieldRow(weights)` stacks fields when one would wrap at its share; `FitRow` puts the end part under the start when a word would break; number chips, segments, figure cells, the week strip and the item editor title shrink to fit one line (`TrackerType.fitMin`; week cells never below `cellTextMin`); word chips take a second line; `Segmented` becomes radio rows when a label cannot fit; the dose field sizes its figures to its width. `LargeFontLayoutTest` (in the gate) fails when any text on Today, Log dose, the item editor, Level detail or Settings › Today and Times of day is cut at 360 dp and 130 %. - Open from the pass: §7 (200 % only). -- Next: the owner picks from the menu in `~/Notes/claude/weekend-2026-10/results/protocoltracker-next.md` (RECO-1/2, JV-1/2, SIM-11; POL-22 closed). ## Handover 2026-10-03 (read first) @@ -86,14 +85,14 @@ Detailed specs for each item were written during design (by id; merged items nam 3. While working, run targeted tests (one module or class). Before pushing, run the full gate once. 4. Only when UI or shared app code changed: look at dev screenshots of the changed screens (light and dark) and run the stable guard. 5. Commit; add a §6 row (hashes, one line) and mark the item done in §3; `git pull --rebase origin main` (docs conflicts: keep both sides; code conflicts: resolve and rerun the targeted tests; a rebase that brought only other commits and no conflicts needs no second gate); push. Findings go to §3, §4 or §7. -6. **Release** at each RELEASE line in §3 (every 3–4 items): one audit of the diffs since the last release (bugs, edge cases, tests, docs, AGENTS.md, anything to remove or merge), fix what it finds, then publish the dev pre-release (Releases bullet below). +6. **Release** at each RELEASE line in §3 (every 3–4 items): one audit of the diffs since the last release (bugs, edge cases, tests, docs, anything to remove or merge), fix what it finds, then publish the dev pre-release (Releases bullet below). - **Gate:** `./gradlew :core:domain:test testDebugUnitTest lintDebug assembleDebug`. One class: `:app:testDevDebugUnitTest --tests …`. After manifest, keep-rule or dev source-set changes also `:app:minifyDevReleaseWithR8 :app:minifyStableReleaseWithR8`. - **Every acceptance below also means:** the gate green once before pushing; when UI or shared app code changed, also the stable guard clean and dev screenshots of changed screens looked at. - **Stable guard:** stable tests, pinned strings and screenshots stay unchanged (pixel-identical to the 8b24b19 baseline; fix a changed PNG, never explain it away; new PNGs only from dev-only tests). Never change `seed()` or an existing `ScreenshotTest` flow; new screens get their own `@Test` and data. `BuildConfig.DEV_FEATURES` is the only gate (§4.4 P5); invisible data-safety fixes go into both flavors. Each new dev entry point gets a `DevEntryPointsTest` row (in dev, not in stable, after an anchor in both). - **Tests:** every behaviour change. Robolectric: no `runBlocking` inside `waitUntil`; scroll into view or use semantics actions before touch; gesture tests in their own class; `@Before` as `fun x(): Unit = runBlocking { … }`. - **Data:** a Room change bumps the version with a real migration, `MigrationTest` and the exported schema, and never drops data. Old backups restore; a dev backup restores in stable. New stored fields: `@EncodeDefault(NEVER)`, no `require`. -- **Commits:** small, on `main`, no attribution lines, pushed after a green gate. Never force-push, rewrite history or touch signing secrets. Update AGENTS.md and the README dev section in the same commit. +- **Commits:** small, on `main`, no attribution lines, pushed after a green gate. Never force-push, rewrite history or touch signing secrets. Update the README dev section in the same commit. - **Releases:** bump `versionCode`; set only the dev flavor's `versionName` (e.g. `0.5.0-dev.2`), never `defaultConfig.versionName` (stable Settings shows it; CI's "Check version names" step fails on a mismatch with the tag); write `docs/releases/.md`; tag `vX.Y.Z-dev.N`, push, `gh run watch`. On red CI: fix, delete the unpublished tag, re-tag. Never publish on red. - **Where things go:** decisions in §4 before building; owner-only checks in §5 with a default; audit findings in §7. If the gate cannot go green without the owner, write what and why in §8 and stop. @@ -179,7 +178,7 @@ Why: dev reads charts two ways. TrendChart (BP chart, marker sheet) always taps ### AUD-12 · Compare mode and Settings › Experimental gone from dev (SIM-7, SIM-3) · S · done (§6) Why: compare is the most settings-heavy feature (three stored settings, an extra segmented row, a baseline, anchor and exclusion UI), and owner check 43's default is "removed in dev". After AUD-11 the Experimental page holds nothing dev uses, yet the Settings overview carries an eighth row for it. - Dev: `compareAvailable` false and no compare references; the Settings overview skips `SettingsPage.EXPERIMENTAL`. Stable keeps both until graduation, said in that release note (§4.5). -- `DevEntryPointsTest` rows: the Experimental row in stable only; the Compare segment absent in dev. README dev section and AGENTS.md's dev-feature list (it still omits the import, trends, sites and web import) follow. Dev Settings and Levels screenshots; stable guard unchanged. +- `DevEntryPointsTest` rows: the Experimental row in stable only; the Compare segment absent in dev. README dev-feature list (it still omits the import, trends, sites and web import) follows. Dev Settings and Levels screenshots; stable guard unchanged. ### AUD-13 · Levels overview: the range row and the curves (dev; SIM-8, SIM-9) · S · done (§6) Why: the overview opens with two full-width segmented rows above the first chart, and each card repeats the detail screen's 4–5 figure tiles (NOW repeats the jump bar): 12–15 tiles with three compounds, a dashboard where a glance is wanted. @@ -194,7 +193,7 @@ AUD-1…13 are done and released (v0.5.0-dev.5…dev.8). A walk through the dev ### DEV-1 · An on-device smoke test (both flavors) · M · done (§6) Why: every crash so far (the ICU regex) passed JVM and Robolectric tests; the on-device check before a tag is manual. -- `app/src/androidTest`: one Compose test that starts `MainActivity`, opens every tab, the Log menu and each of its sheets, Bloodwork › Import results, a Levels detail screen and Settings › every page, and fails on any crash. Runs with `./gradlew connectedDevDebugAndroidTest` (and stable) against a running emulator; AGENTS.md and §1's release rule name it. +- `app/src/androidTest`: one Compose test that starts `MainActivity`, opens every tab, the Log menu and each of its sheets, Bloodwork › Import results, a Levels detail screen and Settings › every page, and fails on any crash. Runs with `./gradlew connectedDevDebugAndroidTest` (and stable) against a running emulator; §1's release rule names it. ### DEV-2 · Time pickers follow the 12/24-hour setting (dev) · S · done (§6) Why: the dial is always 24-hour while the app shows "12:15 AM"; picking 11 PM means finding 23. @@ -457,21 +456,21 @@ Only the owner can verify these. Each default holds until he answers. | 0445fb6 | TR-3 · `ui/components/TrendChart` (`TrendSeries` with SOLID/DASHED/NONE lines and DIAMOND/DOT/RING marks, `TrendBand`, fixed `yRange`, 160 dp): x by time with an 8 dp inset so edge marks are whole, y labels in a 40 dp gutter, start and end dates (with the year when they differ), band in `accentSoft`, lines and marks in `accent`. `onSelect = null` is static (no pointer input, no cursor, no exclusion); otherwise a tap selects the nearest point in time within 24 dp, a sideways slide past slop consumes and moves point by point with `ScrubHaptics.tick()` per new point, a vertical drag is released to the page, `systemGestureExclusion`; `selectedAt` draws a cursor line and a larger ink ring; the caller shows the reading. Pure `TrendAxis`: `xWindow` (every point, widened back from the end to a minimum span, optional end), `yRange` (data and band limits padded 10 %, not below 0 for non-negative data, a single value gets a span, fixed wins), `bandSpan` (open side to the plot edge), `yTicks` (1/2/2.5/5 × 10ⁿ, 2–3 labels, one decimal count), `nearest`. `TrendAxisTest` (10, plain JVM), `TrendChartTest` (4, Robolectric, own class: static chart scrolls and ignores taps, tap within/beyond 24 dp, slide selects day 0 → 20 → 40 without scrolling, vertical drag scrolls and selects nothing). Dev-only `ScreenshotTest.trendChart` at 360 dp (light, dark, pure black: hematocrit band, one-sided HDL band, 26 weeks of BP with a selection) looked at; band visible in dark and black, lines distinct. Unused by screens yet; stable guard clean. | | 4d04958, d5419a1 | Audit of v0.5.0-dev.2 (`v0.5.0-dev.1..v0.5.0-dev.2`: HIST-1/3, BW-13, BWI-1/2/4, TR-1, SITE-1) · web draws keep values entered in conventional units (`WebExportImport.exactValue`, test failed before with 200.0012); README and notes point to Settings › Export and data; `ScreenshotTest.webImportDialog` also renders dark (both looked at). Two reader gaps left for BWI-3/BWI-5 (§7). No app code changed; stable guard not needed. | | e4ae8ef | TR-4 · Domain: `MarkerTrend` keeps only the latest result (`previous*` removed, tests trimmed); `unlistedTrends` (latest per unlisted key, by printed name, flagged by lab range only); `markerSheetData` (results newest first, chart points only with 2+ plottable, band = latest plotted result's range with `bandFromLab`, `leftOut`, `showsRange`). Dev: `JournalViewModel.markerSheet` computes only the tapped key; card rows (`ResultRow`, 56 dp, chevron, "Show results over time") drop "before X"; unlisted rows after the known markers, a flagged Low/High shown, the rest behind "Other tests (N)"; `ui/health/MarkerSheet` (static `TrendChart` with diamonds, 60-day minimum window, "Shaded: lab range of the latest result, …" / "typical adult male range, …", "Results with < or > are listed, not plotted.", ALL RESULTS with date · lab, Settings units, own flag, ref only where it differs from the band; unlisted as printed, no chart). `BloodworkHistoryTest` (+5), `MarkerSheetTest` (4, dev), `DevEntryPointsTest.journalMarkerSheetRows`; `ScreenshotTest.markerSheet`/`markerSheetNarrow` (dev, 411 and 360 dp, light, dark, black; band visible, no clipping). Stable guard clean. | -| b4f78c7 | SITE-2 · Room 3→4: `MIGRATION_3_4` adds `dose_logs.site` (exported `4.json`), entity and mappers. `SiteWrite` (domain: `Keep` reuses the stored site, `Set(key or null)` writes, blank is none); `logOccurrence`/`logUnscheduled`/`DoseActions.take` take `site = SiteWrite.Keep`, a skipped dose never keeps a site; `logOccurrenceIfAbsent(site)` and `takeKeys(keys, sites)` store the shown site and never overwrite a recorded dose. No caller changes. Tests: `MigrationTest.version3KeepsEveryFieldAndAddsNoSite` (real v3 db, one dose log and one bloodwork entry, every field equal, site null; the v2 test now runs 2→3→4), `TrackerRepositoryTest` (re-log keeps, `Set(null)`/blank clear, skip drops; new logs and stale actions; backup keeps a site), `DoseActionsSiteTest` (2, both flavors), `SiteRotationTest.siteWriteKeepsSetsAndClears`. AGENTS.md: version 4. Stable guard unchanged. | +| b4f78c7 | SITE-2 · Room 3→4: `MIGRATION_3_4` adds `dose_logs.site` (exported `4.json`), entity and mappers. `SiteWrite` (domain: `Keep` reuses the stored site, `Set(key or null)` writes, blank is none); `logOccurrence`/`logUnscheduled`/`DoseActions.take` take `site = SiteWrite.Keep`, a skipped dose never keeps a site; `logOccurrenceIfAbsent(site)` and `takeKeys(keys, sites)` store the shown site and never overwrite a recorded dose. No caller changes. Tests: `MigrationTest.version3KeepsEveryFieldAndAddsNoSite` (real v3 db, one dose log and one bloodwork entry, every field equal, site null; the v2 test now runs 2→3→4), `TrackerRepositoryTest` (re-log keeps, `Set(null)`/blank clear, skip drops; new logs and stale actions; backup keeps a site), `DoseActionsSiteTest` (2, both flavors), `SiteRotationTest.siteWriteKeepsSetsAndClears`. Stable guard unchanged. | | 2d95866 | BWI-3 (was BW-10, BW-11) · `io/labimport`: `BloodworkImport.read(text, today)` → `ImportRead.Refused` or `Found(ImportDraft)`. `RowReader` (§4.2-4.3, §5.3, §7): value (no value, not a number, negative → not imported, the no-value word or note into the entry note), identity (the name wins; `ByName`/`ByKey` need a printed accepted unit and clean numbers, else unlisted with C12; `neverUnit` → C2; plain Glucose keyed glucose → C11), ambiguous thousands (block style, else the only possible reading, else left out), missing or wrong units (the only fitting unit with C1, else left out or not imported), C4 impossible (repairs → left out, none → not imported), C1 range fits another unit (free T molar ↔ mass only), C2 lost decimal, consistency (C6, free T exempt), a range printed in another unit is not used, ranges by sex (C5). `ImportDrafts`: per-block decimal style, duplicates per draw (same value once, a later block wins with C7, two values in one block → both left out with Q4's text, unlisted `…_2`), draws with an uncertain date left out whole with D1-D4's text, received caption. `Review.of(draft, choices, existing, zone)`: `RowState` READY/LEFT_OUT/SAME_DAY/UNCERTAIN/NOT_IMPORTED/ALREADY_SAVED (by local date via `sameResult`; another value that day starts left out with C8, which replaces the caption; Keep includes it), counts, the S2-S5 line (S2 without the question count; a repeated name counts), `saveLabel`, `entries` (one per dated draw, 12:00 default, table order then unlisted, lab ≤ 80, note capped). Texts in `ImportMessages`. Fixtures F01, F08, F18, F20, F21, N02, N08, N15, N16, N18, N22 (+ F02, F03, F09): `ImportDraftTest` (14), `ImportReviewTest` (9, incl. the corpus test: every fixture with rows left out and kept saves entries that survive a backup round trip; the same answer twice saves nothing). Domain only, unused by the app until BWI-5; stable guard not needed. | | 074d8e3 | TR-5 · Dev: `JournalState.bpWeeks` (`bloodPressureWeeks`, computed only in dev and under the Blood pressure chip; `BpSummary` untouched); `ui/journal/BpTrend.kt` `BpTrendBlock` inside the BP card under the unchanged header, only with 2+ weeks: interactive `TrendChart` (systolic SOLID + DOT, diastolic DASHED + RING, no band, y from the data, x from the oldest week to now), caption "Each point is a 7-day average" replaced by the selected week's `describe()`, local selection, description "Blood pressure chart, 7-day averages, N points from … to …, latest 122/80 mmHg". `BpTrendTest` (3, dev: chart and caption under the chip, none on All, one week or only readings older than 26 weeks add nothing), `BpTrendTapTest` (tapping the newest point reads "Last 7 days · 128/83 mmHg · 2 readings", the card's average), `DevEntryPointsTest.journalBpChart`; `ScreenshotTest.journalBp`/`journalBpNarrow` (dev, 411 and 360 dp, light, dark, black, plus an older week selected: no clipped y labels, lines distinct, longest caption on one line). Stable guard clean. | -| 25081bd | BWI-5 (was BW-14, BW-15, BW-18) · Dev: `BloodworkImportRoute` registered only in dev; the new-draw Bloodwork sheet shows "Import results" (not in edit mode) from Today's Log menu and Journal's "+". `BloodworkImportScreen`/`ViewModel` (`ui/health`): Start (help §10.1, Copy AI prompt, Paste answer reading the clipboard on the tap, capped at 200,001 characters, the refusal as one line); Check (notices, one card per draw newest first with date, time and lab, the draw's reason or caption; rows: app name, printed name, value and unit as printed, flag word, one line; left-out rows struck through with "Left out." or their reason; "N already saved" / "N not imported" fold open; the S line above Save; a tap toggles). Back from Check → Start, after a tap "Discard this import?" (Discard / Keep checking; `ConfirmDialog` got a `dismiss` label). Save: one `saveJournal(list)`, then `JournalFocus` (in `AppContainer`) hands Journal "Bloodwork saved" / "N blood draws saved" with Undo (`deleteJournal(ids)`); Journal selects the Bloodwork chip. Domain: `ImportMessages.saved`, a button with nothing to save reads "Save". Tests: `BloodworkImportScreenTest` (7: paste, refusals, leave out and keep, Back and discard, two draws saved at 08:40 and 12:00 with lab ranges, the same answer again), `BloodworkImportNavTest` (Log › Bloodwork › Import results › Save → Journal chip, snackbar, Undo), `DevEntryPointsTest.bloodworkSheetImportRow` and `bloodworkImportRoute`; dev screenshots `bloodwork-sheet-import`, `import-start`, `import-start-message`, `import-check`, `import-check-end` (light and dark) looked at; stable guard unchanged. AGENTS.md and README dev section updated; the release note comes with v0.5.0-dev.3. | +| 25081bd | BWI-5 (was BW-14, BW-15, BW-18) · Dev: `BloodworkImportRoute` registered only in dev; the new-draw Bloodwork sheet shows "Import results" (not in edit mode) from Today's Log menu and Journal's "+". `BloodworkImportScreen`/`ViewModel` (`ui/health`): Start (help §10.1, Copy AI prompt, Paste answer reading the clipboard on the tap, capped at 200,001 characters, the refusal as one line); Check (notices, one card per draw newest first with date, time and lab, the draw's reason or caption; rows: app name, printed name, value and unit as printed, flag word, one line; left-out rows struck through with "Left out." or their reason; "N already saved" / "N not imported" fold open; the S line above Save; a tap toggles). Back from Check → Start, after a tap "Discard this import?" (Discard / Keep checking; `ConfirmDialog` got a `dismiss` label). Save: one `saveJournal(list)`, then `JournalFocus` (in `AppContainer`) hands Journal "Bloodwork saved" / "N blood draws saved" with Undo (`deleteJournal(ids)`); Journal selects the Bloodwork chip. Domain: `ImportMessages.saved`, a button with nothing to save reads "Save". Tests: `BloodworkImportScreenTest` (7: paste, refusals, leave out and keep, Back and discard, two draws saved at 08:40 and 12:00 with lab ranges, the same answer again), `BloodworkImportNavTest` (Log › Bloodwork › Import results › Save → Journal chip, snackbar, Undo), `DevEntryPointsTest.bloodworkSheetImportRow` and `bloodworkImportRoute`; dev screenshots `bloodwork-sheet-import`, `import-start`, `import-start-message`, `import-check`, `import-check-end` (light and dark) looked at; stable guard unchanged. README dev section updated; the release note comes with v0.5.0-dev.3. | | 050092d | SITE-3 · `LogDoseSheet(sites = …)`: an optional hook (TodayScreen passes it in dev from `TodayViewModel.siteLogs`, all dose logs) adds `SiteRow` between the dose block and Time for `Route.INJECTION`, extra doses too: the sites used plus the suggestion (preselected), caption "Last: L delt · Yesterday", More for all 12; "Choose site" before any site; a broken chain selects none; a taken dose reopens at its own site ("Last" leaves it out); tapping the selected chip clears; saves pass `SiteWrite.Set`, no hook → `Keep` (stable unchanged, re-saving keeps a site); Skip stores none. Chips are `QuickChip` (48 dp, selected state) announced by the long label. Domain: `SiteRotation.forDose` → `SiteChoice(state, initial, offered, all)`, 4 tests. `LogDoseSiteTest` (8: preselected and logged, another chip, clear, Choose site → all 12, oral no row, no hook no row + Keep, dev Today log stores and reopens, both flavors re-save keeps the site), `DevEntryPointsTest.logDoseSheetSiteRow`, `ScreenshotTest.logDoseSite` (dev, 360 dp, light/dark/More; looked at). Stable guard unchanged. | | 1e93a1d | Audit of v0.5.0-dev.3 (`v0.5.0-dev.2..af21d6d`: BWI-3, BWI-5, TR-2…TR-5, SITE-2, SITE-3) · N1 asks the chatbot for the whole answer again (a "continue" reply has no date line, so its results were left out); a v1 block with `;` or `,` cells answers M5 "another layout", not M12 (`BlockReader.isOtherSeparatorRow`: a listed key or `other` first, 3+ marks); `BloodworkImportScreenTest.copyAiPromptPutsThePromptOnTheClipboard`; import doc N1 and the copy snackbar text. `BlockReaderTest` and `ImportDraftTest.theWholeAnswerAgainReadsEveryResult`; the separator case failed before (M12). Dev screenshots of the import, marker sheet, BP chart and Bloodwork sheet looked at (`shots/audit-wt-a2`); no UI or shared app code changed, so no stable guard. | | (this commit) | Release v0.5.0-dev.3 (tag) · dev pre-release: bloodwork import v1 (BWI-3, BWI-5), last draw, marker sheet and BP chart (TR-2…TR-5), site stored with dose logs and the Log dose sheet's Site row (SITE-2, SITE-3); versionCode 7, dev `versionName` 0.5.0-dev.3; notes in `docs/releases/v0.5.0-dev.3.md` (Room 3→4; a backup restores in 0.4.0 without sites); README dev section adds the BP chart and injection sites. Audit already done (1e93a1d, 04efe6a). | | 35b9267 | SITE-4 · `TodayViewModel` (dev) derives each compound's suggested site from `siteLogs` (all dose logs, not the windowed ones); `withSites` ends a card's rows with it: pending injectables get the suggestion on the compound's first pending row per card (Missed card and each group), taken rows (groups, Logged late, extras) the recorded site, skipped none ("35.71 mg · 0.18 mL · pin 5/7 · R delt"). `DoseItem.site`/`siteWrite`: the check, the Missed check and Log all pass `SiteWrite.Set` with the shown site (none shown → `Keep`); snackbars "Test C taken · R delt" / "Test C logged · R delt". Day sheet rows show and record none; stable unchanged (no suffix, checks store none). `TodaySiteTest` (6: check records the shown site, a missed check moves today's row on, a site-less `takeKeys` log removes the suffix, Log all records each shown site never twice per compound (hCG its own), Day sheet backfill stores none, stable no suffix + none), `DevEntryPointsTest.todayRowSiteSuffix`, `ScreenshotTest.todaySite` (dev, light/dark; looked at). Gate green; stable guard unchanged (new PNGs dev-only). | | 7ece434 | SITE-5 · `AlarmReceiver` (dev) posts each dose reminder with sites from `SiteRotation.firstDoses` (new, domain: a compound's suggestion from all dose logs on its first due injectable only), so a line ends "Test C (testosterone cypionate) · 100 mg · 0.5 mL · R VG"; `Notifications.showDoses(sites = …)` (default empty, stable unchanged) puts them on Taken / Take all as `EXTRA_SITES` (aligned with the keys, only when a line has one) and `NotificationActionReceiver` passes them to `takeKeys`, which never overwrites a logged dose. Snooze re-posts recompute; the widget and the daily summary are unchanged. Both receivers' work moved to `handle` for tests. `ReminderSiteTest` (4, both flavors: without sites the title, text and lines are pinned and Taken stores none; after L VG dev shows and stores vg_r, stable the pinned line and none; dev Take all with two Test C doses and hCG records vg_r, none, abdomen_r; a stale Taken after a Today log keeps delt_l), `SiteRotationTest.aBatchShowsEachSuggestionOnTheCompoundsFirstDoseOnly`, `DevEntryPointsTest.reminderSiteSuffix`. No screen changed. | -| c99ee4f | SITE-6 · `JournalViewModel` (dev) ends a taken dose's detail with its short site label, as a value switch ("125 mg · 0.63 mL · R VG"); skipped lines and stable unchanged; `EditLogDialog` keeps the site through `log.copy` (both flavors). `JournalSiteTest` (editing a sited dose keeps vg_r, both flavors), `DevEntryPointsTest.journalDoseSite` (exact line per flavor), `ScreenshotTest.journalSite` (dev, 360 dp, light/dark; looked at). Guard rows now cover every site surface (sheet row, Today suffix, reminder suffix, Journal line). AGENTS.md gains the site convention; README dev section updated. Gate green; stable guard unchanged (new PNGs dev-only). | -| 53de706 | Audit of v0.5.0-dev.4 (`v0.5.0-dev.3..0e6f0bd`: SITE-4…SITE-6) · Today's rows wait for the site history in dev (`siteSuggestions`: nothing until every dose log has loaded; stable keeps `flowOf(emptyMap())`), so no row shows without its site and a quick check never records none; a reminder puts a compound's site on its latest dose (`SiteRotation.latestDoses`, was `firstDoses`; `SiteRotation.suggestions` shared with Today), so Take all keeps the chain; README says what records no site; AGENTS.md follows. `SiteSuggestionsTest` (failed before), `SiteRotationTest.aBatchKeepsTheChainGoing`, `ReminderSiteTest.takeAllRecordsEachShownSiteOncePerCompound` (the next suggestion exists). Dev screenshots `shots/audit-sites` looked at; stable guard unchanged. Findings §7. | +| c99ee4f | SITE-6 · `JournalViewModel` (dev) ends a taken dose's detail with its short site label, as a value switch ("125 mg · 0.63 mL · R VG"); skipped lines and stable unchanged; `EditLogDialog` keeps the site through `log.copy` (both flavors). `JournalSiteTest` (editing a sited dose keeps vg_r, both flavors), `DevEntryPointsTest.journalDoseSite` (exact line per flavor), `ScreenshotTest.journalSite` (dev, 360 dp, light/dark; looked at). Guard rows now cover every site surface (sheet row, Today suffix, reminder suffix, Journal line). README dev section updated. Gate green; stable guard unchanged (new PNGs dev-only). | +| 53de706 | Audit of v0.5.0-dev.4 (`v0.5.0-dev.3..0e6f0bd`: SITE-4…SITE-6) · Today's rows wait for the site history in dev (`siteSuggestions`: nothing until every dose log has loaded; stable keeps `flowOf(emptyMap())`), so no row shows without its site and a quick check never records none; a reminder puts a compound's site on its latest dose (`SiteRotation.latestDoses`, was `firstDoses`; `SiteRotation.suggestions` shared with Today), so Take all keeps the chain; README says what records no site. `SiteSuggestionsTest` (failed before), `SiteRotationTest.aBatchKeepsTheChainGoing`, `ReminderSiteTest.takeAllRecordsEachShownSiteOncePerCompound` (the next suggestion exists). Dev screenshots `shots/audit-sites` looked at; stable guard unchanged. Findings §7. | | (this commit) | Release v0.5.0-dev.4 (tag) · dev pre-release: suggested site on Today rows recorded by the check and Log all (SITE-4), on dose reminders recorded by Taken (SITE-5), on Journal dose lines (SITE-6); versionCode 8, dev `versionName` 0.5.0-dev.4; notes in `docs/releases/v0.5.0-dev.4.md` (steps, rotation rule, how to change it, what records no site; no Room change, version 4; a backup restores in 0.4.0 without sites); README dev section links the notes and says how to change a site. Audit already done (53de706). | | (this commit) | Backlog: dev build audit 2026-09-28 (§7) and new plan: Open is AUD-1…13 with releases v0.5.0-dev.5…dev.8; Later reviewed (items taken into AUD, triggers, AUD-14 and AUD-15 added), BW-16, BW-19 and DISC-1 not doing; §4.6 corrected. Docs only. | | cf65e1f | AUD-2 · `DrawDates` asks D2 for datum aanvraag, aanvraag datum, aangevraagd, datum uitslag, uitslag datum, datum validatie, gevalideerd, ordered, date ordered, order date, requested and request date; datum ontvangst and ontvangst datum get the received caption; "Datum afname" and "Afnamedatum" read clean. `normalizeUnit` maps mIE/l, mIE/ml and µIE/ml to the IU forms. `LabValuesTest.requestResultAndValidationDatesAreAsked` and the received test, `ImportDraftTest.dutchInternationalUnitsAreRead` (TSH 1,9 mIE/l Ready at 1.9 without caption, prolactin 250 mIE/l at 11.8 ng/mL, LH mIE/ml), `MarkerVocabularyTest` and `BloodworkRulesTest` spellings. Prompt unchanged; import doc §3.5 and §5.5 follow. Domain only; gate green; stable guard not needed. | -| 9c16f80 | AUD-1 · `DoseAdjust.fromField` (domain): a dose field reading the plan or the stored amount as shown (`fieldValue`, four decimals) stands for that amount itself, the plan first. Dev: `LogDoseSheet` saves it (250 mg/week daily logs 35.714285… mg, not an adjusted 35.7143), the diff line compares with the shown plan (no "+0 mg vs plan"), the Log button rounds like "Plan:" ("Log 35.71 mg"); `EditLogDialog` keeps the stored amount (or the plan) when the field is unchanged. Stable unchanged (`BuildConfig.DEV_FEATURES` / `devOr`). `SiteRow`'s expand chip reads "All sites" (was a second "More" next to the dose "+"). `ConversionTest.anUnchangedFieldStandsForThePlanOrTheStoredAmount`, `LogAsPlannedTest` (dev: Today sheet with a site → amount equals `plannedAmount`, no "(plan" on Today or Journal, a note-only Journal edit keeps it; a typed 40 still saves 40), `LogDoseSiteTest` and `ScreenshotTest.logDoseSite` (now `log-dose-site-all-dark`) follow the label. AGENTS.md notes the rule. Gate green; stable guard unchanged; dev screenshots `shots/aud1` looked at. | +| 9c16f80 | AUD-1 · `DoseAdjust.fromField` (domain): a dose field reading the plan or the stored amount as shown (`fieldValue`, four decimals) stands for that amount itself, the plan first. Dev: `LogDoseSheet` saves it (250 mg/week daily logs 35.714285… mg, not an adjusted 35.7143), the diff line compares with the shown plan (no "+0 mg vs plan"), the Log button rounds like "Plan:" ("Log 35.71 mg"); `EditLogDialog` keeps the stored amount (or the plan) when the field is unchanged. Stable unchanged (`BuildConfig.DEV_FEATURES` / `devOr`). `SiteRow`'s expand chip reads "All sites" (was a second "More" next to the dose "+"). `ConversionTest.anUnchangedFieldStandsForThePlanOrTheStoredAmount`, `LogAsPlannedTest` (dev: Today sheet with a site → amount equals `plannedAmount`, no "(plan" on Today or Journal, a note-only Journal edit keeps it; a typed 40 still saves 40), `LogDoseSiteTest` and `ScreenshotTest.logDoseSite` (now `log-dose-site-all-dark`) follow the label. Gate green; stable guard unchanged; dev screenshots `shots/aud1` looked at. | | c68fa15 | Device report: the Bloodwork crash · `BloodworkRules.ANNOTATION` escaped for ICU (and `LabValues.UNIT`'s braces); `AndroidRegexTest` checks every `Regex` held by a domain class. Reproduced and checked on the emulator with the dev release APK. | | c797877, 0293cb4, d3f6b56 | Found on the emulator · dev tab screens drop the doubled status-bar gap (`tabScreenTop`); dev Journal Undo snackbars time out (Long, was indefinite); the Note sheet scrolls and caps its field (both flavors); a new dev plan item starts today (no Missed card for days before it existed). `JournalUndoTest`, `NoteSheetTest`, `NewItemStartTest`. | | f378202, bd9db5c | Release v0.5.0-dev.5 (tag) · the fixes above plus AUD-1 and AUD-2; versionCode 9; notes `docs/releases/v0.5.0-dev.5.md`; on-device smoke on API 36 and API 34 (gesture and three-button). | @@ -480,16 +479,16 @@ Only the owner can verify these. Each default holds until he answers. | (this commit) | AUD-5 · Journal's chip row has a `LazyListState`; in dev a change of filter or compound scrolls the least distance that shows the selected chip whole (`reveal`, snap at Motion Off). `JournalChipRevealTest` (dev, 360 dp: after an import's focus request the Bloodwork chip is selected and displayed). The dev screenshot frame is left for the next screenshot pass (no screenshot dir on this machine). | | (this commit) | AUD-6 · Dev Journal › All shows one 56 dp `BloodworkSummary` row ("Bloodwork · last draw 3 days ago · 2 out of range", the Adherence row's style, click label "Show bloodwork") that selects the Bloodwork chip; the full card only under that chip. Trends are still computed on every filter (cheap; the count needs them). `JournalBloodworkRowTest` (20-marker draw: one row on All, 20 marker rows under the chip); `MarkerSheetTest`, `DevEntryPointsTest.journalMarkerSheetRows` (the row in dev only) and the dev screenshot flows `journalLabRanges` and `markerSheet` open the chip first. | | (this commit) | AUD-7 · Check results opens with the muted caption "Tap a result to leave it out." (`ImportMessages.TAP_TO_LEAVE_OUT`); a tapped-out row reads "Left out. Tap to keep."; `Review.line` (S2) names rows left out by a tap as well as those left out with a reason, using `ReviewRow.label` (moved to the domain from the screen). `ImportReviewTest.aTapLeavesAResultOutAndKeepsIt` (the line names it, then none), `BloodworkImportScreenTest.aTapLeavesARowOutAndKeepsItAgain` (caption, row text, line). Screenshots left for the screenshot pass. | -| (this commit) | Audit of the diffs since v0.5.0-dev.5 (AUD-3…7; one reviewer pass): a compound filter missing from the chip list no longer points the reveal at the wrong chip; the other two notes were not bugs (draft rows are renamed before any choice; a summary with only unlisted results reads "Bloodwork · last draw …"). Release v0.5.0-dev.6 (tag) · versionCode 10; notes `docs/releases/v0.5.0-dev.6.md`; gate green; on the emulator (API 36) with the dev release APK: Earlier at 00:15 → "Yesterday 11:15 PM" saved on yesterday, Check caption, "Left out. Tap to keep." and the S2 line, Save → Bloodwork chip in view, All shows the one row, Undo gone after ten seconds. AGENTS.md: the EOFException test-results quirk. | +| (this commit) | Audit of the diffs since v0.5.0-dev.5 (AUD-3…7; one reviewer pass): a compound filter missing from the chip list no longer points the reveal at the wrong chip; the other two notes were not bugs (draft rows are renamed before any choice; a summary with only unlisted results reads "Bloodwork · last draw …"). Release v0.5.0-dev.6 (tag) · versionCode 10; notes `docs/releases/v0.5.0-dev.6.md`; gate green; on the emulator (API 36) with the dev release APK: Earlier at 00:15 → "Yesterday 11:15 PM" saved on yesterday, Check caption, "Left out. Tap to keep." and the S2 line, Save → Bloodwork chip in view, All shows the one row, Undo gone after ten seconds. | | (this commit) | AUD-8 · `journalDayHeader` (built in `JournalViewModel`, `JournalDay.header`): dev "Today", "Thu, Sep 24", or `Formats.dayYear` when not this year (no ISO date); stable unchanged ("Today · 2026-09-24"). Dev BP card: "Latest 123/83 mmHg" (numeric, ink) and the day and time (caption, muted) on two lines. `JournalHeadersTest` (both flavors). Screenshots left for the screenshot pass. | | (this commit) | AUD-9 · Dev Today rows (Missed, logged late, groups) and Day sheet rows pass no category tag (`rowTag`); Plan and the Log dose sheet keep it. `DoseRow(holdTokens = true)` (dev at those four calls) shows the detail through `tokensTogether`: non-breaking spaces inside each " · " token and before each "·", so lines break only after a separator; its semantics keep the plain text (TalkBack, tests). `DevEntryPointsTest.todayRowsHaveNoCategoryTag`. Screenshots left for the screenshot pass. | | (this commit) | AUD-10 · Dev Symptoms sheet: no advice caption; headings "Often listed with low estrogen", "Often listed with high estrogen", "Other" (`symptomHeading`, sheet only); Journal and Today symptom lines keep the names and mood, no "1 low-E2 · 1 high-E2" counts; Log menu: Note "Anything else, in your own words" (dev, `devOr`), Symptoms "Symptoms, mood and hair shedding". Stable, reports and domain labels unchanged. `DevEntryPointsTest.logMenuSymptomsAndBloodworkRows` (subtitles), `symptomCopyWithoutVerdicts`, `symptomSheetWithoutAdvice`. | | (this commit) | Audit of the diffs since v0.5.0-dev.6 (AUD-8…10): nothing found; stable paths keep their exact strings. Release v0.5.0-dev.7 (tag) · versionCode 11; notes `docs/releases/v0.5.0-dev.7.md`; gate green; on the emulator with the dev release APK: Today row without tag and a new item without Missed, Log menu subtitles, Journal headers "Yesterday" and "Mon, Jun 2 2025". | | (this commit) | AUD-11 · `LevelsViewModel`: `scrub` and `haptics` true in dev (`devOr`; the stored switches stay, ignored). The reading clears on a range change and on Back to now in dev (overview and detail). The scrub gesture already leaves mostly vertical drags to the list. `LevelsAlwaysScrubTest` (both flavors: dev shows the panel on a tap without the switch and Back to now clears it; stable shows none). | -| (this commit) | AUD-12 · Dev: `compareAvailable` false and no compare refs even with the stored switch on; the Settings overview skips `SettingsPage.EXPERIMENTAL`. Stable keeps both (a later graduation note says so). `DevEntryPointsTest.levelsCompareSegment`, `AppNavTest` (dev opens About, Experimental only in stable), `LevelsCompareTest` and `ScreenshotTest.compare` stable only. README dev section and AGENTS.md's dev-feature list follow. | +| (this commit) | AUD-12 · Dev: `compareAvailable` false and no compare refs even with the stored switch on; the Settings overview skips `SettingsPage.EXPERIMENTAL`. Stable keeps both (a later graduation note says so). `DevEntryPointsTest.levelsCompareSegment`, `AppNavTest` (dev opens About, Experimental only in stable), `LevelsCompareTest` and `ScreenshotTest.compare` stable only. README dev-feature list follows. | | (this commit) | AUD-13 · Dev Levels overview: only the range row (stays on Logged + plan) and the charts; the mode row and the figure tiles stay on the detail screen; the jump bar keeps Back to now. `DevEntryPointsTest.levelsOverviewIsAGlance` (mode row and "STEADY AVG" on the overview only in stable), `levelsDetailKeepsModeAndFigures` (both). `LevelsScreenTest.jumpBarScrollsToChartsAndTitleOpensDetail` scrolls to the selected chip (the opened chart can push it out of the composed list). | | (this commit) | Audit of the diffs since v0.5.0-dev.7 (AUD-11…13): nothing found beyond the test fix in AUD-13. Release v0.5.0-dev.8 (tag) · versionCode 12; notes `docs/releases/v0.5.0-dev.8.md`; gate green; on the emulator with the dev release APK: a sideways slide reads Testosterone with the nearest-log panel, no mode row or figure tiles on the overview, Settings ends with About (no Experimental). | -| (this commit) | DEV-1 · `app/src/androidTest/.../SmokeTest`: starts `MainActivity` with seeded data (Test C daily, a BP reading, a draw) and opens the Log menu sheets, a dose sheet, (dev) Symptoms, Bloodwork › Import results, every tab, a Levels detail, (dev) the Bloodwork row and a marker sheet, and every Settings page. Deps: androidx.test runner and rules 1.7.0, espresso-core 3.7.0, Compose ui-test (androidTest). Green on the API 36 emulator in both flavors (15 s and 11 s); with the old `ANNOTATION` regex put back it fails with the PatternSyntaxException. AGENTS.md commands and the before-a-tag rule name it. Not in CI (no emulator there yet). Also: the dev restore question names the local day and counts in the singular (e65f8ce, `RestoreTextTest`). | +| (this commit) | DEV-1 · `app/src/androidTest/.../SmokeTest`: starts `MainActivity` with seeded data (Test C daily, a BP reading, a draw) and opens the Log menu sheets, a dose sheet, (dev) Symptoms, Bloodwork › Import results, every tab, a Levels detail, (dev) the Bloodwork row and a marker sheet, and every Settings page. Deps: androidx.test runner and rules 1.7.0, espresso-core 3.7.0, Compose ui-test (androidTest). Green on the API 36 emulator in both flavors (15 s and 11 s); with the old `ANNOTATION` regex put back it fails with the PatternSyntaxException. The before-a-tag rule names it. Not in CI (no emulator there yet). Also: the dev restore question names the local day and counts in the singular (e65f8ce, `RestoreTextTest`). | | (this commit) | DEV-2 · `TimePickDialog`: dev dial uses `DisplayFormat.current.use24Hour` (AM/PM with the 12-hour clock); stable stays 24-hour. `TimePickerClockTest` (both flavors); `EarlierTimeSheetTest` pins the 24-hour clock. | | (this commit) | DEV-3 · Dev: the item editor's TOTAL reads "40 mg/day" held together (`totalFigure`); the Plan card's DAYS reads "5 days" for three or more weekdays (`daysFigure`; the editor lists them); the Symptoms mood row is two even rows of five 48 dp chips. `FigureTextTest`, `DevEntryPointsTest.symptomSheetWithoutAdvice` (6 and 10 share a row). Screenshots left for the screenshot pass. | | (this commit) | DEV-4 checked: `TrendAxis.yRange` already widens to the band; the band filled the plot because the lab range (193–836) spans nearly all of it, and the top label is the last round tick (750). Nothing to change. DEV-5 · `levelText`: dev drops the decimal from 100 up in the Levels reading, the chart's content description and the NOW figure ("1214 ng/dL"); stable keeps one decimal. `LevelTextTest`. | @@ -575,7 +574,7 @@ Only the owner can verify these. Each default holds until he answers. | 2026-09-28 | Audit v0.5.0-dev.4, SITE-4: the same on Today's Log all when one card has two pending rows of one compound (two plan items of one injectable in one part of the day): the first row records the site, the second none at the same or a later time, so the suggestion stops. Logging the sited row last would rest on millisecond ties. | Open: low; only with two items of one injectable in one part of the day. If it happens, Log all records the site on that compound's last row (and the rows show it there) | | 2026-09-28 | Audit v0.5.0-dev.4, SITE-4: a Missed row checked after today's row of the same compound records the site it then shows (the next one) at its planned, earlier time, so in time order today's pin stays the latest and the rotation can suggest the late pin's site again (Mon L delt; Wed: today → R delt, then Tuesday's missed row → L delt at Tuesday; Thursday suggests L delt). Checking the Missed card first (it is on top) keeps the order. Ordering by time taken stays right for backfills. | Open: low; owner check 49 (§5), default: keep | | 2026-09-28 | Audit v0.5.0-dev.4, SITE-6: a past dose's site cannot be changed or cleared. Journal's `EditLogDialog` has no Site row (it keeps the site) and the Log dose sheet reaches only today's doses, so a wrong one-tap site from yesterday stays in the history; picking the right site for the next pin fixes the suggestion, not the record. | Fixed (SIM-15): Journal opens the dose sheet's edit mode, with the Site row | -| 2026-09-28 | Audit v0.5.0-dev.4, docs: README and the planned dev.4 note did not say what records no site and stops the suggestion (the widget, a past day in the calendar, clearing the site), the first thing the owner would wonder about when the suffix disappears. AGENTS.md named `firstDoses` for every one-tap, though Today does not use it. | Fixed (53de706): README sentence, the Releases line asks the dev.4 note for it, AGENTS.md says where each one-tap puts the site | +| 2026-09-28 | Audit v0.5.0-dev.4, docs: README and the planned dev.4 note did not say what records no site and stops the suggestion (the widget, a past day in the calendar, clearing the site), the first thing the owner would wonder about when the suffix disappears. The docs named `firstDoses` for every one-tap, though Today does not use it. | Fixed (53de706): README sentence, the Releases line asks the dev.4 note for it, the docs say where each one-tap puts the site | | 2026-09-28 | Audit v0.5.0-dev.4 review, no change needed: no Room, backup, manifest or R8-relevant change since dev.3 (`MigrationTest` opens real v2 and v3 databases; `DoseLogSiteCompatTest`, `TrackerRepositoryTest` and `JournalSiteTest` keep sites through backups and edits, and stable shows none of a restored dev backup's sites); stable paths are value switches or early returns (`withSites`; no `EXTRA_SITES` without sites, so stable's Taken records none; the Journal suffix), pinned by `ReminderSiteTest` and `DevEntryPointsTest`; time zones: the rotation compares instants only and every label goes through `Formats`; a stale Taken never overwrites a log (`logOccurrenceIfAbsent`), snooze recomputes, and notifications posted before the update carry no sites and record none; a Journal edit from Taken to Skipped keeps the site on the skipped log, which rotation and every line ignore (switching back restores it). Dev screenshots (`shots/audit-sites`: Today rows and Journal lines, light and dark) read correctly; the suffix wraps like the other separators ("pin 5/7 ·" / "R delt", POL-2). | Closed | | 2026-09-28 | Dev build audit at 618f455 (v0.5.0-dev.4), three lenses: a new user, correctness of the domain and import code, simplicity against the principles. 26 findings; duplicates merged into the rows below. | Closed: new Open list AUD-1…13 (§3) | | 2026-09-28 | Opening the Log dose sheet saves an unchanged per-week dose as adjusted: the field starts at `formatNumber(plan, 4)` (35.7143 for 250 mg/week), the diff reads "+0 mg vs plan", and Today, Journal and reports add "(plan 35.71 mg)"; Journal's `EditLogDialog` does the same on a time-only edit. | Open: AUD-1 | diff --git a/docs/PLAY.md b/docs/PLAY.md index c724266..14c78ad 100644 --- a/docs/PLAY.md +++ b/docs/PLAY.md @@ -72,7 +72,7 @@ Never use in listing text, screenshots or release notes: cycle, blast, cruise, P ## 5. Each release 1. Bump `versionCode` (Play rejects a code it has seen) and `versionName`, add `docs/releases/v.md`. -2. Before the tag (AGENTS.md): `connectedFossDebugAndroidTest connectedPlayDebugAndroidTest` on an emulator, the release build on a device, `adb logcat -b crash` empty. +2. Before the tag: `connectedFossDebugAndroidTest connectedPlayDebugAndroidTest` on an emulator, the release build on a device, `adb logcat -b crash` empty. 3. Push the `v*` tag. The `release` job publishes the foss APK on GitHub and uploads the signed Play bundle as the `steroidtracker-play-bundle` workflow artifact. 4. Upload that `.aab` in Play Console to the testing track, with release notes in the Play wording (the GitHub notes may name foss-only presets; rewrite them). 5. Licence-tester checks on a real device: buy, pending purchase (slow test card), refund (Pro goes away on the next start), Restore purchase.