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>
This commit is contained in:
parent
b338380cb2
commit
ad085fb4ce
55
README.md
55
README.md
|
|
@ -4,7 +4,7 @@
|
|||
Status: Draft
|
||||
Owner: _null
|
||||
Last reviewed: 2026-08-18
|
||||
Governs: README.md as the project-facing overview for Period
|
||||
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
|
||||
```
|
||||
|
|
@ -45,9 +45,13 @@ Two promises hold the product up:
|
|||
> 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.
|
||||
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
|
||||
|
|
@ -55,28 +59,31 @@ no article feed, no symptom encyclopedia, and not contraception — is in
|
|||
|
||||
## Status
|
||||
|
||||
**Skeleton.** There is no usable app yet, and this section says so rather than
|
||||
listing features that do not exist.
|
||||
**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, 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](docs/qa/ClaudeReport.md) |
|
||||
| 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, export, irreversible delete, monetization | Not built | Batches 06–07 |
|
||||
| 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 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.
|
||||
`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
|
||||
|
||||
|
|
@ -104,13 +111,17 @@ prevent. `post-commit` pushes; see
|
|||
## Repository map
|
||||
|
||||
```text
|
||||
app/ application module, MainActivity, navigation shell
|
||||
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/ the six template scripts this project adopted
|
||||
scripts/ six from the template, plus schema-guard.sh, our own
|
||||
.githooks/ pre-commit, commit-msg, post-commit
|
||||
docs/ product, architecture, design, QA, security, history
|
||||
docs/ product, architecture, design, brand, QA, security, history
|
||||
```
|
||||
|
||||
`domain/*` are `kotlin("jvm")` modules and cannot see the Android SDK. That is
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ disagree, and nothing will say which one is right.
|
|||
| What is a QA round? | [`qa/ClaudeQAPlan.md`](qa/ClaudeQAPlan.md) |
|
||||
| How is it built — modules, boundaries, data shapes, migrations? | [`architecture/README.md`](architecture/README.md) |
|
||||
| What should it feel like, and what words does it use? | [`design/README.md`](design/README.md) |
|
||||
| What colour, logo or type do I use? | [`design/BRAND_GUIDE.md`](design/BRAND_GUIDE.md) — it outranks [`planning/PRODUCT_PLAN.md`](planning/PRODUCT_PLAN.md) §39 where they differ |
|
||||
| What is protected, from whom, and what must never be logged? | [`security/SECURITY.md`](security/SECURITY.md) |
|
||||
| What is checked before a Play release? | [`security/SECURITY_CHECKLIST.md`](security/SECURITY_CHECKLIST.md) |
|
||||
| Which script do I run, and can it stop me? | [`TOOLS.md`](TOOLS.md) — the signpost; [`architecture/README.md`](architecture/README.md) has the table |
|
||||
|
|
@ -80,10 +81,10 @@ Where the two disagree, `PRODUCT_PLAN.md` is the specification and
|
|||
| `docs/planning/` | `PRODUCT_PLAN` — the V1 specification. `PROJECT_PLAN` — the vision. Neither is the schedule; that is the tracker. |
|
||||
| `docs/qa/` | `ClaudeQAPlan` (playbook, passes A–H), `ClaudeQACoverage` (what each pass reached), `ClaudeReport` (the verdict) |
|
||||
| `docs/architecture/` | modules, boundaries, data shapes, the migration table; `GUARDS.md`; `githooks/README.md` |
|
||||
| `docs/design/` | tone, the four rules that settle arguments, and which specification sections own each surface |
|
||||
| `docs/design/` | `README.md` — tone, the four rules that settle arguments, and which specification sections own each surface. `BRAND_GUIDE.md` — the owner-supplied visual identity, which outranks `PRODUCT_PLAN.md` §39 on colour. `brand/` — the source PNGs the marks were cut from |
|
||||
| `docs/security/` | `SECURITY` — threat model, the advertising boundary, logging rules. `SECURITY_CHECKLIST` — the pre-release list |
|
||||
| `docs/history/` | `DEVELOPMENT_LOG` (dated, append-only), `HISTORY` (decisions and their reasons), `BATCH_LEDGER` (archived) |
|
||||
| `docs/data/` | the three branding marks privacyllc.dev renders — **currently absent, tracked as issue #8** |
|
||||
| `docs/data/` | `README.md` — the rule, and why this folder cannot be exempted. `img/README.md` — the three required names, their dimensions and the 512 KB ceiling. `img/icon.webp`, `img/logo.webp`, `img/banner.webp` — the marks privacyllc.dev renders |
|
||||
| `docs/` root | this map; `WORK_CYCLE`; `TOOLS` |
|
||||
|
||||
`README.md` stays at the repository root; it is the landing page and moving it
|
||||
|
|
@ -96,7 +97,7 @@ look identical from outside, and only one of them is fine.
|
|||
|
||||
- **docs/OPERATIONS.md — deleted.** Named without backticks, because
|
||||
`doc-claims.sh` reads a backticked path as a claim the file is there and this
|
||||
one says the opposite. Period is an offline-first Android app
|
||||
one says the opposite. Privacy: Period Tracker is an offline-first Android app
|
||||
distributed through Google Play. There is no host, no container, no uptime and
|
||||
no restore path of ours. An empty runbook reads as one nobody wrote rather
|
||||
than one that never applied.
|
||||
|
|
@ -111,7 +112,7 @@ look identical from outside, and only one of them is fine.
|
|||
`doc-claims.sh` noting a document that mentions a script this project does not
|
||||
have is expected, not a failure.
|
||||
- **A master copy of the hooks under `docs/architecture/githooks/`.** The
|
||||
template keeps one and installs copies; Period keeps only `.githooks/`,
|
||||
template keeps one and installs copies; this repository keeps only `.githooks/`,
|
||||
because `pre-commit` here is adapted for Gradle and a second copy would drift.
|
||||
- **No `Exempt:` declarations.** Every document this convention asks for is
|
||||
either present in git or listed above as deliberately deleted.
|
||||
|
|
@ -184,12 +185,13 @@ Exempt: <the path> — <why, in a few words>
|
|||
and the checker will report that document as deliberately absent. Keep
|
||||
illustrations fenced, and use placeholders anyway, as the form above does.
|
||||
|
||||
Period currently declares none. Where a document is genuinely not applicable it
|
||||
This repository currently declares none. Where a document is genuinely not applicable it
|
||||
has been **deleted** and recorded above instead, which is the honest form: an
|
||||
exemption says "kept elsewhere", and `OPERATIONS.md` is not kept anywhere.
|
||||
|
||||
**`docs/data/` and `docs/data/img/` cannot be exempted.** A declaration naming
|
||||
either is refused rather than honoured, because the Command Center renders what
|
||||
is in them — an exemption would produce a project card with no icon and nothing
|
||||
explaining why. Their absence here is a filed issue, which is the mechanism that
|
||||
gets them drawn.
|
||||
explaining why. If one of them is ever missing, the mechanism is a filed issue —
|
||||
[`data/README.md`](data/README.md) says what to file and why a placeholder is
|
||||
worse than a gap — never an exemption.
|
||||
|
|
|
|||
|
|
@ -23,21 +23,34 @@ A second table here would be two records of one fact, and the other one would
|
|||
never hear that a script was renamed. So this file answers the questions that
|
||||
table does not, and points at it for everything else.
|
||||
|
||||
## Period has seven of them, and that is deliberate
|
||||
## This project has seven of them, and that is deliberate
|
||||
|
||||
The template this repository adopted ships around twenty scripts. Period took
|
||||
six of them and wrote one of its own. `scaffold.sh` copies none of them on purpose — *"an unconfigured
|
||||
`release.sh` landing in every new repository is a loaded gun, not a head
|
||||
start"* — so each one is taken having been read and configured.
|
||||
The template this repository adopted ships around twenty scripts. Privacy:
|
||||
Period Tracker took six of them and wrote one of its own. `scaffold.sh` copies
|
||||
none of them on purpose — *"an unconfigured `release.sh` landing in every new
|
||||
repository is a loaded gun, not a head start"* — so each one is taken having been
|
||||
read and configured.
|
||||
|
||||
**The ones not taken were not forgotten.** `release.sh`, `deploy.py`,
|
||||
`backup.sh`, `restore-check.sh`, `preflight.sh`, `healthcheck.sh`, `migrate.sh`,
|
||||
`status.sh`, `dev.sh`, `audit-gate.mjs`, `release-notes.mjs`, `duplication.py`
|
||||
and `dead-code.py` all assume a deployed Node or Postgres service with an npm
|
||||
dependency tree and a URL. Period is an Android app that ships through Google
|
||||
Play and has no server at all. A release here is an AAB and a Play Console
|
||||
submission, so [`security/SECURITY_CHECKLIST.md`](security/SECURITY_CHECKLIST.md)
|
||||
carries what `release.sh` would have gated.
|
||||
`status.sh`, `dev.sh`, `audit-gate.mjs` and `release-notes.mjs` all assume a
|
||||
deployed Node or Postgres service with an npm dependency tree and a URL.
|
||||
Privacy: Period Tracker is an Android app that ships through Google Play and has
|
||||
no server at all. A release here is an AAB and a Play Console submission, so
|
||||
[`security/SECURITY_CHECKLIST.md`](security/SECURITY_CHECKLIST.md) carries what
|
||||
`release.sh` would have gated.
|
||||
|
||||
`duplication.py` and `dead-code.py` need no server, but both read a JavaScript or
|
||||
TypeScript source tree: `dead-code.py` walks an export graph and excludes
|
||||
Next.js entry points, and `duplication.py` does not scan `.kt` at all. Either
|
||||
would have to be adapted before it said anything true about Kotlin, so neither is
|
||||
taken yet.
|
||||
|
||||
`controls.sh` reports which operational controls a project has — backups, a
|
||||
proven restore, somewhere errors go, an environment that is not production — and
|
||||
exits `2` until some are declared expected. An app with nothing running has none
|
||||
to declare, so taking it would install a check that could only report that it did
|
||||
not run.
|
||||
|
||||
Two were deferred rather than declined:
|
||||
|
||||
|
|
@ -45,7 +58,7 @@ Two were deferred rather than declined:
|
|||
Today `./gradlew check` is the whole answer and wrapping it would add a layer
|
||||
that could only be wrong.
|
||||
- **`check-env.sh`** — its SPEC ships empty and exits `2` until it has entries,
|
||||
and Period has no environment variables yet. It gets taken with the first
|
||||
and this project has no environment variables yet. It gets taken with the first
|
||||
signing or Play credential, which is exactly the moment it becomes worth
|
||||
running.
|
||||
|
||||
|
|
|
|||
|
|
@ -208,8 +208,8 @@ is* — one copy, named once:
|
|||
|
||||
```bash
|
||||
set -a; . ~/.openclaw/docker-registry.env; set +a
|
||||
python3 docs/architecture/scripts/forgejo-issue.py list
|
||||
python3 docs/architecture/scripts/forgejo-issue.py close 42 "Fixed in a1b2c3d; tests/foo.test.js covers it."
|
||||
python3 scripts/forgejo-issue.py list
|
||||
python3 scripts/forgejo-issue.py close 42 "Fixed in a1b2c3d; PredictionAcceptanceTest covers it."
|
||||
```
|
||||
|
||||
Step 7 talks to the Command Center instead:
|
||||
|
|
@ -262,7 +262,7 @@ one, and never report progress the API did not return.
|
|||
|
||||
- **Not the work list.** That is the tracker: milestones are batches, issues are
|
||||
deliverables.
|
||||
- **Not the release procedure.** Period has no `release.sh` — a release here is
|
||||
- **Not the release procedure.** This project has no `release.sh` — a release here is
|
||||
a signed AAB and a Play Console submission, so
|
||||
[`security/SECURITY_CHECKLIST.md`](security/SECURITY_CHECKLIST.md) carries it.
|
||||
- **Not the QA procedure.** That is `docs/qa/ClaudeQAPlan.md`, which ends in its
|
||||
|
|
|
|||
|
|
@ -4,8 +4,9 @@
|
|||
Status: Current
|
||||
Owner: _null
|
||||
Last reviewed: 2026-08-18
|
||||
Governs: docs/architecture/**, the Gradle module graph, and the data shapes that
|
||||
outlive a function
|
||||
Governs: docs/architecture/**, settings.gradle.kts, build.gradle.kts,
|
||||
core/database/**, domain/** — 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
|
||||
|
|
@ -30,14 +31,14 @@ function calls. Nothing below the ViewModel knows Compose exists.
|
|||
|
||||
## Modules
|
||||
|
||||
Seven today — a module created before it has contents is a place
|
||||
Eight today — 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](../planning/PRODUCT_PLAN.md) 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 |
|
||||
| `app` | Android application | `MainActivity`, the four-tab navigation shell, DI wiring | `core/designsystem`, `core/data`, `core/datastore`, `core/notifications`, `domain/*` — never `core/database` |
|
||||
| `core/designsystem` | Android library | Material 3 theme, colour and type tokens | nothing in this project |
|
||||
| `core/database` | Android library | Room entities, DAOs, converters, the schema export | `domain/cycle`, `domain/prediction` |
|
||||
| `core/datastore` | Android library | `UserPreferences` and the settings that are not health history | nothing in this project |
|
||||
|
|
@ -187,8 +188,9 @@ for months.
|
|||
|
||||
## Data shapes
|
||||
|
||||
Defined in `domain/cycle` as plain Kotlin, and mirrored by Room entities in
|
||||
core/database once issue #3 creates it. The full field lists are
|
||||
Defined in `domain/cycle` and `domain/prediction` as plain Kotlin, plus
|
||||
`UserPreferences` in `core/datastore`, and mirrored by Room entities in
|
||||
`core/database`. The full field lists are
|
||||
[`PRODUCT_PLAN.md` §10](../planning/PRODUCT_PLAN.md); what matters here is why
|
||||
each exists and what must not happen to it.
|
||||
|
||||
|
|
|
|||
|
|
@ -17,8 +17,8 @@ exactly one clone on exactly one machine.
|
|||
## The hooks are in `.githooks/`, and only there
|
||||
|
||||
The template this repository adopted keeps a master copy under
|
||||
`docs/architecture/githooks/` and installs copies into `.githooks/`. **Period
|
||||
does not**, deliberately: `pre-commit` here is adapted for Gradle rather than
|
||||
`docs/architecture/githooks/` and installs copies into `.githooks/`. **This
|
||||
repository does not**, deliberately: `pre-commit` here is adapted for Gradle rather than
|
||||
npm, so a second copy would be a second version of a file somebody edits once
|
||||
and forgets — the exact failure `../../DOC_TRUST_MAP.md` exists to prevent.
|
||||
|
||||
|
|
@ -90,7 +90,7 @@ the version without it.
|
|||
## Escape hatches, and why they are loud
|
||||
|
||||
```bash
|
||||
SKIP_GUARDS=1 git commit ... # skips the scan and the tests, and says so
|
||||
SKIP_GUARDS=1 git commit ... # skips the scan, the tests and the message check, and says so
|
||||
SKIP_PUSH=1 git commit ... # commits without publishing, and says so
|
||||
git commit --no-verify ... # skips the hooks entirely, silently
|
||||
```
|
||||
|
|
|
|||
|
|
@ -26,10 +26,10 @@ All **webp**. All **required**. Only `img/` is checked — an asset left in
|
|||
document owns the rule; that one owns the spec. Stating both in both places is
|
||||
how two copies of one convention start disagreeing.
|
||||
|
||||
**No placeholders ship with this template, deliberately.** It carried a 0-byte
|
||||
`logo.webp` once, and an empty file is the worst of the three states: a check
|
||||
that asks "does the path exist" calls it present, and anything that reads the
|
||||
bytes rejects it — a consumer verifying the webp signature answers 415, which
|
||||
**No placeholders ship here, deliberately.** The template this repository
|
||||
adopted carried a 0-byte `logo.webp` once, and an empty file is the worst of the
|
||||
three states: a check that asks "does the path exist" calls it present, and
|
||||
anything that reads the bytes rejects it — a consumer verifying the webp signature answers 415, which
|
||||
reads as a corrupt asset rather than a missing one. Absent is honest and the
|
||||
conformance check reports it as absent, which is what gets it filled in.
|
||||
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ specified.
|
|||
Extra sizes and variants are welcome beside these — `icon-512.webp`,
|
||||
`logo-dark.webp`. Only the three exact names are checked for.
|
||||
|
||||
No placeholders ship with this template. An empty file is the worst of the three
|
||||
No placeholders ship here. An empty file is the worst of the three
|
||||
states: a check that asks whether the path exists calls it present, and anything
|
||||
reading the bytes rejects it. Absent is honest, and the conformance check reports
|
||||
it as absent — which is what gets it filled in.
|
||||
|
|
|
|||
|
|
@ -367,11 +367,12 @@ The brand name is:
|
|||
Keep the colon between Privacy and Period, with a single space after it.
|
||||
|
||||
> **The drawn wordmark does not match this yet.** The supplied artwork sets the
|
||||
> name as *Privacy: Period Tracker*, with no space, and that is what `logo.webp`
|
||||
> and `banner.webp` currently show. The written form above is canonical — it is
|
||||
> what every document, the store listing and the app itself use — so the
|
||||
> lockup needs redrawing to agree with it. Filed as an issue rather than fixed
|
||||
> here, because closing the gap needs an artist and not a rename.
|
||||
> name as *Privacy:Period Tracker* — no space after the colon — and that is what
|
||||
> `docs/data/img/logo.webp` and `docs/data/img/banner.webp` currently show. The
|
||||
> written form above is canonical — it is what every document, the store listing
|
||||
> and the app itself use — so the lockup needs redrawing to agree with it. Filed
|
||||
> as issue #28 (Batch 08, `P2`) rather than fixed here, because closing the gap
|
||||
> needs an artist and not a rename.
|
||||
|
||||
Preferred presentation:
|
||||
|
||||
|
|
@ -381,6 +382,13 @@ Preferred presentation:
|
|||
|
||||
Typography should feel like an elegant modern serif.
|
||||
|
||||
> **This governs the wordmark, store assets and marketing artwork.** In-app
|
||||
> typography is unchanged and still belongs to
|
||||
> [`../planning/PRODUCT_PLAN.md` §40](../planning/PRODUCT_PLAN.md) — system or
|
||||
> Material type, no licensed font — which is what `PeriodTypography` in
|
||||
> `core/designsystem` implements; its one override is the forecast's
|
||||
> `displayLarge`, because §38 makes the number the hero.
|
||||
|
||||
Look for fonts similar to:
|
||||
|
||||
- Cormorant Garamond
|
||||
|
|
@ -655,7 +663,10 @@ Future feature icons should look like they belong to the same family.
|
|||
Examples:
|
||||
|
||||
### Period
|
||||
Droplet or calendar dot
|
||||
Calendar dot or filled cycle disc — never a droplet.
|
||||
[`../planning/PRODUCT_PLAN.md` §42](../planning/PRODUCT_PLAN.md) forbids blood
|
||||
drops, and [`README.md`](README.md) applies that list to every in-app
|
||||
illustration and marker.
|
||||
|
||||
### Fertility
|
||||
Small flower/petal or ovulation circle
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@
|
|||
Status: Current
|
||||
Owner: _null
|
||||
Last reviewed: 2026-08-18
|
||||
Governs: docs/design/**, the design tokens in core/designsystem, and the product's
|
||||
tone and interface copy
|
||||
Governs: docs/design/**, core/designsystem/** — the design tokens — and the
|
||||
product's tone and interface copy
|
||||
Review trigger: Any new user-facing screen or state; any change to the colour or
|
||||
type tokens; any change to notification copy or to a privacy or
|
||||
fertility disclaimer
|
||||
|
|
@ -28,7 +28,8 @@ disclaimer come to exist.
|
|||
| Insights | §27 |
|
||||
| Notification modes, types and flow | §28, §29, §30 |
|
||||
| Incognito launcher | §32 |
|
||||
| Look and feel, visual direction, colour, typography, motion | §37–§41 |
|
||||
| Look and feel, visual direction, typography, motion | §37, §38, §40, §41 |
|
||||
| Colour | §39 — **superseded by [`BRAND_GUIDE.md`](BRAND_GUIDE.md)**; see [Colour](#colour) below |
|
||||
| Artwork | §42 |
|
||||
| Accessibility | §43 |
|
||||
|
||||
|
|
@ -53,8 +54,12 @@ wrong, including anything of ours.
|
|||
|
||||
**2. Nothing asserts certainty the model does not have.** A window and a
|
||||
confidence label, never a bare exact date presented as fact. "Estimated
|
||||
ovulation", never "you are ovulating today". Fertility copy carries the
|
||||
not-contraception line wherever it appears.
|
||||
ovulation", never "you are ovulating today". Every **screen** that shows a
|
||||
fertility estimate carries the not-contraception line, which is where
|
||||
[§18](../planning/PRODUCT_PLAN.md) scopes it — "near fertility features and in
|
||||
About". Notification copy is the deliberate exception: it says "Estimated
|
||||
fertile window" and stops, because a disclaimer is a long sentence about
|
||||
fertility on a lock screen.
|
||||
|
||||
**3. State is never colour alone.** Confirmed period is a solid fill, predicted
|
||||
is dotted or outlined, fertile window is a ring, ovulation is its own small
|
||||
|
|
@ -62,6 +67,11 @@ marker — distinguishable in greyscale, because that is also what makes them
|
|||
distinguishable to a colourblind user and to a screenshot in a bug report.
|
||||
Predicted and confirmed days must never look identical.
|
||||
|
||||
**4. Ads never touch a health action.** No banner in onboarding, in the
|
||||
period-start confirmation, in the period-end confirmation, or between steps of a
|
||||
health workflow — and never an interstitial after logging
|
||||
([§33](../planning/PRODUCT_PLAN.md)).
|
||||
|
||||
**The banner space is reserved before there is an ad to put in it.** §48 wants no
|
||||
layout jump when one loads and a graceful gap when one fails, and both are
|
||||
properties of the space existing whether or not it is filled. Retrofitting the
|
||||
|
|
@ -69,12 +79,6 @@ reservation in Batch 07 means shipping the jump first and finding it in a QA
|
|||
round. It is empty and invisible now — deliberately not a "your ad here" box,
|
||||
which would be a placeholder for the thing a user pays to remove.
|
||||
|
||||
**4. Ads never touch a health action.** No banner in onboarding, in the
|
||||
period-start confirmation, in the period-end confirmation, or between steps of a
|
||||
health workflow — and never an interstitial after logging
|
||||
([§33](../planning/PRODUCT_PLAN.md)). Banner space is reserved in the layout so a
|
||||
failed ad does not move the content.
|
||||
|
||||
## Colour
|
||||
|
||||
The palette is [`BRAND_GUIDE.md`](BRAND_GUIDE.md), supplied by the project owner
|
||||
|
|
@ -131,13 +135,20 @@ Designed here on purpose, because they are the two most people meet first:
|
|||
rather than showing a confident forecast it has not earned. "Getting to know
|
||||
your pattern", not a percentage.
|
||||
|
||||
## The artwork is placeholder, and says so here
|
||||
## The in-app artwork is placeholder, and says so here
|
||||
|
||||
Every illustration and calendar marker in the app is a **Compose vector path**
|
||||
in [`core/designsystem/src/main/kotlin/dev/privacyllc/period/designsystem/art/`](../../core/designsystem/src/main/kotlin/dev/privacyllc/period/designsystem/art/), written as PRODUCT_PLAN.md §42 asks: polished
|
||||
placeholders behind replaceable names, so real artwork changes one function body
|
||||
and no call site.
|
||||
|
||||
Real brand artwork exists — the owner's sources in [`brand/`](brand/) and the
|
||||
three exported marks in [`../data/img/`](../data/img/) — and none of it is in the
|
||||
app yet. The launcher icon keeps its simplified vector because the supplied
|
||||
emblem has content close to its edges and an adaptive icon masks about a quarter
|
||||
of the canvas away, so dropping it in unmodified would crop the shield. Fitting
|
||||
it to the safe zone is Batch 08's final-artwork work.
|
||||
|
||||
The visual language is deliberately narrow — **overlapping circular forms** and
|
||||
nothing else. §42's forbidden list (blood drops, tampons, pads, uterus imagery,
|
||||
gender symbols, anatomy) is a product decision rather than squeamishness: this
|
||||
|
|
|
|||
|
|
@ -14,8 +14,9 @@ Planning used to live in markdown as a numbered batch list. It now lives in the
|
|||
tracker: **milestones are batches, issues are deliverables**, and severity is
|
||||
`P0` / `P1` / `P2` / `release-blocker`.
|
||||
|
||||
This file is what that list *was*. It is kept because the reasoning in it is
|
||||
still worth reading, and deleted files are not readable.
|
||||
In repositories that made that move, this file is what that list *was*. This one
|
||||
never had a batch list to archive — see the final section below — so it is kept
|
||||
for the section above it and for nothing else.
|
||||
|
||||
**It must not be updated.** A batch list beside the tracker is a second answer to
|
||||
"what is open", and two records of the same thing will disagree without saying
|
||||
|
|
@ -38,7 +39,7 @@ ledger and no batch percentages.
|
|||
Creating that file to turn the line green would rebuild, in a new name, the
|
||||
exact second record this one was archived for.
|
||||
|
||||
## Period never had one
|
||||
## This repository never had one
|
||||
|
||||
This repository adopted the tracker convention in its **first commit**, so there
|
||||
was never a markdown batch list here to migrate. Nothing was lost and nothing was
|
||||
|
|
|
|||
|
|
@ -32,6 +32,76 @@ written and stay true. It is exempt from review for the same reason a receipt is
|
|||
|
||||
## Entries
|
||||
|
||||
### 2026-08-18 — A doc audit against the code: 57 wrong claims in eighteen files
|
||||
|
||||
No code changed. Every document in the tree was checked against the source, the
|
||||
tracker and git history, and each finding was then handed to a second reader
|
||||
whose job was to refute it — 74 raised, 12 refuted, 57 applied after dedup.
|
||||
|
||||
**The README described a skeleton.** Its Status table — the one place in this
|
||||
repository a claim about what is built is allowed to live — still said "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. The rename commit touched the file and left
|
||||
the table alone, which is the whole failure mode in one line: a document gets
|
||||
opened, edited for one reason, and its stale half is not what the editor was
|
||||
looking at.
|
||||
|
||||
**Two guards were silently never firing.** `docs/architecture/README.md` and
|
||||
`docs/design/README.md` both wrote their `Governs:` field 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 that match
|
||||
nothing, and because each file had one real glob it was classed as
|
||||
path-governing rather than subject-governing — the one state the script's own
|
||||
header calls invisible. Editing a Room entity never fired the document that owns
|
||||
the migration table; editing the palette never fired the document that owns
|
||||
colour. Both now fire, checked by running the script rather than by reading it.
|
||||
|
||||
**`SECURITY.md` claimed four things the app does not do.** App lock listed among
|
||||
what works offline, biometric/PIN gating described as protecting app launch, the
|
||||
incognito launcher as existing, and Google Play Billing sitting in the third
|
||||
parties table without the *not yet integrated* marker its two neighbours carry.
|
||||
All four are Batch 06 and 07 work. A security document that describes unbuilt
|
||||
protections as shipped is worse than one that omits them, because it is the
|
||||
document somebody checks *instead of* the code.
|
||||
|
||||
**The advertising boundary was overstated in the same way, in two files.** Both
|
||||
`SECURITY.md` and the README said the ads module declares no dependency on the
|
||||
cycle database and a guard proves it. There is no ads module. What exists is
|
||||
better than the prose and had to be described accurately: `":core:ads" to
|
||||
emptySet()` is pre-declared in the root build script, stricter than the sentence
|
||||
it replaced, and matching no module until Batch 07 — which is exactly why the
|
||||
guard is proved by injection rather than trusted.
|
||||
|
||||
**Smaller, and each a real trap.** `WORK_CYCLE.md` told the reader to run
|
||||
`docs/architecture/scripts/forgejo-issue.py`, a template path that does not
|
||||
exist here — missed by `doc-claims.sh` because it only reads backticked prose,
|
||||
not fenced blocks. `ClaudeReport.md`'s Round notes said "No rounds yet" after
|
||||
three rounds, because `ClaudeQAPlan.md`'s after-a-round list never mentioned that
|
||||
section; both are fixed, the playbook 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. The
|
||||
brand guide's wordmark callout had been rewritten by the rename into saying the
|
||||
artwork has "no space" while printing the spaced form — destroying the only
|
||||
counter-example in the repository.
|
||||
|
||||
**What was deliberately left alone.** `ClaudeReport.md`'s last verified build
|
||||
SHA stays at `0451fbe`: no QA round has run since, and moving it to `HEAD` would
|
||||
claim a verification nobody performed. Every `DEVELOPMENT_LOG.md` entry stays as
|
||||
written. `PRODUCT_PLAN.md` §39's teal palette stays superseded rather than
|
||||
rewritten — the routing table now says so at the point a reader would go looking.
|
||||
|
||||
- **Closed:** nothing — these were doc defects, not tracker deliverables.
|
||||
- **Next action:** Batch 06 — Privacy and Security, unchanged. File its issues
|
||||
first; the milestone is empty. Biometric and PIN lock, export, irreversible
|
||||
delete, the privacy promise in Settings, and making §45's logging rules true in
|
||||
a release build. `SECURITY.md` now describes that surface as unbuilt, so the
|
||||
batch has an accurate starting description for the first time.
|
||||
- **Blockers:** #9, the Command Center webhook, still needs a person. The four QA
|
||||
gaps are unchanged and still the better spend: no real lock screen has been
|
||||
looked at, TalkBack has never been run, text has never been scaled, and nothing
|
||||
has run at `minSdk` 26.
|
||||
|
||||
### 2026-08-18 — Renamed to Privacy: Period Tracker
|
||||
|
||||
"Period" was always a working name — `PRODUCT_PLAN.md` §55 said so — and the real
|
||||
|
|
|
|||
|
|
@ -30,9 +30,9 @@ monetization and compliance — written to be handed to a coding agent whole. It
|
|||
is still here, unchanged in substance, as
|
||||
[`../planning/PRODUCT_PLAN.md`](../planning/PRODUCT_PLAN.md).
|
||||
|
||||
That is worth recording because it explains a shape a newcomer would otherwise
|
||||
find odd: this repository has an unusually complete specification and no code.
|
||||
Most projects have the reverse problem.
|
||||
That is worth recording because it explains the shape the first weeks of this
|
||||
repository had: an unusually complete specification and, at the start, no code at
|
||||
all. Most projects have the reverse problem.
|
||||
|
||||
## Decisions and their reasons
|
||||
|
||||
|
|
@ -49,15 +49,32 @@ was true at the time" survives Y stopping being true.
|
|||
| 2026-08-18 | WorkManager for reminders, and **no exact-alarm permission** | a period reminder does not need alarm-clock precision, and requesting that permission is a Play scrutiny cost with no user benefit |
|
||||
| 2026-08-18 | Ads stay behind an abstraction in a module that cannot reach cycle data | the privacy promise is the product; a rule people remember is not a control, a compile error is |
|
||||
| 2026-08-18 | One-time purchase to remove ads, not a subscription | it keeps prediction quality free for everyone and avoids an entire class of entitlement defect — nothing to claw back on cancellation |
|
||||
| 2026-08-18 | Six Gradle modules at the skeleton, not the seventeen the specification sketches | a module created before it has contents is a place for things to be put by accident; the rest arrive with the batch that needs them |
|
||||
| 2026-08-18 | Four Gradle modules at the skeleton, not the seventeen the specification sketches | a module created before it has contents is a place for things to be put by accident; the rest arrive with the batch that needs them |
|
||||
| 2026-08-18 | Eight milestones opened at once, issues filed only under Batch 01 | the roadmap is genuinely known and worth being visible; the *work items* are not, and inventing them would make every tracker percentage permanently wrong |
|
||||
| 2026-08-18 | OPERATIONS.md deleted rather than kept empty | this is an offline app, not a deployed service; an empty runbook reads as one nobody wrote |
|
||||
|
||||
## What was tried and dropped
|
||||
|
||||
Nothing yet. This is the most useful section in the file and the one most often
|
||||
missing — an approach abandoned for a good reason will be proposed again by
|
||||
somebody who does not know it was tried, including you, in a year.
|
||||
This is the most useful section in the file and the one most often missing — an
|
||||
approach abandoned for a good reason will be proposed again by somebody who does
|
||||
not know it was tried, including you, in a year.
|
||||
|
||||
- **A Room schema-export comparison as the schema-drift guard.** `SchemaTest` was
|
||||
written believing it caught drift. Room regenerates the export during
|
||||
compilation, so both sides of every comparison agree by construction, and
|
||||
adding a column without bumping the version stayed green.
|
||||
`scripts/schema-guard.sh` asks git instead, which is the one party Room cannot
|
||||
overwrite.
|
||||
- **`BaselinePredictionEngine` as the product engine.** A robust median over
|
||||
recent intervals, correct and not personal enough: PRODUCT_PLAN §11 names a
|
||||
plain average as an acceptable prototype and an unacceptable final engine.
|
||||
Replaced in Batch 02 by `PersonalPredictionEngine`, and kept as the control
|
||||
`EngineComparisonTest` scores the new engine against — which is what makes the
|
||||
replacement demonstrably better rather than merely different.
|
||||
- **PRODUCT_PLAN §39's muted teal and sage palette.** Dropped for the owner's
|
||||
plum-and-rose brand guide, including a pink fertile window where §39 wanted
|
||||
teal. Safe to drop only because the calendar's states differ in shape rather
|
||||
than colour, which is what §43 actually requires.
|
||||
|
||||
The specification already carries a partial version of it: every `Avoid:` and
|
||||
`Do not:` list in [`../planning/PRODUCT_PLAN.md`](../planning/PRODUCT_PLAN.md)
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ repository:
|
|||
|
||||
Two of those are harder rules than the rest. **Prediction quality is never a
|
||||
paid feature**, and **health data never reaches the advertising subsystem** —
|
||||
see [`PRODUCT_PLAN.md` §34](PRODUCT_PLAN.md) and the boundary guard in
|
||||
see [`PRODUCT_PLAN.md` §33 and §34](PRODUCT_PLAN.md) and the boundary guard in
|
||||
[`../architecture/README.md`](../architecture/README.md).
|
||||
|
||||
## Stack and platform
|
||||
|
|
@ -80,7 +80,7 @@ specification's own version numbers — which the specification itself asks for.
|
|||
| Background work | WorkManager 2.11.2 | reminders must survive process death; **no exact-alarm permission** — a period reminder does not need alarm-clock precision |
|
||||
| DI | Hilt 2.60.1 | wired at the skeleton stage; retrofitting DI across modules later is the expensive order |
|
||||
| Prediction engine | pure Kotlin JVM module | testable without an emulator, which is the only way it gets the test coverage §50 asks for |
|
||||
| Target | `compileSdk`/`targetSdk` 36, `minSdk` 26 | Play requires API 36 for new apps and updates from **2026-08-31** |
|
||||
| Target | `compileSdk` 37, `targetSdk` 36, `minSdk` 26 | current AndroidX requires compiling against 37; `targetSdk` 36 is Play's floor for new apps and updates from **2026-08-31**, and raising it opts the app into runtime behaviour changes, which is a tested decision rather than a build fix |
|
||||
|
||||
## Success looks like
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,8 @@ Batches 04 and 05 landed: fertility estimates and the reminder system. Pass F
|
|||
became runnable for the first time.
|
||||
|
||||
**Environment:** emulator `PeriodQA`, API 36, Pixel 6 profile, debug build, plus
|
||||
8 instrumented tests on the same device.
|
||||
6 instrumented tests on the same device — 4 in `NotificationPrivacyTest`, 2 in
|
||||
`PeriodCrudTest`.
|
||||
|
||||
| Pass | Result | Notes |
|
||||
| --- | --- | --- |
|
||||
|
|
@ -28,7 +29,7 @@ became runnable for the first time.
|
|||
| C — Failure paths | **Partial** | As Round 2. Airplane mode, denied notification permission and a killed process still untried. |
|
||||
| D — Persistence | **Partial** | As Round 2. Reboot and update-over-install untried — the second matters more now that WorkManager holds scheduled work. |
|
||||
| E — Forecast under hard histories | **Partial** | Unit-tested against both engines; the §51 histories still have not been entered by hand. |
|
||||
| F — Notification privacy | **Partial, and this is the important row** | Eight instrumented tests on a device assert what a lock screen *would* render: every kind × both private modes attaches a public version, is marked `VISIBILITY_PRIVATE`, and leaks no health word — including the action labels. **Nobody has yet looked at an actual locked screen.** That is a real gap: the tests check the notification object, and the last mile is what the system chooses to draw. |
|
||||
| F — Notification privacy | **Partial, and this is the important row** | Four instrumented tests on a device assert what a lock screen *would* render: every kind × both private modes attaches a public version and is marked `VISIBILITY_PRIVATE`, no public title or body carries a health word, Direct is the only mode marked public, no channel name in any of the three modes carries one either, and Discreet cannot pop over the screen. Action labels are checked by the JVM unit tests in `NotificationCopyTest`, not on the device. **Nobody has yet looked at an actual locked screen.** That is a real gap: the tests check the notification object, and the last mile is what the system chooses to draw. |
|
||||
| G — Accessibility | **Partial** | Unchanged. Calendar verified in real greyscale again after the palette change. TalkBack and font scaling still never run. |
|
||||
| H — Data ownership | **Partial** | Unchanged. |
|
||||
|
||||
|
|
@ -80,7 +81,7 @@ Three defects, none of which any unit test would have caught:
|
|||
3. **Today's underline collided with the spotting dot**, on the one day that was
|
||||
both. Invisible in the colour screenshot, obvious in greyscale.
|
||||
|
||||
The pattern is now four rounds old and worth stating as a rule: **the defects in
|
||||
The pattern is now two rounds old and worth stating as a rule: **the defects in
|
||||
this project are found by opening it, not by reading it.**
|
||||
|
||||
## Round 1 — 2026-08-18 at `adc5075`, partial
|
||||
|
|
|
|||
|
|
@ -40,8 +40,10 @@ skipped one is visible.
|
|||
| G | Accessibility: TalkBack through the core loop, largest font scale, calendar states distinguishable in greyscale, touch targets, focus order, reduced motion |
|
||||
| H | Data ownership and leakage: export, Delete My Data, biometric/PIN gate, incognito launcher, and the built artifact inspected for anything health-derived reaching the ads or analytics path |
|
||||
|
||||
<Add, remove and rename to fit. A pass that never applies is noise; a pass that
|
||||
is always skipped is a lie.>
|
||||
Add, remove and rename to fit the product. A pass that never applies is noise; a
|
||||
pass that is always skipped is a lie — which is why the template's money and
|
||||
authorisation passes were deleted here rather than carried as permanently
|
||||
skipped rows; *What is deliberately not here*, below, says why.
|
||||
|
||||
### Why E is separate from B
|
||||
|
||||
|
|
@ -105,10 +107,12 @@ Severity is what it costs, not how annoying it is to fix.
|
|||
## After a round
|
||||
|
||||
File each finding as a labelled issue. Update `ClaudeReport.md`'s run-state
|
||||
block and its overall sentence, and `ClaudeQACoverage.md` with what each pass
|
||||
actually reached. A pass that could
|
||||
not be run is recorded as blocked, with what blocks it — never quietly left out,
|
||||
which reads identically to "passed".
|
||||
block, its overall sentence, and its **Round notes** — one entry per round, the
|
||||
verdict only, because the pass-by-pass detail belongs in `ClaudeQACoverage.md`
|
||||
and a second copy of it will drift. Then update `ClaudeQACoverage.md` with what
|
||||
each pass actually reached. A pass that could not be run is recorded as blocked,
|
||||
with what blocks it — never quietly left out, which reads identically to
|
||||
"passed".
|
||||
|
||||
Then **push, and reconcile**. The verdict on the project screen at
|
||||
privacyllc.dev is read out of `ClaudeReport.md` in the pushed repository, so a
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ Review trigger: Any QA round run
|
|||
two-tap logging, a calendar readable in greyscale, insights that decline to
|
||||
overstate, fertility estimates that refuse to appear when the forecast is too
|
||||
vague to support them, and discreet reminders. It now wears its real brand,
|
||||
supplied by the owner. Eight instrumented tests assert that nothing about a
|
||||
supplied by the owner. Four instrumented tests assert that nothing about a
|
||||
period reaches a lock screen in either private mode — but **nobody has yet
|
||||
looked at an actual locked screen**, and that last mile is the single most
|
||||
valuable thing left to check, because a notification read over a shoulder is
|
||||
|
|
@ -57,4 +57,20 @@ build SHA it was found at — a finding that cannot be re-tested cannot be close
|
|||
|
||||
## Round notes
|
||||
|
||||
No rounds yet.
|
||||
One entry per round, the verdict only. What each pass reached, and what it could
|
||||
not, lives in [`ClaudeQACoverage.md`](ClaudeQACoverage.md) and is deliberately
|
||||
not repeated here.
|
||||
|
||||
- **Round 3 — 2026-08-18 at `0451fbe`, partial.** Fertility and reminders landed
|
||||
and pass F became runnable for the first time. Verdict: fit to keep building,
|
||||
not fit to ship — the privacy promise is proved against the notification
|
||||
object and never against a locked screen. Two defects, both in how Android
|
||||
behaves rather than in the app's logic, and both found by running on a device.
|
||||
- **Round 2 — 2026-08-18 at `19edf4c`, partial.** Every screen Batch 03 built
|
||||
was driven by hand. Verdict: the product became coherent, and three defects
|
||||
surfaced that no unit test would have caught — one of them dark mode, broken
|
||||
since Batch 01.
|
||||
- **Round 1 — 2026-08-18 at `adc5075`, partial.** Two passes were all the first
|
||||
build could support. Verdict: sound enough to keep going, and the first thing
|
||||
tried by hand crashed the app with 70 unit tests green — which is the argument
|
||||
for driving it at all.
|
||||
|
|
|
|||
|
|
@ -7,8 +7,9 @@ Last reviewed: 2026-08-18
|
|||
Governs: what this app protects, secret handling, data at rest, and what leaves
|
||||
the device
|
||||
Review trigger: Any new SDK or external service; any new secret; any change to
|
||||
what is stored, exported, backed up or logged; any change to what
|
||||
the ads or billing subsystems can see
|
||||
what is stored, exported, backed up or logged; any change to the
|
||||
declared permission set; any change to what the ads or billing
|
||||
subsystems can see
|
||||
```
|
||||
|
||||
## What this protects, and from whom
|
||||
|
|
@ -47,14 +48,16 @@ Data Safety section, and never lets health data reach any of them.
|
|||
- Cycle history is in **app-private storage** — never external or shared
|
||||
storage, never a world-readable path.
|
||||
- The app works fully offline for logging, editing, prediction, fertility
|
||||
estimates, calendar, insights, notification scheduling and app lock. **No
|
||||
estimates, calendar, insights and notification scheduling. **No
|
||||
server is involved in producing a prediction**, which is the strongest privacy
|
||||
control here: data that never leaves cannot be sold, subpoenaed from us, or
|
||||
breached from a server we do not run.
|
||||
- **No account is required** for core tracking, so there is no identity to
|
||||
correlate the history with.
|
||||
- Biometric/PIN gating protects app launch. Where a secret is needed to back
|
||||
that, it is Android Keystore-backed — never a value in DataStore.
|
||||
- Biometric/PIN gating is **Batch 06 and not built** — nothing gates app launch
|
||||
today, and `UserPreferences.biometricLockEnabled` in `core/datastore` is a
|
||||
persisted flag that nothing outside that module acts on yet. When it lands, any
|
||||
secret backing it is Android Keystore-backed — never a value in DataStore.
|
||||
- **Platform backup is reviewed before the health database is allowed into it.**
|
||||
An Android auto-backup that silently ships the cycle database to a cloud
|
||||
account defeats the entire local-first argument, and it is on by default.
|
||||
|
|
@ -62,6 +65,14 @@ Data Safety section, and never lets health data reach any of them.
|
|||
excluded from backup.
|
||||
- **Delete My Data is irreversible after confirmation** and actually deletes —
|
||||
not a soft flag.
|
||||
- **The declared permission set is a decision, not a build output.**
|
||||
`checkPermissions` in the root `build.gradle.kts` holds the allowed and
|
||||
forbidden sets and fails on anything else in the merged manifest — the
|
||||
**release** one as well as debug. The list itself and the reason for each entry
|
||||
are in [`../architecture/README.md`](../architecture/README.md), which owns
|
||||
them; it is named here because the Play listing and the Data Safety form both
|
||||
describe this set, and because the guard's own failure message sends the reader
|
||||
to this file.
|
||||
|
||||
## Third parties
|
||||
|
||||
|
|
@ -70,7 +81,7 @@ a good table, and this one gets filled in as each subsystem lands.
|
|||
|
||||
| Service | What it receives | Why that is acceptable |
|
||||
| --- | --- | --- |
|
||||
| Google Play Billing | purchase token, product id, device Play identity | required to sell anything on Play; carries no cycle data, and none may be put in billing metadata |
|
||||
| Google Play Billing *(Batch 07, not yet integrated)* | purchase token, product id, device Play identity | required to sell anything on Play; carries no cycle data, and none may be put in billing metadata |
|
||||
| Ads provider *(Batch 07, not yet integrated)* | non-personalized ad request, region consent signal | **no health-derived attribute, ever** — see the boundary below |
|
||||
| Crash reporting *(not yet decided)* | stack traces | only if raw cycle dates cannot appear in them; otherwise not adopted |
|
||||
|
||||
|
|
@ -90,8 +101,11 @@ rather than only as an architecture note:
|
|||
- Every SDK's data collection is audited before each release, against what Data
|
||||
Safety declares.
|
||||
|
||||
Enforced structurally, not remembered: the `ads` module declares no dependency
|
||||
on core/database or `domain/*`, and a Gradle guard proves it — see
|
||||
Written down before it can be broken, and not yet exercised: `checkModuleBoundaries`
|
||||
in the root `build.gradle.kts` already carries `":core:ads" to emptySet()` — when
|
||||
the module arrives in Batch 07 it may declare **no** project dependency at all,
|
||||
which is stricter than "nothing from core/database or `domain/*`". `core:ads` is
|
||||
not in `settings.gradle.kts` yet, so the rule matches no module today — see
|
||||
[`../architecture/README.md`](../architecture/README.md).
|
||||
|
||||
## Logging
|
||||
|
|
@ -154,7 +168,8 @@ Written down so an unknown gap becomes a known one:
|
|||
the database; it does not overwrite flash.
|
||||
- **Someone who knows the unlock PIN.** Biometric/PIN gating raises the bar over
|
||||
an unlocked phone; it does not defend against a person the user has given
|
||||
access to. The incognito launcher option exists for the adjacent problem —
|
||||
access to. The incognito launcher option ([§32](../planning/PRODUCT_PLAN.md),
|
||||
not built yet) is the answer to the adjacent problem —
|
||||
what the app *looks* like on a shared home screen.
|
||||
- **Network-level observation of ad traffic.** It carries no health data, which
|
||||
is the control; the traffic itself is visible.
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ it means the release list stays short enough to finish rather than skim.
|
|||
- [ ] Play **Health apps** declaration completed as applicable — proves the category rules were checked, not assumed
|
||||
- [ ] Store listing carries no medical or contraceptive claim — proves the copy did not drift past what the app can support
|
||||
- [ ] Current Play target-API requirement confirmed **at submission time**, not from this document — proves the deadline was checked rather than remembered
|
||||
- [ ] `./gradlew checkPermissions` passes and every permission it reports is one somebody decided on — proves the Play listing and the Data Safety form describe a chosen set rather than whatever the dependencies brought
|
||||
|
||||
## Standing checks
|
||||
|
||||
|
|
@ -41,7 +42,7 @@ it means the release list stays short enough to finish rather than skim.
|
|||
The one group that is not generic. Every item proves part of
|
||||
[`../planning/PRODUCT_PLAN.md` §34](../planning/PRODUCT_PLAN.md).
|
||||
|
||||
- [ ] the ads module declares no Gradle dependency on core/database or `domain/*`, and the boundary guard was **proved to fail** this release — proves the check is evidence rather than decoration
|
||||
- [ ] `core:ads` is included in `settings.gradle.kts` and declares **no** Gradle project dependency at all — the rule is `":core:ads" to emptySet()` in the root `build.gradle.kts`, stricter than "nothing from core/database or `domain/*`" — and the boundary guard was **proved to fail** this release — proves the check is evidence rather than decoration, and that it examined the module rather than a rule matching nothing
|
||||
- [ ] No health-derived value appears in an ad request extra, user property, or callback log — proves targeting cannot happen by accident
|
||||
- [ ] Ad requests are non-personalized/contextual, with region-appropriate consent — proves the default is the private one
|
||||
- [ ] The ads provider is not initialised for entitled (ad-free) users where practical — proves the purchase removes the SDK, not just the view
|
||||
|
|
|
|||
Loading…
Reference in New Issue