A private Android period tracker that learns your cycle. Kotlin, Jetpack Compose, offline-first.
Go to file
null 4456f351ac feat: the privacy promise appears in Settings, from one string
§4 requires the promise in onboarding, in Settings, and on a public privacy
page. It was made once, during onboarding, before the user had entered a single
date — which makes it a marketing line. Repeated above the controls that act on
that data, it is a statement somebody can hold the product to.

One copy, in strings.xml, read by both screens. A second literal is how two
versions of a promise come to exist, which is the failure DOC_TRUST_MAP.md
exists to prevent, here in code rather than in prose.

There is NO Privacy Policy row. §4 wants one and no hosted page exists, and a
policy link that 404s is worse than no link — which is also the convention
SettingsScreen already states: a row for something unbuilt is absent, not
disabled. The issue's verify line allows exactly this.

Three tests, and the second is the one that matters. The promise must say we
never SELL the data, and must NOT have been strengthened into claims the app
cannot keep — no third party, never shared, end-to-end — because Play Billing
and an ad SDK eventually will process something, and a promise the
implementation cannot keep is worse than a narrower one that holds. The third
scans Kotlin for a re-introduced literal, with comments stripped first per
GUARDS.md §2, or the KDoc explaining the rule would fail it.

Proved: replacing the resource lookup with the literal fails exactly one test.

## Two defects found on the way, both pre-existing

**No Robolectric test in :app could read a string resource.** core/database and
core/data have carried unitTests.isIncludeAndroidResources since they were
written; app never did. So the module owning almost all of the user-facing copy
was the one module whose copy could not be tested, and every getString() threw
NotFoundException with an id that had resolved perfectly well.

**checkPermissions read manifests that do not ship.** Turning the above on made
AGP write merged_manifest/debugUnitTest/, the guard walked the whole tree, and
the build failed on REORDER_TASKS — a test-runner permission no user ever sees.
The tempting fix is to allowlist it, which would then permit it in the real
manifest too and quietly undo the guard. It now reads only debug and release,
and refuses to pass unless it read BOTH: checking debug while release went
unread is the failure that matters, since the Play listing and the Data Safety
form describe the release manifest.

That is strictly stricter than before, and proved twice — a forbidden permission
in the app manifest still fails it, and a missing release manifest now fails it
where it used to pass.

GUARDS.md §8 gains a third prove-guard edge, found while proving the above: a
FAIL_PATTERN matching nothing gives the same "caught it, and only it" verdict as
one matching exactly once, because the script only refuses on more than one. The
empty "what failed" block is the tell.

closes #37

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 22:00:06 -05:00
.githooks fix: the pre-commit hook refused every deletion-only commit 2026-08-18 15:00:03 -05:00
app feat: the privacy promise appears in Settings, from one string 2026-08-19 22:00:06 -05:00
core feat: the status bar shows this app's mark, not a framework glyph 2026-08-19 21:51:54 -05:00
docs feat: the privacy promise appears in Settings, from one string 2026-08-19 22:00:06 -05:00
domain feat: guard §45's logging rules, and stop the leak that needed no log call 2026-08-18 22:01:33 -05:00
gradle chore: hiltViewModel comes from the package that still supports it 2026-08-19 21:52:10 -05:00
scripts feat: Room database for cycle history, and the schema guard that actually works 2026-08-18 02:32:07 -05:00
.gitignore chore: ignore .kotlin/, the compiler's session directory 2026-08-18 14:57:07 -05:00
README.md feat: app lock, with no way to reset a forgotten PIN 2026-08-19 04:02:47 -05:00
build.gradle.kts feat: the privacy promise appears in Settings, from one string 2026-08-19 22:00:06 -05:00
gradle.properties chore: adopt the project template and add the Kotlin/Compose skeleton 2026-08-18 02:16:47 -05:00
gradlew chore: adopt the project template and add the Kotlin/Compose skeleton 2026-08-18 02:16:47 -05:00
gradlew.bat chore: adopt the project template and add the Kotlin/Compose skeleton 2026-08-18 02:16:47 -05:00
settings.gradle.kts feat: app lock, with no way to reset a forgotten PIN 2026-08-19 04:02:47 -05:00

README.md

Privacy: Period Tracker

Status: Draft
Owner: _null
Last reviewed: 2026-08-18
Governs: README.md as the project-facing overview for Privacy: Period Tracker
Review trigger: The first buildable feature release; any change to the stack, the
                privacy promise, or how the project is built and run

A private Android period tracker that learns your cycle rather than the average person's — and never sells your data.

What it is | Status | Build and run | Repository map | Project docs | Agent notes

{ Android / Google Play | Kotlin + Jetpack Compose | Room, offline-first | Personalized prediction with an honest window | Discreet notifications | No account, no server, no data sale }

The core loop is:

Track → Learn → Predict → Remind → Learn again

What it is

A focused tracker that answers one question well: when is my next period likely to start? It records confirmed period dates, learns the individual's pattern from them, and produces a most-likely date with a window and a confidence label — never a bare date presented as fact. From that it estimates ovulation and the fertile window, and it asks discreetly whether the period started, using both yes and not yet to improve the next forecast.

Two promises hold the product up:

Your period. Better predicted. — once there is enough personal history, the app must not fall back to a generic 28-day cycle. A user recording 34, 35, 36, 34, 35 who is predicted 28 is a core product defect, not a tuning issue.

Your cycle belongs to you. We will never sell period history, fertility information, ovulation estimates, cycle predictions or personally identifiable information — to advertisers, data brokers or anyone else.

The second is held structurally rather than remembered, and the rule was written before the code it constrains: checkModuleBoundaries in the root build.gradle.kts already carries ":core:ads" to emptySet(), so when the ads module arrives in Batch 07 it may declare no project dependency at all — stricter than "nothing from the cycle database or the prediction domain". It is not in settings.gradle.kts yet, so today the rule matches no module, which is why the guard is proved against injected violations rather than trusted.

What it deliberately is not — no community, no pregnancy mode, no chatbot, no article feed, no symptom encyclopedia, and not contraception — is in docs/planning/PROJECT_PLAN.md.

Status

Five of the eight batches are done. Foundation, prediction engine, core UX, fertility and notifications have landed; privacy and security (Batch 06) is the next work. Every row below cites the file or test that proves it.

Surface Status Evidence
Documentation tree and tracker convention Adopted this tree; milestones and issues in the tracker
Gradle / Kotlin / Compose project Builds ./gradlew assembleDebug and assembleRelease both pass
Material 3 theme and tokens Built core/designsystem
Four-tab navigation shell Built app/src/main/kotlin/dev/privacyllc/period/navigation/PeriodApp.kt — Today, Calendar, Insights, Settings, each wired to its real screen
Cycle model and interval derivation Built domain/cycle (13 tests); domain/prediction/src/main/kotlin/dev/privacyllc/period/domain/prediction/IntervalAnalysis.kt (12 tests)
Prediction engine Built — personalized PersonalPredictionEngine, wired in app/src/main/kotlin/dev/privacyllc/period/di/DataModule.kt; the 12 acceptance tests from PRODUCT_PLAN §51 run against both engines (PersonalAcceptanceTest, BaselineAcceptanceTest), and EngineComparisonTest scores the personal engine against the retained baseline every build
Room persistence, DataStore Built core/database (schema v1 exported, SchemaTest + scripts/schema-guard.sh), core/datastore, core/data
Onboarding, Today, Calendar, Insights Built app/src/main/kotlin/dev/privacyllc/period/feature/ — onboarding, today, calendar, insights; OnboardingViewModelTest, TodayViewModelTest, TodayUiStateTest
Fertility window and ovulation estimate Built domain/prediction/src/main/kotlin/dev/privacyllc/period/domain/prediction/FertilityEstimate.kt, 11 tests; shown on Today and Calendar
Discreet notifications Built core/notifications (24 JVM tests), instrumented NotificationPrivacyTest
App lock (PIN + fingerprint) Built core/security (Keystore-backed verifier, lockout policy), app/src/main/kotlin/dev/privacyllc/period/lock gate; 28 JVM tests plus KeystoreVerifierTest run on PeriodMinSdk26 and PeriodQA
Export, monetization Not built Batches 0607
QA Round 3 run, partial docs/qa/ClaudeReport.md — partial at 0451fbe; TalkBack, text scaling, minSdk and a real locked screen still unreached

BaselinePredictionEngine is still in the tree, but it stopped being the product in Batch 02. DataModule provides PersonalPredictionEngine — the recency-weighted, trend-aware, "not yet"-conditioned engine PRODUCT_PLAN §12 specifies — and the baseline is now the control: EngineComparisonTest scores both over the §51 fixtures every build, so "the new engine is better" is a number rather than an opinion.

Build and run

Prerequisites: a JDK 21 and the Android SDK with platform 37 and build-tools 37.0.0 installed, ANDROID_HOME set.

git config core.hooksPath .githooks   # per clone, every clone — see below

./gradlew test                        # JVM suites, no emulator, ~1s
./gradlew assembleDebug               # app/build/outputs/apk/debug/
./gradlew assembleRelease             # minified, unsigned

compileSdk is 37 because the current AndroidX libraries require it; targetSdk is 36, Google Play's floor for new apps from 2026-08-31. They are deliberately different — raising targetSdk opts the app into runtime behaviour changes and is a tested decision, not a build fix.

git config core.hooksPath .githooks is per clone. Without it the hooks are not installed and fail silently, which is the failure mode they exist to prevent. post-commit pushes; see docs/architecture/githooks/README.md.

Repository map

app/                     application module, MainActivity, navigation shell, DI
core/designsystem/       Material 3 theme and colour tokens
core/database/           Room entities, DAOs, converters, the exported schema
core/datastore/          user preferences that are not health history
core/data/               CycleRepository — the only module that touches a DAO
core/notifications/      reminder copy, privacy modes, WorkManager scheduling
domain/cycle/            pure Kotlin — period records, cycle derivation
domain/prediction/       pure Kotlin — the forecast, window and confidence
scripts/                 six from the template, plus schema-guard.sh, our own
.githooks/               pre-commit, commit-msg, post-commit
docs/                    product, architecture, design, brand, QA, security, history

domain/* are kotlin("jvm") modules and cannot see the Android SDK. That is on purpose: the prediction engine is the product, so it needs the most tests, and tests that need an emulator are tests that do not get run.

Project docs

Detailed procedures belong in docs; open work belongs in the tracker, not in this README.

Doc Purpose
docs/DOC_TRUST_MAP.md Which document owns which answer, and which source wins when records disagree. Read this first.
docs/planning/PRODUCT_PLAN.md The full V1 specification — prediction requirements, screens, copy, compliance
docs/planning/PROJECT_PLAN.md The short vision, the stack and its reasons, and what this is deliberately not
docs/WORK_CYCLE.md What happens at the end of a piece of work
docs/architecture/README.md Modules, boundaries, data shapes, migrations
docs/design/README.md Tone, and the four rules that settle design arguments
docs/security/SECURITY.md Threat model, the advertising boundary, logging rules
docs/TOOLS.md Which script to run, and which ones can stop you

Where the tracker is

Milestones are batches, issues are deliverables, and severity labels are exactly P0, P1, P2 and release-blocker. Credentials come from the environment, never from this repository:

set -a; . ~/.openclaw/docker-registry.env; set +a
python3 scripts/forgejo-issue.py list

Two traps that cost an hour each otherwise: Cloudflare 1010-blocks clients that do not look like a browser or curl, so every request needs User-Agent: curl/8.5.0forgejo-issue.py sends it and anything new must too. And /issues returns pull requests unless type=issues is passed.

Agent notes

  • Product truth comes from the code and the tracker before prose.
  • Do not keep a work list in this README or anywhere else in docs/.
  • Finish with docs/WORK_CYCLE.md, every time: close what you finished with the evidence, close the milestone if the batch landed, update the documents the change triggered in the same commit, push, log the entry, then reconcile and write the summary and next action.
  • Nothing on privacyllc.dev writes itself except the tracker counts and the pushed docs.
  • Do not claim a feature is built unless you can cite the file, test or screenshot that proves it. The status table above is the standard.
  • Verify current stable versions before changing the toolchain. The ones in gradle/libs.versions.toml were checked against the official sources on 2026-08-18; PRODUCT_PLAN.md asks for that check rather than for its own numbers to be trusted.
  • Never put a cycle date, a prediction or a fertility state into a log, an analytics event, or an ad request.