Privacy-Period-Tracker/docs/architecture
null 96dd878ac5 chore: adopt the project template and add the Kotlin/Compose skeleton
Period was a bare directory holding one 2,527-line specification, with no git
repository, no tracker and no documentation convention. This is the adoption
from Projects/Template/START-HERE-New-Project.md, plus a project that compiles
so the hooks and future guards have something real to run against.

Documents. scaffold.sh created 19 paths, 0 skipped. The specification moved to
docs/planning/PRODUCT_PLAN.md unchanged in substance, with a status header; the
capitalised Docs/ is gone. Every scaffolded document was filled in for Period.
docs/OPERATIONS.md deleted — an offline app is not a deployed service.
DOC_TRUST_MAP.md written last, describing what is actually here, including what
this project deliberately does not have.

Code. Four Gradle modules. domain/cycle and domain/prediction are kotlin("jvm")
and cannot see the Android SDK, so the engine is testable without an emulator —
17 tests pass, 12 of them the acceptance cases from PRODUCT_PLAN.md §51.
BaselinePredictionEngine is a robust-median prototype and explicitly not the
product; it exists so Batch 02's replacement can be shown to be better rather
than merely different.

Versions verified against their official sources today rather than inherited
from the specification's own numbers, which that document asks for: Kotlin
2.4.10, AGP 9.3.1, Gradle 9.7.0, Compose BOM 2026.08.00, Room 2.8.4, Hilt
2.60.1. AGP 9 ships Kotlin built in, so org.jetbrains.kotlin.android is no
longer applied. compileSdk is 37 because current AndroidX requires it; targetSdk
stays 36, Play's floor from 2026-08-31, and the difference is deliberate.

Six scripts taken into scripts/; the rest declined and named in docs/TOOLS.md.
Three hooks in .githooks/, with pre-commit adapted to Gradle.

closes #1
closes #2
2026-08-18 02:16:47 -05:00
..
githooks chore: adopt the project template and add the Kotlin/Compose skeleton 2026-08-18 02:16:47 -05:00
GUARDS.md chore: adopt the project template and add the Kotlin/Compose skeleton 2026-08-18 02:16:47 -05:00
README.md chore: adopt the project template and add the Kotlin/Compose skeleton 2026-08-18 02:16:47 -05:00

README.md

Architecture

Status: Current
Owner: _null
Last reviewed: 2026-08-18
Governs: docs/architecture/**, the Gradle module graph, and the data shapes that
         outlive a function
Review trigger: Any new Gradle module, any change to a module boundary, any change
                to a Room entity or a DAO, any new Room migration, any dependency
                added to a domain/* module

The shape

Compose UI  (app, feature/*)
    ↓
ViewModel — immutable StateFlow of screen state
    ↓
Use case / prediction engine  (domain/*)
    ↓
Repository  (core/data)
    ↓
Room + DataStore  (core/database, core/datastore)

Unidirectional: state flows down as an immutable UiState, events flow up as function calls. Nothing below the ViewModel knows Compose exists.

Modules

Four today. core/database and core/datastore are Batch 01 issues #3 and #4 and do not exist yet — a module created before it has contents is a place for things to be put by accident. The wider layout sketched in ../planning/PRODUCT_PLAN.md §9 arrives the same way, with the batch that needs it.

Module Plugin Owns May depend on
app Android application MainActivity, the four-tab navigation shell, DI wiring everything below
core/designsystem Android library Material 3 theme, colour and type tokens nothing in this project
domain/cycle Kotlin JVM PeriodRecord, SpottingRecord, CycleRecord and the rules over them nothing
domain/prediction Kotlin JVM the forecast, the window, confidence, NotYetObservation domain/cycle

Planned, with the issue that brings each one:

Module Plugin Owns May depend on Issue
core/database Android library Room entities, DAOs, migrations domain/cycle #3
core/datastore Android library UserPreferences domain/cycle #4
core/data Android library the repositories — the only things that touch a DAO core/database, core/datastore, domain/cycle #5
core/ads Android library the AdProvider implementation neither core/database nor domain/* Batch 07

Why domain/* is kotlin("jvm") and not an Android library

PRODUCT_PLAN.md §57.10 asks for the prediction engine to be unit-testable without Android. A convention saying "do not import android.* here" is a convention somebody breaks at 11pm; a module that cannot see the Android SDK at all is a compile error instead.

It buys the thing §50 depends on: the acceptance tests in §51 — stable 35-day user, variable user, 45-day outlier, "not yet" — run on the JVM in under a second, so they run on every commit rather than on an emulator when someone remembers.

The boundary that is not negotiable

The advertising subsystem must never receive menstrual dates, cycle length, period duration, fertility status, ovulation estimates, prediction confidence, prediction history, spotting records, or any other health-derived attribute. — PRODUCT_PLAN.md §34

Expressed structurally rather than as a rule people remember: when core/ads exists it will declare no dependency on core/database or domain/*, and a Gradle check enforces the whole table above by enumerating each module's allowed dependencies. Ads reach the UI through an AdProvider interface owned by app.

Per GUARDS.md §1, that check is proved to fail — a deliberate forbidden dependency added, the guard watched going red, the file restored — before it is treated as evidence. scripts/prove-guard.sh performs it.

Data shapes

Defined in domain/cycle as plain Kotlin, mirrored by Room entities in core/database. The full field lists are PRODUCT_PLAN.md §10; what matters here is why each exists and what must not happen to it.

Type Why it exists The rule that goes with it
PeriodRecord a confirmed period, with its source and whether it is confirmed a record's source is kept; edits are recorded, never silent
SpottingRecord spotting, tracked separately must not start a cycle or reset one
CycleRecord derived interval between two confirmed starts derived, never stored as truth — recomputed from period records
PredictionRecord a snapshot taken before the outcome is known this is what makes accuracy measurable at all; never overwritten in place
NotYetObservation the user said the period had not started by a date a censoring observation — the forecast is re-conditioned on it, not shifted by +1 day
UserPreferences notification privacy, reminder time, lock, theme, ads entitlement lives in DataStore, never in the cycle database

Never secretly modify health history. A gap that looks like a missing entry (§14) produces a question, not a correction. That is an architectural constraint as much as a UX one: nothing in the data layer may write a PeriodRecord the user did not confirm.

Migrations

Room migrations are numbered, tested, and listed in this document — one row per migration, added in the same commit as the migration itself. The template this repository came from records why: a manual's migration table sat six behind, and every reader in between trusted it.

Version What changed Migration test
1 initial schema

Room's exported schemas are committed, so a migration can be tested against the real previous schema rather than a remembered one.

Documents here

  • GUARDS.md — how to write a check that actually checks. Read it before adding a structural test or a probe.

What ships in this folder

Nothing. This project took six scripts from the template into scripts/, and ../TOOLS.md explains why the rest are absent and where the menu is.

Path What it is
scripts/secrets.sh credential shapes in a staged diff — the one that stops a keystore reaching a commit
scripts/doc-claims.sh every file a document names must exist; --covers asks the inverse
scripts/doc-triggers.py which documents a pending change fires, read from the Governs: headers
scripts/commit-mine.sh commits only the paths you name, by pathspec, after the secret scan
scripts/forgejo-issue.py files and closes issues in the tracker convention, with every rule of it as a check
scripts/prove-guard.sh breaks what a guard protects and requires the guard to go red
.githooks/ pre-commit, commit-msg, post-commit — see githooks/README.md

What does not belong here

A note on drift

Architecture docs go stale faster than any other kind, because code changes under them silently. That is what the Review trigger above is for, and why it names a new Gradle module and a new Room migration specifically: those are the two changes here that make this document wrong without touching it.