The export was a one-way door. A user changing phone started the prediction model from zero, which made "your history is yours" a smaller promise than it sounded — and the archive says, in its own prose, "keep this file, a future version of the app will be able to read it back". Nothing checked that was true. `ExportReader` mirrors `ExportDocument.render` in the same module, over a hand-written strict JSON reader. Strict is the point: it refuses duplicate keys, trailing content, non-integer numbers and unsupported escapes, because a parser that quietly accepts a trailing comma is a parser that will one day accept somebody else's file and import it as a cycle history. A missing magic string is `NotOurFile`; a version above this build's is `NewerFormat` rather than a half-read; unknown *keys* are ignored, which is what lets the format grow; an unknown `source` becomes `IMPORTED` rather than a guess about the user. The write is `CycleRepository.importHistory`, not a loop over `confirmPeriodStart`, because that would score the standing forecast against history the app never predicted — and the backfill guard does not catch all of it, since a file exported this morning carries this morning's period. So the scoring is not dodged by accident of date; it is not reached. The engine runs exactly once, at the end, over the whole history. Merge keeps what is on this phone untouched (§14 — a file does not get to rewrite a record she made here), and replace goes through the new `PeriodDatabase.replaceEverything`, which empties the tables inside the same transaction as the writes: a failure between the wipe and the inserts would leave her with neither her own history nor the file's. Delete My Data still calls `deleteEverything` and still gets its VACUUM — replacing a history is not erasing one. The screen asks the one question the app must not answer for her, before the picker rather than after, because the screen that asked is the screen a re-lock destroys. Add is the filled button and needs no confirmation; replace is confirmed by a dialog that names the deletion and points back at the safe option. The result lands in the banner above the tabs, beside the export's, as a live region — it arrives with no focus change. The archive cannot touch the app lock. It carries `biometricUnlock` and the importer does not apply it: there is no PIN in the file and there cannot be. Also raises the lock ViewModel test's hang budget to three minutes. 60s passed alone and failed when four modules ran in one invocation — three real PBKDF2 derivations at 210k iterations. The budget is for catching a stuck coroutine, not a slow one. Verified: `:core:export` round trip against the committed golden file (17); `CycleRepositoryImportTest` — one recalculation for a four-record archive, nothing scored, one standing forecast, and a failed replace leaving the history intact (prove-guard: remove `withTransaction` from `replaceEverything`, exactly one red); `DataImporterTest`, `ImportScreenTest`, `ImportControllerTest`, `ImportCopyTest`. On PeriodQA: Settings → Restore → Add → Downloads → records-2026-08-19.json gives "Restored. 4 periods and 2 spotting days added.", Insights then shows three cycles and accuracy still unscored, and the same file a second time gives "Everything in that file was already recorded here, so nothing changed." closes #58 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|---|---|---|
| .githooks | ||
| app | ||
| core | ||
| docs | ||
| domain | ||
| gradle | ||
| scripts | ||
| .gitignore | ||
| README.md | ||
| build.gradle.kts | ||
| gradle.properties | ||
| gradlew | ||
| gradlew.bat | ||
| settings.gradle.kts | ||
README.md
Privacy: Period Tracker
Status: Draft
Owner: _null
Last reviewed: 2026-08-20
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
Foundation through Batch 06 are closed, Batch 08 polish is complete, and monetization (Batch 07) remains unbuilt. 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 |
| Discreet launcher alias | Built | app/src/main/AndroidManifest.xml aliases; LauncherAliasSwitcherTest, PrivacyViewModelTest; device checked on emulator-5554 |
| Export My Data | Built | core/export (format pinned byte-for-byte against core/export/src/test/resources/golden-v1.json), written through the Storage Access Framework by app/src/main/kotlin/dev/privacyllc/period/feature/export |
| Monetization | Not built | Batch 07 |
| 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.0 — forgejo-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.tomlwere 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.