core/database holds the four entities from PRODUCT_PLAN.md §10 —
period_records, spotting_records, prediction_records, not_yet_observations —
with DAOs returning Flow, epoch-day/epoch-milli converters, and the schema
exported to core/database/schemas and committed.
Three constraints are structural rather than remembered:
- startDate is UNIQUE and inserts ABORT rather than REPLACE. REPLACE would
delete the original row with its createdAt and source; §14 says health
history is never modified silently.
- spotting has its own table, so no query for periods can reach it. §25: it
must never start or reset a cycle.
- a prediction snapshot can be scored but not rewritten — score() sets only
actualStartDate and absoluteErrorDays. A snapshot editable after the fact
can only ever report that the app was right, which would make §16's whole
accuracy feature a lie.
deleteEverything() is one transaction and the only bulk delete in the module: a
partial wipe leaves the cycle reconstructible from the tables the user asked to
be rid of.
14 tests, on the JVM under Robolectric — no emulator.
THE SCHEMA GUARD, AND WHY IT IS A SCRIPT
SchemaTest was written as a drift guard and proved not to be one. Room
regenerates the schema export during compilation, so adding a column to
PeriodRecordEntity without bumping VERSION leaves the suite green while the
committed schema quietly changes underneath it. That was not reasoned about, it
was run: the column was added, 1.json gained it, and every test passed. On a
device that is "Room cannot verify the data integrity" — a crash on update,
after shipping.
scripts/schema-guard.sh asks git instead, which Room cannot overwrite. Proved
both ways before being trusted: green on a clean tree, exit 1 on the injected
drift. It runs in pre-commit when an entity or the schema directory is staged,
and the hook treats its exit 2 as a refusal.
SchemaTest keeps its four tests and now documents what it does not catch.
Room's own MigrationTestHelper is not used: every constructor needs an
Instrumentation and schema assets, and AGP 9's library source-set DSL throws
DefaultAndroidLibrarySourceSet_Decorated cannot be cast to
AndroidLibrarySourceSet when you add an asset directory. Recorded so the next
person does not spend the afternoon on it.
Docs updated in this commit, as their triggers required: the migration table
now has its version 1 row and the trap that makes such tables go stale, TOOLS
explains the seventh script, and the hooks README lists the new guard.
closes #3
|
||
|---|---|---|
| .githooks | ||
| app | ||
| core | ||
| docs | ||
| domain | ||
| gradle | ||
| scripts | ||
| .gitignore | ||
| README.md | ||
| build.gradle.kts | ||
| gradle.properties | ||
| gradlew | ||
| gradlew.bat | ||
| settings.gradle.kts | ||
README.md
Period
Status: Draft
Owner: _null
Last reviewed: 2026-08-18
Governs: README.md as the project-facing overview for Period
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 enforced structurally, not remembered: health data cannot reach the advertising subsystem, because the module that will hold ad code declares no dependency on the cycle database or the prediction domain, and a guard proves it.
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
Skeleton. There is no usable app yet, and this section says so rather than listing features that do not exist.
| 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, placeholder screens | app/src/main/kotlin/dev/privacyllc/period/navigation/PeriodApp.kt |
| Cycle model and interval derivation | Built | domain/cycle, 5 tests |
| Prediction engine | Baseline only | domain/prediction, 12 acceptance tests from PRODUCT_PLAN §51 |
| Room persistence, DataStore | Not built | Batch 01 |
| Onboarding, Today, Calendar, Insights | Not built | Batch 03 |
| Fertility, notifications, privacy features, monetization | Not built | Batches 04–07 |
| QA | No round run | docs/qa/ClaudeReport.md |
BaselinePredictionEngine is a robust-median prototype and is not the
product. PRODUCT_PLAN §11 names a plain average as an acceptable prototype and
an unacceptable final engine; Batch 02 replaces it with the recency-weighted,
trend-aware, "not yet"-conditioned engine §12 specifies. The acceptance tests
exist now so that replacement can be shown to be better rather than merely
different.
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
core/designsystem/ Material 3 theme and colour tokens
domain/cycle/ pure Kotlin — period records, cycle derivation
domain/prediction/ pure Kotlin — the forecast, window and confidence
scripts/ the six template scripts this project adopted
.githooks/ pre-commit, commit-msg, post-commit
docs/ product, architecture, design, 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.