Privacy-Period-Tracker/README.md

184 lines
10 KiB
Markdown
Raw Permalink Normal View History

chore: rename the project to Privacy: Period Tracker "Period" was always a working name — PRODUCT_PLAN.md §55 said so. The real identity arrived with the brand guide and artwork, and this makes the project call itself by it. The repository is renamed in place on Forgejo (null/Period -> null/Privacy-Period-Tracker), which keeps all 30 commits, all 27 issues, all 8 milestones and the four severity labels — verified by counting them on both sides rather than assuming a rename is lossless. The git remote follows in the same breath, so this commit's automatic push is what proves the new URL works. WHAT DELIBERATELY DID NOT MOVE "Period" is the product name AND the central domain word, and a mass rename would have turned the data model into nonsense. PeriodRecord, PeriodWriteResult, confirmPeriodStart, the period_records table and the button that says "Started period" all describe a menstrual period rather than a product, and all 239 references to them are untouched. So are the Kotlin package and applicationId. dev.privacyllc.period already reads correctly under the new name — privacyllc is the company, period is the app — and changing it would rewrite 68 files, rename the Room schema directory and break the hardcoded path in schema-guard.sh for no gain. period.db stays for a sharper reason: renaming a database file orphans the data on every device that already has it. Sixteen files changed, and the diff is small on purpose. THE NAME HAS A SPACE AND THE ARTWORK DOES NOT Canonical is "Privacy: Period Tracker". The supplied wordmark sets it without one, so logo.webp and banner.webp now disagree with every document and with the app itself. BRAND_GUIDE.md §10 is updated to the space form and says plainly that the drawn mark has not caught up. Filed separately rather than papered over, because closing that gap needs an artist and not a rename.
2026-08-18 15:54:34 -05:00
# Privacy: Period Tracker
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
```text
Status: Draft
Owner: _null
Last reviewed: 2026-08-20
docs: correct 57 claims the code and tracker disagreed with Every document in the tree audited against the source, the tracker and git history, each finding then given to a second reader tasked with refuting it. 74 raised, 12 refuted, 57 applied. No code changed. THE README DESCRIBED A SKELETON Its Status table — the one place a claim about what is built is allowed to live — still read "there is no usable app yet", with Not built against Room, the four core screens, fertility and notifications, and No round run against QA. Five batches had shipped and three QA rounds had run. TWO DOCUMENTS WERE SILENTLY NEVER FIRING architecture/README.md and design/README.md wrote Governs: as prose ("the Gradle module graph", "the design tokens in core/designsystem"). Neither contains a path token, so doc-triggers.py reduced them to globs matching nothing, and one real glob apiece made them look path-governing rather than subject-governing — the state the script's own header calls invisible. Editing a Room entity never fired the document owning the migration table. Both now fire, proved by running the script. SECURITY.md CLAIMED FOUR UNBUILT PROTECTIONS App lock listed among what works offline; biometric/PIN gating described as protecting app launch; the incognito launcher as existing; Play Billing in the third parties table without the "not yet integrated" marker its neighbours carry. All are Batch 06/07 work. The advertising boundary was overstated in SECURITY.md and the README alike: both said the ads module declares no dependency and a guard proves it. There is no ads module. The pre-declared ":core:ads" to emptySet() rule is stricter than the sentence it replaced and matches nothing until Batch 07, which is why the guard is proved by injection rather than trusted. SMALLER, EACH A REAL TRAP WORK_CYCLE.md pointed at docs/architecture/scripts/forgejo-issue.py, a template path absent here — missed by doc-claims.sh, which reads backticked prose and not fenced blocks. ClaudeReport.md's Round notes said "No rounds yet" after three rounds because ClaudeQAPlan.md's after-a-round list never named that section; the playbook is fixed first. The instrumented-test count was eight in three places and is four. HISTORY.md said the repository had no code and that nothing had been tried and dropped, when three approaches had. DELIBERATELY UNCHANGED ClaudeReport.md's last verified build SHA stays at 0451fbe — no round has run since, and moving it would claim a verification nobody performed. Every DEVELOPMENT_LOG entry stays as written. Guards: ./gradlew check, schema-guard.sh, doc-claims.sh (235 claimed paths, all present), doc-triggers.py, and a link sweep over 21 markdown files. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 16:33:14 -05:00
Governs: README.md as the project-facing overview for Privacy: Period Tracker
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
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](#what-it-is) | [Status](#status) | [Build and run](#build-and-run) |
[Repository map](#repository-map) | [Project docs](#project-docs) |
[Agent notes](#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:
```text
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.
docs: correct 57 claims the code and tracker disagreed with Every document in the tree audited against the source, the tracker and git history, each finding then given to a second reader tasked with refuting it. 74 raised, 12 refuted, 57 applied. No code changed. THE README DESCRIBED A SKELETON Its Status table — the one place a claim about what is built is allowed to live — still read "there is no usable app yet", with Not built against Room, the four core screens, fertility and notifications, and No round run against QA. Five batches had shipped and three QA rounds had run. TWO DOCUMENTS WERE SILENTLY NEVER FIRING architecture/README.md and design/README.md wrote Governs: as prose ("the Gradle module graph", "the design tokens in core/designsystem"). Neither contains a path token, so doc-triggers.py reduced them to globs matching nothing, and one real glob apiece made them look path-governing rather than subject-governing — the state the script's own header calls invisible. Editing a Room entity never fired the document owning the migration table. Both now fire, proved by running the script. SECURITY.md CLAIMED FOUR UNBUILT PROTECTIONS App lock listed among what works offline; biometric/PIN gating described as protecting app launch; the incognito launcher as existing; Play Billing in the third parties table without the "not yet integrated" marker its neighbours carry. All are Batch 06/07 work. The advertising boundary was overstated in SECURITY.md and the README alike: both said the ads module declares no dependency and a guard proves it. There is no ads module. The pre-declared ":core:ads" to emptySet() rule is stricter than the sentence it replaced and matches nothing until Batch 07, which is why the guard is proved by injection rather than trusted. SMALLER, EACH A REAL TRAP WORK_CYCLE.md pointed at docs/architecture/scripts/forgejo-issue.py, a template path absent here — missed by doc-claims.sh, which reads backticked prose and not fenced blocks. ClaudeReport.md's Round notes said "No rounds yet" after three rounds because ClaudeQAPlan.md's after-a-round list never named that section; the playbook is fixed first. The instrumented-test count was eight in three places and is four. HISTORY.md said the repository had no code and that nothing had been tried and dropped, when three approaches had. DELIBERATELY UNCHANGED ClaudeReport.md's last verified build SHA stays at 0451fbe — no round has run since, and moving it would claim a verification nobody performed. Every DEVELOPMENT_LOG entry stays as written. Guards: ./gradlew check, schema-guard.sh, doc-claims.sh (235 claimed paths, all present), doc-triggers.py, and a link sweep over 21 markdown files. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 16:33:14 -05:00
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.
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
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](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.
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
| 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` |
docs: correct 57 claims the code and tracker disagreed with Every document in the tree audited against the source, the tracker and git history, each finding then given to a second reader tasked with refuting it. 74 raised, 12 refuted, 57 applied. No code changed. THE README DESCRIBED A SKELETON Its Status table — the one place a claim about what is built is allowed to live — still read "there is no usable app yet", with Not built against Room, the four core screens, fertility and notifications, and No round run against QA. Five batches had shipped and three QA rounds had run. TWO DOCUMENTS WERE SILENTLY NEVER FIRING architecture/README.md and design/README.md wrote Governs: as prose ("the Gradle module graph", "the design tokens in core/designsystem"). Neither contains a path token, so doc-triggers.py reduced them to globs matching nothing, and one real glob apiece made them look path-governing rather than subject-governing — the state the script's own header calls invisible. Editing a Room entity never fired the document owning the migration table. Both now fire, proved by running the script. SECURITY.md CLAIMED FOUR UNBUILT PROTECTIONS App lock listed among what works offline; biometric/PIN gating described as protecting app launch; the incognito launcher as existing; Play Billing in the third parties table without the "not yet integrated" marker its neighbours carry. All are Batch 06/07 work. The advertising boundary was overstated in SECURITY.md and the README alike: both said the ads module declares no dependency and a guard proves it. There is no ads module. The pre-declared ":core:ads" to emptySet() rule is stricter than the sentence it replaced and matches nothing until Batch 07, which is why the guard is proved by injection rather than trusted. SMALLER, EACH A REAL TRAP WORK_CYCLE.md pointed at docs/architecture/scripts/forgejo-issue.py, a template path absent here — missed by doc-claims.sh, which reads backticked prose and not fenced blocks. ClaudeReport.md's Round notes said "No rounds yet" after three rounds because ClaudeQAPlan.md's after-a-round list never named that section; the playbook is fixed first. The instrumented-test count was eight in three places and is four. HISTORY.md said the repository had no code and that nothing had been tried and dropped, when three approaches had. DELIBERATELY UNCHANGED ClaudeReport.md's last verified build SHA stays at 0451fbe — no round has run since, and moving it would claim a verification nobody performed. Every DEVELOPMENT_LOG entry stays as written. Guards: ./gradlew check, schema-guard.sh, doc-claims.sh (235 claimed paths, all present), doc-triggers.py, and a link sweep over 21 markdown files. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 16:33:14 -05:00
| 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` |
feat: app lock, with no way to reset a forgotten PIN §45 asks for biometric/PIN gating. UserPreferences.biometricLockEnabled has existed since Batch 01 with nothing outside its own module reading it; this wires it, and adds the rest. The recovery question was the reason #34 sat open, and it is decided: there is no recovery. A backdoor into a period tracker's lock would be used by exactly the person the lock exists to stop. Everything below follows from that. ## The gate AppLockGate wraps the whole composition rather than being a screen inside it. Today, Calendar and Insights each start collecting from CycleRepository the moment they compose, so a lock implemented as a nav destination would already have read the history before the user proved anything. content() is invoked only in the unlocked branch. Re-lock on ON_STOP, not ON_PAUSE — pause fires for the shade, quick settings and a permission dialog. Two guards on top: isChangingConfigurations, or rotation and the fontScale-2.0 pass both re-lock; and authInProgress, or an OEM biometric overlay that stops the activity produces a lock that can never be opened. No grace period: SECURITY.md leads with "someone who picks up an unlocked phone", which is the window a grace period covers. The unlock flag lives in a @Singleton, never in saved state. rememberSaveable looks like the obvious home and would restore a background-killed app already unlocked — the single most likely way to meet the lock screen would be the one path that skipped it. ## What is stored is not the PIN mac = HMAC(keystoreKey, 0x01 || salt || PBKDF2-SHA256(pin, salt, 210k)) Two layers because they defend different things. The Keystore MAC is what makes a six-digit PIN safe at all — a million candidates is nothing to an attacker who can compute the hash, and impossible for one who cannot get the key off the device. PBKDF2 underneath is for the day that assumption breaks. 0x01 is a domain-separation tag; the lockout counter is MACed under 0x02. The key omits six builder calls and the KDoc names every one. setUserAuthenti- cationRequired is the important absence: it would bind the key to the device lock, so changing a passcode would destroy it — and under no-recovery that is somebody's whole history gone for an unrelated reason. It would also be a bypass, since SECURITY.md already names "someone who knows the unlock PIN" as an adversary. The biometric key is separate and takes the opposite policy, where invalidation correctly degrades to "use your PIN". ## Wrong PINs cost time, never data Four free attempts, then 30s/1m/2m/5m/15m, capped forever. No attempt limit and no auto-wipe: under no-recovery an auto-wipe would let a partner, a child or a pocket destroy a history while knowing nothing. Both clock bypasses are closed — the wait is the longer of a wall-clock and a monotonic deadline, and a reboot re-applies it in full, detected by elapsedRealtime going backwards. ## Two writes that had to move Tapping "Not yet" on a reminder writes a NotYetObservation. That button is on the phone's own lock screen, reachable by anybody, so the action is now parked in AppLockController and applied only after an unlock — dropped if the session never unlocks. Behaviour is unchanged when the lock is off. The erase behind "Forgot your PIN?" deletes health data, then the Keystore key, then the lock store. Skipping the middle step leaves the user erased AND still locked out; prove-guard mutates that line out and requires exactly one red. ## Found by testing, not by review - A fresh install began in a 15-minute lockout: "no counter yet" and "counter was tampered with" were the same value. They are now distinct. - Setting a PIN locked you out of the session you set it in. Found on the emulator, not in a test. - Kotlin block comments nest, so `domain/*` in a KDoc opens one. Twice. ## Verified 244 JVM tests, 0 skipped. KeystoreVerifierTest runs on PeriodMinSdk26 and PeriodQA — including that PBKDF2WithHmacSHA256 exists at API 26, the one choice here with no margin, and that the key is not auth-bound on either. On device: wrong PIN refused, correct PIN opens, am kill then reopen lands on the lock screen, turning the lock off requires the current PIN, and `adb exec-out screencap` returns mean=0 stddev=0 — FLAG_SECURE is real. androidx.biometric 1.1.0 is the newest stable (1.4.0 is alpha; biometric-ktx never shipped one). It merges USE_BIOMETRIC and USE_FINGERPRINT, which failed checkPermissions until they were allowed on purpose, and it drags fragment to 1.5.1 — pinned to 1.9.0 since MainActivity is now a FragmentActivity. closes #34 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 04:02:47 -05:00
| 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` |
feat: export my data, as one plaintext file the user places "Your cycle belongs to you" was a promise the app could not keep: there was no way to get the data out. ## The format, because it outlives the batch One pretty-printed JSON file. The issue asks for "human-readable" and means it — this is what somebody's archive will be in for years, so it is a contract with eight rules written down beside it, not an implementation detail. Pinned byte-for-byte against a committed golden file, which doubles as the documented example so SECURITY.md links at it rather than keeping a second copy that would drift. A reformat, a reordered key or a changed date rendering all fail in a test rather than in an archive. Dates are ISO calendar dates with no timezone and no conversion, ever. Converters.kt stores a LocalDate as its epoch day precisely so it "cannot carry a timezone by accident", and a zone-aware formatter here would shift every date for users east or west of whoever wrote it — a cycle tracker off by one day is wrong in the way that matters. There is a test that renders the same fixture in UTC, +14 and -12 and requires identical bytes, because that bug would never fire where it was written. ## Plaintext, and that is the decision rather than the default An earlier note said this would be encrypted. It should not be, and SECURITY.md now says why: the export is the copy that makes a lost Keystore key survivable instead of final — the exact condition recorded for ever revisiting database encryption. Putting it behind a passphrase reproduces the failure that decision was taken to avoid: a forgotten secret and an archive nobody, including this app, can open. §45's "prefer encrypted backup/export formats" is scoped to backup, which this is not. ## Only the user's own data, as a compile error :core:export is pure JVM and depends on :domain:cycle alone. Prediction, PredictionAccuracy, FertilityEstimate and CycleRecord live in :domain:prediction and are simply not on its classpath, and kotlin("jvm") keeps android.os.Build off it too — so a forecast or a device fact cannot be added by accident. The key set is asserted with assertEquals rather than contains, so a new field is a failing test rather than a silent addition. Row ids are out because they are monotonic and would disclose how many records the user DELETED. The Play entitlement is out because a purchase one file-edit away from being granted is a purchase that will be. ## No second copy, ever The Storage Access Framework writes straight into the document the user picked. The alternative — write to cacheDir, share by FileProvider, delete after — creates the temporary file the issue warns about and races the receiving app still reading it. A test walks cacheDir after a successful export and requires it empty; it fails the moment anybody reintroduces that pattern. The destination is parked until the session is unlocked. Returning from the picker can re-lock, and writing while locked would hand the whole history to whoever took the phone during the save dialog. ## Two new guards, both proved to fail checkNoSharedStorageWrites: §45's shared-storage ban was enforced by nobody having typed it. checkPermissions structurally cannot see it — it matches <uses-permission>, and a <provider> declaring FileProvider merges green. checkNoHealthLogging gains a completeness check. A module missing from modulesSeeingHealthData was silently exempt with a green build, which app/proguard-rules.pro has described as a hazard since before :core:security and :core:export existed. Every module must now be in that list or in an explicit modulesWithNoHealthData with its reason; being in neither is a violation rather than an exemption. 261 JVM tests, none skipped. Five guards green. closes #35 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 22:13:07 -05:00
| 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 |
docs: correct 57 claims the code and tracker disagreed with Every document in the tree audited against the source, the tracker and git history, each finding then given to a second reader tasked with refuting it. 74 raised, 12 refuted, 57 applied. No code changed. THE README DESCRIBED A SKELETON Its Status table — the one place a claim about what is built is allowed to live — still read "there is no usable app yet", with Not built against Room, the four core screens, fertility and notifications, and No round run against QA. Five batches had shipped and three QA rounds had run. TWO DOCUMENTS WERE SILENTLY NEVER FIRING architecture/README.md and design/README.md wrote Governs: as prose ("the Gradle module graph", "the design tokens in core/designsystem"). Neither contains a path token, so doc-triggers.py reduced them to globs matching nothing, and one real glob apiece made them look path-governing rather than subject-governing — the state the script's own header calls invisible. Editing a Room entity never fired the document owning the migration table. Both now fire, proved by running the script. SECURITY.md CLAIMED FOUR UNBUILT PROTECTIONS App lock listed among what works offline; biometric/PIN gating described as protecting app launch; the incognito launcher as existing; Play Billing in the third parties table without the "not yet integrated" marker its neighbours carry. All are Batch 06/07 work. The advertising boundary was overstated in SECURITY.md and the README alike: both said the ads module declares no dependency and a guard proves it. There is no ads module. The pre-declared ":core:ads" to emptySet() rule is stricter than the sentence it replaced and matches nothing until Batch 07, which is why the guard is proved by injection rather than trusted. SMALLER, EACH A REAL TRAP WORK_CYCLE.md pointed at docs/architecture/scripts/forgejo-issue.py, a template path absent here — missed by doc-claims.sh, which reads backticked prose and not fenced blocks. ClaudeReport.md's Round notes said "No rounds yet" after three rounds because ClaudeQAPlan.md's after-a-round list never named that section; the playbook is fixed first. The instrumented-test count was eight in three places and is four. HISTORY.md said the repository had no code and that nothing had been tried and dropped, when three approaches had. DELIBERATELY UNCHANGED ClaudeReport.md's last verified build SHA stays at 0451fbe — no round has run since, and moving it would claim a verification nobody performed. Every DEVELOPMENT_LOG entry stays as written. Guards: ./gradlew check, schema-guard.sh, doc-claims.sh (235 claimed paths, all present), doc-triggers.py, and a link sweep over 21 markdown files. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 16:33:14 -05:00
| QA | Round 3 run, partial | [docs/qa/ClaudeReport.md](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.
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
## Build and run
Prerequisites: a JDK 21 and the Android SDK with **platform 37** and
**build-tools 37.0.0** installed, `ANDROID_HOME` set.
```bash
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](docs/architecture/githooks/README.md).
## Repository map
```text
docs: correct 57 claims the code and tracker disagreed with Every document in the tree audited against the source, the tracker and git history, each finding then given to a second reader tasked with refuting it. 74 raised, 12 refuted, 57 applied. No code changed. THE README DESCRIBED A SKELETON Its Status table — the one place a claim about what is built is allowed to live — still read "there is no usable app yet", with Not built against Room, the four core screens, fertility and notifications, and No round run against QA. Five batches had shipped and three QA rounds had run. TWO DOCUMENTS WERE SILENTLY NEVER FIRING architecture/README.md and design/README.md wrote Governs: as prose ("the Gradle module graph", "the design tokens in core/designsystem"). Neither contains a path token, so doc-triggers.py reduced them to globs matching nothing, and one real glob apiece made them look path-governing rather than subject-governing — the state the script's own header calls invisible. Editing a Room entity never fired the document owning the migration table. Both now fire, proved by running the script. SECURITY.md CLAIMED FOUR UNBUILT PROTECTIONS App lock listed among what works offline; biometric/PIN gating described as protecting app launch; the incognito launcher as existing; Play Billing in the third parties table without the "not yet integrated" marker its neighbours carry. All are Batch 06/07 work. The advertising boundary was overstated in SECURITY.md and the README alike: both said the ads module declares no dependency and a guard proves it. There is no ads module. The pre-declared ":core:ads" to emptySet() rule is stricter than the sentence it replaced and matches nothing until Batch 07, which is why the guard is proved by injection rather than trusted. SMALLER, EACH A REAL TRAP WORK_CYCLE.md pointed at docs/architecture/scripts/forgejo-issue.py, a template path absent here — missed by doc-claims.sh, which reads backticked prose and not fenced blocks. ClaudeReport.md's Round notes said "No rounds yet" after three rounds because ClaudeQAPlan.md's after-a-round list never named that section; the playbook is fixed first. The instrumented-test count was eight in three places and is four. HISTORY.md said the repository had no code and that nothing had been tried and dropped, when three approaches had. DELIBERATELY UNCHANGED ClaudeReport.md's last verified build SHA stays at 0451fbe — no round has run since, and moving it would claim a verification nobody performed. Every DEVELOPMENT_LOG entry stays as written. Guards: ./gradlew check, schema-guard.sh, doc-claims.sh (235 claimed paths, all present), doc-triggers.py, and a link sweep over 21 markdown files. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 16:33:14 -05:00
app/ application module, MainActivity, navigation shell, DI
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
core/designsystem/ Material 3 theme and colour tokens
docs: correct 57 claims the code and tracker disagreed with Every document in the tree audited against the source, the tracker and git history, each finding then given to a second reader tasked with refuting it. 74 raised, 12 refuted, 57 applied. No code changed. THE README DESCRIBED A SKELETON Its Status table — the one place a claim about what is built is allowed to live — still read "there is no usable app yet", with Not built against Room, the four core screens, fertility and notifications, and No round run against QA. Five batches had shipped and three QA rounds had run. TWO DOCUMENTS WERE SILENTLY NEVER FIRING architecture/README.md and design/README.md wrote Governs: as prose ("the Gradle module graph", "the design tokens in core/designsystem"). Neither contains a path token, so doc-triggers.py reduced them to globs matching nothing, and one real glob apiece made them look path-governing rather than subject-governing — the state the script's own header calls invisible. Editing a Room entity never fired the document owning the migration table. Both now fire, proved by running the script. SECURITY.md CLAIMED FOUR UNBUILT PROTECTIONS App lock listed among what works offline; biometric/PIN gating described as protecting app launch; the incognito launcher as existing; Play Billing in the third parties table without the "not yet integrated" marker its neighbours carry. All are Batch 06/07 work. The advertising boundary was overstated in SECURITY.md and the README alike: both said the ads module declares no dependency and a guard proves it. There is no ads module. The pre-declared ":core:ads" to emptySet() rule is stricter than the sentence it replaced and matches nothing until Batch 07, which is why the guard is proved by injection rather than trusted. SMALLER, EACH A REAL TRAP WORK_CYCLE.md pointed at docs/architecture/scripts/forgejo-issue.py, a template path absent here — missed by doc-claims.sh, which reads backticked prose and not fenced blocks. ClaudeReport.md's Round notes said "No rounds yet" after three rounds because ClaudeQAPlan.md's after-a-round list never named that section; the playbook is fixed first. The instrumented-test count was eight in three places and is four. HISTORY.md said the repository had no code and that nothing had been tried and dropped, when three approaches had. DELIBERATELY UNCHANGED ClaudeReport.md's last verified build SHA stays at 0451fbe — no round has run since, and moving it would claim a verification nobody performed. Every DEVELOPMENT_LOG entry stays as written. Guards: ./gradlew check, schema-guard.sh, doc-claims.sh (235 claimed paths, all present), doc-triggers.py, and a link sweep over 21 markdown files. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 16:33:14 -05:00
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
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
domain/cycle/ pure Kotlin — period records, cycle derivation
domain/prediction/ pure Kotlin — the forecast, window and confidence
docs: correct 57 claims the code and tracker disagreed with Every document in the tree audited against the source, the tracker and git history, each finding then given to a second reader tasked with refuting it. 74 raised, 12 refuted, 57 applied. No code changed. THE README DESCRIBED A SKELETON Its Status table — the one place a claim about what is built is allowed to live — still read "there is no usable app yet", with Not built against Room, the four core screens, fertility and notifications, and No round run against QA. Five batches had shipped and three QA rounds had run. TWO DOCUMENTS WERE SILENTLY NEVER FIRING architecture/README.md and design/README.md wrote Governs: as prose ("the Gradle module graph", "the design tokens in core/designsystem"). Neither contains a path token, so doc-triggers.py reduced them to globs matching nothing, and one real glob apiece made them look path-governing rather than subject-governing — the state the script's own header calls invisible. Editing a Room entity never fired the document owning the migration table. Both now fire, proved by running the script. SECURITY.md CLAIMED FOUR UNBUILT PROTECTIONS App lock listed among what works offline; biometric/PIN gating described as protecting app launch; the incognito launcher as existing; Play Billing in the third parties table without the "not yet integrated" marker its neighbours carry. All are Batch 06/07 work. The advertising boundary was overstated in SECURITY.md and the README alike: both said the ads module declares no dependency and a guard proves it. There is no ads module. The pre-declared ":core:ads" to emptySet() rule is stricter than the sentence it replaced and matches nothing until Batch 07, which is why the guard is proved by injection rather than trusted. SMALLER, EACH A REAL TRAP WORK_CYCLE.md pointed at docs/architecture/scripts/forgejo-issue.py, a template path absent here — missed by doc-claims.sh, which reads backticked prose and not fenced blocks. ClaudeReport.md's Round notes said "No rounds yet" after three rounds because ClaudeQAPlan.md's after-a-round list never named that section; the playbook is fixed first. The instrumented-test count was eight in three places and is four. HISTORY.md said the repository had no code and that nothing had been tried and dropped, when three approaches had. DELIBERATELY UNCHANGED ClaudeReport.md's last verified build SHA stays at 0451fbe — no round has run since, and moving it would claim a verification nobody performed. Every DEVELOPMENT_LOG entry stays as written. Guards: ./gradlew check, schema-guard.sh, doc-claims.sh (235 claimed paths, all present), doc-triggers.py, and a link sweep over 21 markdown files. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 16:33:14 -05:00
scripts/ six from the template, plus schema-guard.sh, our own
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/ pre-commit, commit-msg, post-commit
docs: correct 57 claims the code and tracker disagreed with Every document in the tree audited against the source, the tracker and git history, each finding then given to a second reader tasked with refuting it. 74 raised, 12 refuted, 57 applied. No code changed. THE README DESCRIBED A SKELETON Its Status table — the one place a claim about what is built is allowed to live — still read "there is no usable app yet", with Not built against Room, the four core screens, fertility and notifications, and No round run against QA. Five batches had shipped and three QA rounds had run. TWO DOCUMENTS WERE SILENTLY NEVER FIRING architecture/README.md and design/README.md wrote Governs: as prose ("the Gradle module graph", "the design tokens in core/designsystem"). Neither contains a path token, so doc-triggers.py reduced them to globs matching nothing, and one real glob apiece made them look path-governing rather than subject-governing — the state the script's own header calls invisible. Editing a Room entity never fired the document owning the migration table. Both now fire, proved by running the script. SECURITY.md CLAIMED FOUR UNBUILT PROTECTIONS App lock listed among what works offline; biometric/PIN gating described as protecting app launch; the incognito launcher as existing; Play Billing in the third parties table without the "not yet integrated" marker its neighbours carry. All are Batch 06/07 work. The advertising boundary was overstated in SECURITY.md and the README alike: both said the ads module declares no dependency and a guard proves it. There is no ads module. The pre-declared ":core:ads" to emptySet() rule is stricter than the sentence it replaced and matches nothing until Batch 07, which is why the guard is proved by injection rather than trusted. SMALLER, EACH A REAL TRAP WORK_CYCLE.md pointed at docs/architecture/scripts/forgejo-issue.py, a template path absent here — missed by doc-claims.sh, which reads backticked prose and not fenced blocks. ClaudeReport.md's Round notes said "No rounds yet" after three rounds because ClaudeQAPlan.md's after-a-round list never named that section; the playbook is fixed first. The instrumented-test count was eight in three places and is four. HISTORY.md said the repository had no code and that nothing had been tried and dropped, when three approaches had. DELIBERATELY UNCHANGED ClaudeReport.md's last verified build SHA stays at 0451fbe — no round has run since, and moving it would claim a verification nobody performed. Every DEVELOPMENT_LOG entry stays as written. Guards: ./gradlew check, schema-guard.sh, doc-claims.sh (235 claimed paths, all present), doc-triggers.py, and a link sweep over 21 markdown files. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 16:33:14 -05:00
docs/ product, architecture, design, brand, QA, security, history
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
```
`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](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](docs/planning/PRODUCT_PLAN.md) | The full V1 specification — prediction requirements, screens, copy, compliance |
| [docs/planning/PROJECT_PLAN.md](docs/planning/PROJECT_PLAN.md) | The short vision, the stack and its reasons, and what this is deliberately not |
| [docs/WORK_CYCLE.md](docs/WORK_CYCLE.md) | What happens at the end of a piece of work |
| [docs/architecture/README.md](docs/architecture/README.md) | Modules, boundaries, data shapes, migrations |
| [docs/design/README.md](docs/design/README.md) | Tone, and the four rules that settle design arguments |
| [docs/security/SECURITY.md](docs/security/SECURITY.md) | Threat model, the advertising boundary, logging rules |
| [docs/TOOLS.md](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:
```bash
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](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.