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
|
|
|
|
# Development log — 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
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
Status: Current
|
|
|
|
|
|
Owner: _null
|
|
|
|
|
|
Last reviewed: 2026-08-18
|
|
|
|
|
|
Governs: the dated record of what happened
|
|
|
|
|
|
Review trigger: Nothing. This file is appended to, never revised.
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
## How to use this
|
|
|
|
|
|
|
|
|
|
|
|
Newest first. **One entry per work session**, written before you stop — that is
|
|
|
|
|
|
step 6 of `docs/WORK_CYCLE.md`, and the two lines it insists on are `Next
|
|
|
|
|
|
action` and `Blockers`.
|
|
|
|
|
|
|
|
|
|
|
|
Those two are not decoration. The next session starts by reading the top of this
|
|
|
|
|
|
file, and a session that ended without saying what came next hands the one after
|
|
|
|
|
|
it a re-derivation instead of a starting point — which is where drift enters.
|
|
|
|
|
|
Neither line competes with anything: the live next action is the field on the
|
|
|
|
|
|
project at privacyllc.dev and the live blockers are issues in the tracker, while
|
|
|
|
|
|
these say what both were **at this date**. A record of then never disagrees with
|
|
|
|
|
|
a record of now.
|
|
|
|
|
|
|
|
|
|
|
|
**Append-only by convention.** Correcting an old entry rewrites the record of
|
|
|
|
|
|
what was known at the time, which is the one thing this file is for. If an entry
|
|
|
|
|
|
turns out to be wrong, add a later entry saying so; do not edit the first.
|
|
|
|
|
|
|
|
|
|
|
|
Note the Review trigger above says "nothing", deliberately. A dated log cannot
|
|
|
|
|
|
rot the way a description of current state can — the entries were true when
|
|
|
|
|
|
written and stay true. It is exempt from review for the same reason a receipt is.
|
|
|
|
|
|
|
|
|
|
|
|
## Entries
|
|
|
|
|
|
|
feat: the wordmark has its space, and logo and banner say so
closes #28
The owner supplied a redrawn lockup, docs/design/dist/splash.webp, which sets
the name as "Privacy: Period Tracker" — with the space after the colon.
NECESSARY AND NOT SUFFICIENT
The tempting move was to look at the new artwork and close the issue. #28's
Verify names the two DERIVED assets, not the lockup: the wordmark in
docs/data/img/logo.webp and banner.webp had to match app_full_name, and both
still carried the old spaceless form because they were generated from the
earlier source. Regenerated from the new lockup and checked by cropping the
colon out of each finished webp and reading it at 1350px wide.
THE BANNER IS COMPOSED, NEVER CROPPED
The lockup is square and the banner is 3:1, so a crop takes the ends off the
wordmark — the trap the first pass at these assets hit. A 660px square card
centred on 2176x725, matching how the existing asset did it. No flood-fill this
time either: splash.webp has real transparency where the earlier sources
rendered their rounded corners against black.
icon.webp is deliberately untouched. It carries no wordmark, it is the same
visual family, and #28 is about the name — regenerating it would be churn
dressed as consistency.
BRAND_GUIDE.md §10 carried a callout stating the drawn mark disagreed with the
written name. That is now false, so it records what happened instead and marks
lockup-light-source.png and lockup-dark-source.png as superseded rather than
deleting them; they are the owner's files.
Also corrects a path in the previous log entry, which named splash.png in
backticks — doc-claims reads that as a claim the file is present, and it is now
splash.webp. Named bare, per the convention DOC_TRUST_MAP.md documents for a
path that deliberately does not exist. Second time this session; the lesson is
that converting a file means grepping the docs for its old name.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 22:12:33 -05:00
|
|
|
|
### 2026-08-18 — The wordmark got its space, and #28 closed
|
|
|
|
|
|
|
|
|
|
|
|
The owner supplied a redrawn lockup, `docs/design/dist/splash.webp`. It sets the
|
|
|
|
|
|
name as **Privacy: Period Tracker** — with the space after the colon — which is
|
|
|
|
|
|
the whole of what #28 asked for.
|
|
|
|
|
|
|
|
|
|
|
|
**It was necessary and not sufficient**, which is worth writing down because the
|
|
|
|
|
|
tempting move was to look at the new artwork and close the issue. #28's `Verify`
|
|
|
|
|
|
line names the two *derived* assets, not the lockup: the wordmark in
|
|
|
|
|
|
`docs/data/img/logo.webp` and `banner.webp` had to match `app_full_name`. Both
|
|
|
|
|
|
still carried the old spaceless form, because they were generated from the
|
|
|
|
|
|
earlier source months of decisions ago. Regenerated from the new lockup, checked
|
|
|
|
|
|
by cropping the colon out of each finished webp at 1350px wide and reading it.
|
|
|
|
|
|
|
|
|
|
|
|
**The banner is composed, never cropped.** The lockup is square and the banner
|
|
|
|
|
|
is 3:1, so a crop takes the ends off the wordmark — the exact trap the first pass
|
|
|
|
|
|
at these assets hit. A 660px square card centred on a 2176×725 field, same as the
|
|
|
|
|
|
existing asset did it. No flood-fill this time either: `splash.webp` has real
|
|
|
|
|
|
transparency where the earlier sources rendered their rounded corners against
|
|
|
|
|
|
black.
|
|
|
|
|
|
|
|
|
|
|
|
**`icon.webp` was deliberately left alone.** It carries no wordmark, it is the
|
|
|
|
|
|
same visual family, and #28 is about the name. Regenerating it would have been
|
|
|
|
|
|
churn dressed as consistency.
|
|
|
|
|
|
|
|
|
|
|
|
`BRAND_GUIDE.md` §10 carried a callout saying the drawn mark disagreed with the
|
|
|
|
|
|
written name. That statement is now false, so it says what happened instead, and
|
|
|
|
|
|
names the old lockup sources as superseded rather than deleting them — they are
|
|
|
|
|
|
the owner's files.
|
|
|
|
|
|
|
|
|
|
|
|
- **Closed:** #28.
|
|
|
|
|
|
- **Next action:** Batch 06 has #34, #35 and #37 left, and Batch 08 has #31 and
|
|
|
|
|
|
#32. #37 is the smallest and is unblocked. #34 still carries the open product
|
|
|
|
|
|
question about a forgotten PIN.
|
|
|
|
|
|
- **Blockers:** #9, the Command Center webhook, still needs a person. TalkBack
|
|
|
|
|
|
has still never been run and no real lock screen has been looked at — both
|
|
|
|
|
|
cheap, both never done.
|
|
|
|
|
|
|
feat: guard §45's logging rules, and stop the leak that needed no log call
closes #38
checkNoHealthLogging fails the build on any logging call in a module that can
see a cycle date. It runs in `./gradlew check`.
WHY IT IS A GUARD AND NOT A GREP
Both traps were already live in this repository. PeriodApplication passes
android.util.Log.WARN to WorkManager as a CONSTANT, which is not a log call.
ReminderWorker's KDoc says "a Log.d in a worker is the kind that survives",
explaining why there isn't one — a naive grep fails the build on the clearest
possible explanation, and the obvious fix is to delete the explanation. So it
matches a call shape, and strips comments first.
Proved both directions per GUARDS.md §1: an injected Log.d in CycleRepository
produced exactly one failure; a comment containing Log.d( and println( stayed
green. It also failed its own first run by walking domain/*/bin/, a gitignored
IDE output holding stale copies of test files — a guard that fails on untracked
build output is one somebody switches off.
THE LEAK IT WAS NOT LOOKING FOR
Prediction's init block interpolated dates into its require messages:
require(!windowStart.isAfter(windowEnd)) { "window start $windowStart is..." }
Five predicted dates across three messages, inside an IllegalArgumentException —
the one string a crash reporter collects without anybody choosing to log it.
§45 forbids exactly this and no logging statement was involved.
The same applies to every data class, since toString() renders every field into
any string that touches it. PeriodRecord, SpottingRecord, CycleRecord,
Prediction and NotYetObservation now override it: ids and cycle lengths survive,
dates do not. NoDatesInDiagnosticsTest pins seven cases and was itself proved to
fail.
R8 -assumenosideeffects strips android.util.Log from release, covering what a
source guard cannot reach: a dependency logging on our behalf, and a module
added without being listed in the guard.
VERIFIED ON A RELEASE BUILD, NOT REASONED ABOUT
assembleRelease signed with the debug keystore, installed, driven from
onboarding to a forecast and then logging a period: zero ISO dates in logcat,
zero health words, and the only mentions of the package are the system's own. A
screenshot confirms it reached a real forecast, because "no logs" is trivially
true of an app that did nothing.
201 tests pass. All three guards green.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 22:01:33 -05:00
|
|
|
|
### 2026-08-18 — A logging guard, and the leak it was not looking for
|
|
|
|
|
|
|
|
|
|
|
|
#38 asked for a guard that stops health data reaching a log. Writing it found a
|
|
|
|
|
|
worse problem than the one it was written for.
|
|
|
|
|
|
|
|
|
|
|
|
**The guard.** `checkNoHealthLogging` fails the build on any logging call in a
|
|
|
|
|
|
module that can see a cycle date. Two things make it a guard rather than a grep,
|
|
|
|
|
|
and both were live in this repository already: `PeriodApplication` passes
|
|
|
|
|
|
`android.util.Log.WARN` to WorkManager as a **constant**, which is not a log
|
|
|
|
|
|
call, and `ReminderWorker`'s KDoc says *"a `Log.d` in a worker is the kind that
|
|
|
|
|
|
survives"* — the comment explaining why there isn't one. A naive grep fails the
|
|
|
|
|
|
build on the clearest possible explanation, and the obvious fix is to delete the
|
|
|
|
|
|
explanation. So it matches a call shape and strips comments first.
|
|
|
|
|
|
|
|
|
|
|
|
Proved in both directions, per GUARDS.md §1: injecting a real `Log.d` into
|
|
|
|
|
|
`CycleRepository` produced **exactly one** failure, and a comment containing both
|
|
|
|
|
|
`Log.d(` and `println(` stayed green.
|
|
|
|
|
|
|
|
|
|
|
|
It also failed its first run for a reason worth recording — it walked
|
|
|
|
|
|
`domain/*/bin/`, an IDE output directory that is gitignored and holds stale
|
|
|
|
|
|
copies of test files, and reported a `println` in one of them. A guard that
|
|
|
|
|
|
fails on untracked build output is a guard somebody switches off.
|
|
|
|
|
|
|
|
|
|
|
|
**And the thing it could never have caught.** `Prediction`'s `init` block
|
|
|
|
|
|
interpolated dates into its `require` messages:
|
|
|
|
|
|
|
|
|
|
|
|
require(!windowStart.isAfter(windowEnd)) { "window start $windowStart is after window end $windowEnd" }
|
|
|
|
|
|
|
|
|
|
|
|
Five predicted dates across three messages, in an `IllegalArgumentException` —
|
|
|
|
|
|
the one string a crash reporter is guaranteed to collect without anybody
|
|
|
|
|
|
choosing to log it. §45 says "do not include raw cycle dates in crash reports",
|
|
|
|
|
|
and this was the app doing exactly that, with no logging statement anywhere near
|
|
|
|
|
|
it.
|
|
|
|
|
|
|
|
|
|
|
|
The same shape applies to every data class: `toString()` renders every field
|
|
|
|
|
|
into any string that touches it. `PeriodRecord`, `SpottingRecord`,
|
|
|
|
|
|
`CycleRecord`, `Prediction` and `NotYetObservation` now override it — ids and
|
|
|
|
|
|
lengths survive, dates do not, because an id identifies a row without describing
|
|
|
|
|
|
a person and a cycle length says nothing about when. `NoDatesInDiagnosticsTest`
|
|
|
|
|
|
pins all seven cases, and was itself proved to fail.
|
|
|
|
|
|
|
|
|
|
|
|
**The release build was driven, not reasoned about.** `assembleRelease` is
|
|
|
|
|
|
unsigned, so it was signed with the debug keystore and installed alongside the
|
|
|
|
|
|
debug build. Onboarding to a forecast, then logging a period: **zero ISO dates in
|
|
|
|
|
|
logcat, zero health words, and the only mentions of the package are the system's
|
|
|
|
|
|
own.** The screenshot confirms it reached a real forecast rather than failing
|
|
|
|
|
|
early, because "no logs" is trivially true of an app that did nothing.
|
|
|
|
|
|
|
|
|
|
|
|
- **Closed:** #30, #38.
|
|
|
|
|
|
- **Next action:** Batch 06 has #34, #35 and #37 left. #37, the privacy promise
|
|
|
|
|
|
in Settings, is the smallest and needs only the screen that now exists. #35,
|
|
|
|
|
|
export, is the bigger one and its trap is §45 again — an export is health data
|
|
|
|
|
|
leaving the app, so it uses the Storage Access Framework rather than writing to
|
|
|
|
|
|
shared storage. #34, app lock, still carries the open product question: what
|
|
|
|
|
|
happens when somebody forgets their PIN, with an irreversible delete on the
|
|
|
|
|
|
same screen.
|
|
|
|
|
|
- **Blockers:** #9, the Command Center webhook, still needs a person. TalkBack
|
|
|
|
|
|
has still never been run and no real lock screen has been looked at. A
|
feat: the wordmark has its space, and logo and banner say so
closes #28
The owner supplied a redrawn lockup, docs/design/dist/splash.webp, which sets
the name as "Privacy: Period Tracker" — with the space after the colon.
NECESSARY AND NOT SUFFICIENT
The tempting move was to look at the new artwork and close the issue. #28's
Verify names the two DERIVED assets, not the lockup: the wordmark in
docs/data/img/logo.webp and banner.webp had to match app_full_name, and both
still carried the old spaceless form because they were generated from the
earlier source. Regenerated from the new lockup and checked by cropping the
colon out of each finished webp and reading it at 1350px wide.
THE BANNER IS COMPOSED, NEVER CROPPED
The lockup is square and the banner is 3:1, so a crop takes the ends off the
wordmark — the trap the first pass at these assets hit. A 660px square card
centred on 2176x725, matching how the existing asset did it. No flood-fill this
time either: splash.webp has real transparency where the earlier sources
rendered their rounded corners against black.
icon.webp is deliberately untouched. It carries no wordmark, it is the same
visual family, and #28 is about the name — regenerating it would be churn
dressed as consistency.
BRAND_GUIDE.md §10 carried a callout stating the drawn mark disagreed with the
written name. That is now false, so it records what happened instead and marks
lockup-light-source.png and lockup-dark-source.png as superseded rather than
deleting them; they are the owner's files.
Also corrects a path in the previous log entry, which named splash.png in
backticks — doc-claims reads that as a claim the file is present, and it is now
splash.webp. Named bare, per the convention DOC_TRUST_MAP.md documents for a
path that deliberately does not exist. Second time this session; the lesson is
that converting a file means grepping the docs for its old name.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 22:12:33 -05:00
|
|
|
|
docs/design/dist/splash.png — named bare, because it no longer exists under
|
|
|
|
|
|
that name — appeared in the tree during this session and was untracked at the
|
|
|
|
|
|
time. It turned out to be the redrawn lockup, and is now committed as
|
|
|
|
|
|
`splash.webp`.
|
feat: guard §45's logging rules, and stop the leak that needed no log call
closes #38
checkNoHealthLogging fails the build on any logging call in a module that can
see a cycle date. It runs in `./gradlew check`.
WHY IT IS A GUARD AND NOT A GREP
Both traps were already live in this repository. PeriodApplication passes
android.util.Log.WARN to WorkManager as a CONSTANT, which is not a log call.
ReminderWorker's KDoc says "a Log.d in a worker is the kind that survives",
explaining why there isn't one — a naive grep fails the build on the clearest
possible explanation, and the obvious fix is to delete the explanation. So it
matches a call shape, and strips comments first.
Proved both directions per GUARDS.md §1: an injected Log.d in CycleRepository
produced exactly one failure; a comment containing Log.d( and println( stayed
green. It also failed its own first run by walking domain/*/bin/, a gitignored
IDE output holding stale copies of test files — a guard that fails on untracked
build output is one somebody switches off.
THE LEAK IT WAS NOT LOOKING FOR
Prediction's init block interpolated dates into its require messages:
require(!windowStart.isAfter(windowEnd)) { "window start $windowStart is..." }
Five predicted dates across three messages, inside an IllegalArgumentException —
the one string a crash reporter collects without anybody choosing to log it.
§45 forbids exactly this and no logging statement was involved.
The same applies to every data class, since toString() renders every field into
any string that touches it. PeriodRecord, SpottingRecord, CycleRecord,
Prediction and NotYetObservation now override it: ids and cycle lengths survive,
dates do not. NoDatesInDiagnosticsTest pins seven cases and was itself proved to
fail.
R8 -assumenosideeffects strips android.util.Log from release, covering what a
source guard cannot reach: a dependency logging on our behalf, and a module
added without being listed in the guard.
VERIFIED ON A RELEASE BUILD, NOT REASONED ABOUT
assembleRelease signed with the debug keystore, installed, driven from
onboarding to a forecast and then logging a period: zero ISO dates in logcat,
zero health words, and the only mentions of the package are the system's own. A
screenshot confirms it reached a real forecast, because "no logs" is trivially
true of an app that did nothing.
201 tests pass. All three guards green.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 22:01:33 -05:00
|
|
|
|
|
feat: Delete My Data, and fix a privacy option nobody could tap
closes #36
The deletion has existed since Batch 01 with an instrumented test and no way to
reach it. This adds the Privacy & Security section, a confirmation that says
what goes and what stays, and PrivacyViewModelTest.
A PRIVACY CONTROL WHOSE LABEL DID NOTHING
The confirmation promises "your reminder settings are unchanged". Proving that
meant changing a setting first, so I tapped "Maximum privacy" on a device and
nothing happened: PrivacyRow and onboarding's PrivacyOption both put onClick on
the RadioButton and left the row inert. The option that decides what a lock
screen shows could only be changed by hitting a 20dp circle — in both places a
user ever chooses it.
Modifier.selectable on the row, onClick = null on the radio. That is Material's
documented pattern and it also merges the semantics, so TalkBack announces one
selectable option instead of a radio button and two loose strings.
Found by trying to verify a different claim, which is the argument for verifying
claims rather than asserting them. The setting does survive deletion — set to
Maximum privacy, deleted everything, still Maximum privacy.
A DESIGN THAT WAS WRONG BEFORE IT WAS WRITTEN
The first draft cancelled the reminder schedule on delete. Seems obviously
right; is not. ReminderWorker reads the forecast each run and NoData maps to no
decision, so scheduled work already does nothing while there is nothing to say —
and scheduling only happens from ReminderCoordinator and the settings screen, so
cancelling would have left reminders silently off until the user next toggled
something, long after logging a new period. Checked the call sites instead of
reasoning from the name.
Delete touches health data only. UserPreferences is a separate store precisely
so a privacy action cannot reset a choice somebody made, and there is no undo —
§45 says irreversible, and an undo snackbar keeps the data alive for its
timeout.
Round 4 recorded in docs/qa/. 194 tests pass; ./gradlew check, schema-guard and
doc-claims all pass. Driven on PeriodMinSdk26.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 21:03:16 -05:00
|
|
|
|
### 2026-08-18 — Batch 06 starts, and two defects that only a device could show
|
|
|
|
|
|
|
|
|
|
|
|
The designed Settings screen and Delete My Data both landed. Neither is the
|
|
|
|
|
|
interesting part of the day.
|
|
|
|
|
|
|
|
|
|
|
|
**A privacy control whose label did nothing.** The delete confirmation promises
|
|
|
|
|
|
"your reminder settings are unchanged", and proving that meant changing a
|
|
|
|
|
|
setting first — so I tapped "Maximum privacy" on a device and watched nothing
|
|
|
|
|
|
happen. `PrivacyRow` and onboarding's `PrivacyOption` both put `onClick` on the
|
|
|
|
|
|
`RadioButton` and left the row inert. The option deciding what a lock screen
|
|
|
|
|
|
shows could only be changed by hitting a 20dp circle, in the two places a user
|
|
|
|
|
|
ever chooses it. Fixed with `Modifier.selectable` on the row, which also merges
|
|
|
|
|
|
the semantics so TalkBack announces one option instead of a radio and two loose
|
|
|
|
|
|
strings.
|
|
|
|
|
|
|
|
|
|
|
|
It was found by trying to verify a different claim. That is worth noting on its
|
|
|
|
|
|
own: the check that found it was not looking for it.
|
|
|
|
|
|
|
|
|
|
|
|
**And the setting does survive deletion** — set to Maximum privacy, deleted
|
|
|
|
|
|
everything, still Maximum privacy. Now demonstrated rather than argued from the
|
|
|
|
|
|
fact that `core/datastore` is a separate store.
|
|
|
|
|
|
|
|
|
|
|
|
**A design I had wrong before writing it.** My first draft of `PrivacyViewModel`
|
|
|
|
|
|
cancelled the reminder schedule on delete, which seems obviously right and is
|
|
|
|
|
|
not. `ReminderWorker` reads the forecast each run and `NoData` maps to no
|
|
|
|
|
|
decision, so the scheduled work already does nothing while there is nothing to
|
|
|
|
|
|
say — and scheduling only happens from `ReminderCoordinator` and the settings
|
|
|
|
|
|
screen, so cancelling would have left reminders silently off until the user next
|
|
|
|
|
|
toggled something, long after they had logged a new period. Checked the call
|
|
|
|
|
|
sites instead of reasoning from the name.
|
|
|
|
|
|
|
|
|
|
|
|
**Also fixed, from the font-scale pass:** the bottom navigation wrapped its
|
|
|
|
|
|
labels mid-word at scale 2.0 — *"Calenda / r"*. `maxLines = 1` and an ellipsis.
|
|
|
|
|
|
That defect predates all of today's work; the font-scale gap is simply the first
|
|
|
|
|
|
thing that looked.
|
|
|
|
|
|
|
|
|
|
|
|
**Settings shows only the sections that have something behind them.** A
|
|
|
|
|
|
greyed-out "Delete My Data" tells a user that control over their own health data
|
|
|
|
|
|
exists and that they may not have it, which is a bad first thing for this
|
|
|
|
|
|
product to say. Recorded in `docs/design/README.md` so the next person adding a
|
|
|
|
|
|
row knows it was a decision.
|
|
|
|
|
|
|
|
|
|
|
|
Round 4 recorded in `docs/qa/`. Four rounds in, the pattern has not moved: every
|
|
|
|
|
|
defect of consequence has been found by a person using the app, and none by a
|
|
|
|
|
|
test.
|
|
|
|
|
|
|
|
|
|
|
|
- **Closed:** #29, #33, #36.
|
|
|
|
|
|
- **Next action:** Continue Batch 06. #38, the release-build logging guard, is
|
|
|
|
|
|
independent of the rest and can go next — write the guard first and prove it
|
|
|
|
|
|
fails, per `docs/architecture/GUARDS.md`, because three guards in this project
|
|
|
|
|
|
have been green over the exact failure they claimed to catch. #35 export and
|
|
|
|
|
|
#37 the privacy promise both need only the Settings screen, which now exists.
|
|
|
|
|
|
#34, app lock, still carries an open product question: what happens when
|
|
|
|
|
|
somebody forgets their PIN, given an irreversible delete sits on the same
|
|
|
|
|
|
screen.
|
|
|
|
|
|
- **Blockers:** #9, the Command Center webhook, still needs a person. TalkBack
|
|
|
|
|
|
has still never been run and no real lock screen has been looked at — and
|
|
|
|
|
|
after today's find, the TalkBack gap looks more expensive than it did: two
|
|
|
|
|
|
screens had merged-semantics problems nobody would see without it.
|
|
|
|
|
|
|
feat: ship the onboarding artwork in both themes
closes #29
All eight illustrations are in the app, every onboarding step has art for the
first time, and the theme swap is resource resolution rather than a branch:
drawable/ and drawable-night/, so Android picks the asset and the artwork follows
a device theme change with nothing of ours running.
NOT VECTORS, AND THE ISSUE WAS WRONG TO ASK FOR THEM
#29 called for a redraw as Compose vector paths taking colour from theme tokens.
That is right for artwork that does not exist yet and wrong for gradient
landscapes with glow and depth — there is no honest VectorDrawable of one, and
the attempt loses the drawing or produces path soup slower than the image. §42
objects to *unnecessary* raster, which this is not.
The cost was never measured before it was argued about: all sixteen files at the
height they are actually drawn at come to 130 KB. Release APK 1.90 -> 2.03 MB.
WHAT ONLY A DEVICE SHOWED
Each drawing is composed as a card with its own rounded corner painted in, so the
pixels outside that curve are the card's own backdrop — near-black in the dark
set. Rendered unclipped, every illustration showed four notches against the app
background. Illustration() now clips just inside the painted curve.
WHAT STAYED A VECTOR
The calendar markers and CycleProgressMark, because they are drawn from data — a
marker's shape is what a day is, and the progress arc is where the user stands in
their cycle. CycleProgressMark moves from Illustrations.kt to CycleMarkers.kt so
the file layout carries that boundary.
Sized by height rather than a square box: the set has no common aspect ratio, and
height is the dimension that decides whether the primary button stays on screen.
Steps 3 and 6 take 104dp because they carry three buttons and three option cards;
verified by driving all seven steps, not by reasoning about it.
OnboardingPreviews.kt now has one entry per illustration, because a missing
drawable-night asset is invisible in light mode.
Driven end to end on PeriodMinSdk26 in both themes, no crash. ./gradlew check,
schema-guard and doc-claims all pass.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 20:25:59 -05:00
|
|
|
|
### 2026-08-18 — The onboarding artwork ships, and a plan I had argued for turned out to be wrong
|
|
|
|
|
|
|
|
|
|
|
|
All eight illustrations are in the app in both themes, every onboarding step has
|
|
|
|
|
|
art for the first time, and **#29 closes**. The interesting part is that the plan
|
|
|
|
|
|
I pushed hardest for is not the one that shipped.
|
|
|
|
|
|
|
|
|
|
|
|
**I argued for vectors and was wrong.** The issue said redraw the set as Compose
|
|
|
|
|
|
vector paths taking colour from theme tokens. That is the right answer for
|
|
|
|
|
|
artwork that does not exist yet — it is exactly what §42 prescribes and what the
|
|
|
|
|
|
placeholders did. It is the wrong answer for gradient landscapes with glow and
|
|
|
|
|
|
depth: there is no honest `VectorDrawable` of one, and the attempt would either
|
|
|
|
|
|
lose the drawing or produce path soup slower than the image. §42 objects to
|
|
|
|
|
|
*unnecessary* raster, and I had been reading it as objecting to raster.
|
|
|
|
|
|
|
|
|
|
|
|
The number settled it. All sixteen files at the height they are actually drawn
|
|
|
|
|
|
at cost **130 KB**: the release APK went 1.90 MB to 2.03 MB. I had been arguing
|
|
|
|
|
|
against a cost I never measured.
|
|
|
|
|
|
|
|
|
|
|
|
**The theme swap is resource resolution, not a branch.** `drawable/` and
|
|
|
|
|
|
`drawable-night/`, so Android picks the asset and the artwork follows a device
|
|
|
|
|
|
theme change with nothing of ours running. It also means a missing night asset is
|
|
|
|
|
|
invisible in light mode, which is why `OnboardingPreviews.kt` now has one entry
|
|
|
|
|
|
per illustration rather than a representative few.
|
|
|
|
|
|
|
|
|
|
|
|
**What only a device showed.** Each drawing is composed as a card with its own
|
|
|
|
|
|
rounded corner painted in, so the pixels outside that curve are the card's own
|
|
|
|
|
|
backdrop — near-black in the dark set. Rendered unclipped, every illustration had
|
|
|
|
|
|
four visible notches against the app background. Nothing in the build says so,
|
|
|
|
|
|
the previews are small, and it was obvious within a second of looking at a phone.
|
|
|
|
|
|
`Illustration` now clips just inside the painted curve.
|
|
|
|
|
|
|
|
|
|
|
|
**Two vectors stayed vectors**, and the line is worth keeping: the calendar
|
|
|
|
|
|
markers and `CycleProgressMark` are drawn *from data* — a marker's shape is what
|
|
|
|
|
|
a day is, and the progress arc is where the user actually stands in their cycle.
|
|
|
|
|
|
Good artwork does not make those into images. `CycleProgressMark` moved from
|
|
|
|
|
|
`Illustrations.kt` to `CycleMarkers.kt` to put that boundary in the file layout.
|
|
|
|
|
|
|
|
|
|
|
|
**Sizing is by height, not a square box.** The set does not share one aspect
|
|
|
|
|
|
ratio and a square would letterbox some and crop others. Height is also the
|
|
|
|
|
|
dimension that decides whether the primary button stays on screen, so steps 3 and
|
|
|
|
|
|
6 — three buttons and three option cards respectively — take 104 dp where the
|
|
|
|
|
|
others take 120–128 dp. Checked by driving all seven steps rather than by
|
|
|
|
|
|
reasoning about it.
|
|
|
|
|
|
|
|
|
|
|
|
Driven end to end on `PeriodMinSdk26` in both themes, no crash.
|
|
|
|
|
|
|
|
|
|
|
|
- **Closed:** #29.
|
|
|
|
|
|
- **Next action:** Batch 06 — Privacy and Security. File its issues first; the
|
|
|
|
|
|
milestone is still empty. Biometric and PIN lock, export, irreversible delete,
|
|
|
|
|
|
the privacy promise in Settings, and making §45's logging rules true in a
|
|
|
|
|
|
release build.
|
|
|
|
|
|
- **Blockers:** #9, the Command Center webhook, still needs a person. The QA gaps
|
|
|
|
|
|
are unchanged: no real lock screen has been looked at, TalkBack has never been
|
|
|
|
|
|
run, and text has never been scaled — the last one now matters more, because
|
|
|
|
|
|
every onboarding step has an illustration above its heading and nobody has seen
|
|
|
|
|
|
that layout at the largest font scale.
|
|
|
|
|
|
|
docs: record the minSdk emulator, and the first run at API 26
A second AVD exists — PeriodMinSdk26, API 26, Pixel 6 — and the app was built,
installed and driven on it end to end. Everything before today ran on API 36,
which the standing gaps list has said since Batch 01.
The QA plan now names both emulators, because rounds cite PeriodQA by name and
nothing defined it. It also carries the recreate command and three mechanical
traps that each cost time today:
- a windowed emulator dies with its session's X server (XIO: fatal IO error on
X server ":0") and took one run down mid-flow; -no-window has no such
dependency and screencap still works
- adb regularly has three devices attached on this machine, since other projects
keep emulators running, so the serial must be resolved from the AVD name
rather than assumed to be emulator-5554
- the debug build's application id carries a .debug suffix, so
`monkey -p dev.privacyllc.period` aborts with "No activities found to run"
after a successful install
The coverage doc's "No device at minSdk" gap is marked partly closed, with what
was reached (onboarding to first forecast, the date picker, all four tabs, a
relaunch after a cold boot) and what was not (notifications, the instrumented
suites, anything needing more than one cycle of history).
Also fixes a guard failure I introduced in ad085fb: that commit's log entry
named docs/architecture/scripts/forgejo-issue.py in backticks, and doc-claims.sh
reads a backticked path as a claim the file exists. I had run the guard before
writing the entry and not after. Named bare now, per the convention
DOC_TRUST_MAP.md already documents; doc-claims reports 255 paths, all present.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 18:07:07 -05:00
|
|
|
|
### 2026-08-18 — The app ran at `minSdk` for the first time
|
|
|
|
|
|
|
|
|
|
|
|
A second emulator exists — `PeriodMinSdk26`, API 26, Pixel 6 — and the app was
|
|
|
|
|
|
built, installed and driven on it start to finish. Everything before today ran on
|
|
|
|
|
|
API 36, which the standing gaps list has said since Batch 01.
|
|
|
|
|
|
|
|
|
|
|
|
**It works.** Onboarding from *Get Started* to a first forecast, the Material 3
|
|
|
|
|
|
date picker, Today, Calendar, Insights and Settings, all rendering correctly on
|
|
|
|
|
|
Android 8.0. The forecast was right — 4 August plus 28 days is 1 September, an
|
|
|
|
|
|
11-day window, confidence honestly **Low** on one cycle of history — and
|
|
|
|
|
|
fertility declined to estimate rather than inventing one. No crash, and no
|
|
|
|
|
|
`NoSuchMethodError`, `VerifyError` or `NoClassDefFoundError` attributable to the
|
|
|
|
|
|
app. The one `NoClassDefFoundError` in logcat is
|
|
|
|
|
|
`com.google.android.googlequicksearchbox` failing to resolve `TracingController`,
|
|
|
|
|
|
an API 28 class, in its own process.
|
|
|
|
|
|
|
|
|
|
|
|
That is a genuinely useful negative result: the two `NewApi` bugs found in Batch
|
|
|
|
|
|
01 were exactly this shape and would have crashed here.
|
|
|
|
|
|
|
|
|
|
|
|
**Three mechanical traps, all now in the QA playbook** rather than in somebody's
|
|
|
|
|
|
memory:
|
|
|
|
|
|
|
|
|
|
|
|
1. **A windowed emulator dies with the session's X server.** The first one was
|
|
|
|
|
|
killed by `XIO: fatal IO error on X server ":0"` mid-flow. `-no-window` has no
|
|
|
|
|
|
such dependency and screenshots still work through `adb exec-out screencap`.
|
|
|
|
|
|
2. **`adb` regularly has three devices attached here**, because other projects
|
|
|
|
|
|
keep their own emulators running, and a bare `adb shell` then fails with *more
|
|
|
|
|
|
than one device/emulator*. The serial has to be resolved from the AVD name;
|
|
|
|
|
|
assuming `emulator-5554` targets somebody else's work.
|
|
|
|
|
|
3. **The debug build's application id carries a `.debug` suffix**, so
|
|
|
|
|
|
`monkey -p dev.privacyllc.period` aborts with *No activities found to run*
|
|
|
|
|
|
after a successful install — which reads as a broken app rather than a wrong
|
|
|
|
|
|
package name.
|
|
|
|
|
|
|
|
|
|
|
|
**And a free persistence result.** The emulator was killed and cold-booted with
|
|
|
|
|
|
onboarding half finished. The install survived, and the app restarted at the
|
|
|
|
|
|
beginning of onboarding with nothing partial committed — which is correct, and is
|
|
|
|
|
|
the first reboot evidence pass D has ever had.
|
|
|
|
|
|
|
|
|
|
|
|
**A guard failure of my own, from the entry above.** That entry named the bad
|
|
|
|
|
|
`forgejo-issue.py` path in backticks, and `doc-claims.sh` reads a backticked path
|
|
|
|
|
|
as a claim the file exists — so commit `ad085fb` shipped with the guard failing.
|
|
|
|
|
|
I had run it before writing the log entry and not after. The path is named bare
|
|
|
|
|
|
now, which is the convention `DOC_TRUST_MAP.md` already documents for
|
|
|
|
|
|
deliberately-absent files, and the guard reports 251 claimed paths all present.
|
|
|
|
|
|
|
|
|
|
|
|
- **Closed:** nothing. This closes no issue; it partly closes a standing QA gap,
|
|
|
|
|
|
which lives in `docs/qa/ClaudeQACoverage.md` rather than the tracker.
|
|
|
|
|
|
- **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.
|
|
|
|
|
|
- **Blockers:** #9, the Command Center webhook, still needs a person. Three of the
|
|
|
|
|
|
four QA gaps are unchanged and remain the cheaper spend: nobody has looked at a
|
|
|
|
|
|
real lock screen, TalkBack has never been run, and text has never been scaled.
|
|
|
|
|
|
The fourth — nothing has run at `minSdk` — is now partly answered, though
|
|
|
|
|
|
notifications, the instrumented suites and any multi-cycle history are still
|
|
|
|
|
|
unreached at API 26.
|
|
|
|
|
|
|
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
|
|
|
|
### 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.
|
|
|
|
|
|
|
docs: record the minSdk emulator, and the first run at API 26
A second AVD exists — PeriodMinSdk26, API 26, Pixel 6 — and the app was built,
installed and driven on it end to end. Everything before today ran on API 36,
which the standing gaps list has said since Batch 01.
The QA plan now names both emulators, because rounds cite PeriodQA by name and
nothing defined it. It also carries the recreate command and three mechanical
traps that each cost time today:
- a windowed emulator dies with its session's X server (XIO: fatal IO error on
X server ":0") and took one run down mid-flow; -no-window has no such
dependency and screencap still works
- adb regularly has three devices attached on this machine, since other projects
keep emulators running, so the serial must be resolved from the AVD name
rather than assumed to be emulator-5554
- the debug build's application id carries a .debug suffix, so
`monkey -p dev.privacyllc.period` aborts with "No activities found to run"
after a successful install
The coverage doc's "No device at minSdk" gap is marked partly closed, with what
was reached (onboarding to first forecast, the date picker, all four tabs, a
relaunch after a cold boot) and what was not (notifications, the instrumented
suites, anything needing more than one cycle of history).
Also fixes a guard failure I introduced in ad085fb: that commit's log entry
named docs/architecture/scripts/forgejo-issue.py in backticks, and doc-claims.sh
reads a backticked path as a claim the file exists. I had run the guard before
writing the entry and not after. Named bare now, per the convention
DOC_TRUST_MAP.md already documents; doc-claims reports 255 paths, all present.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 18:07:07 -05:00
|
|
|
|
**Smaller, and each a real trap.** `WORK_CYCLE.md` told the reader to run a
|
|
|
|
|
|
`forgejo-issue.py` under docs/architecture/scripts/ — a template path that does
|
|
|
|
|
|
not exist here, named bare above because `doc-claims.sh` reads a backticked path
|
|
|
|
|
|
as a claim the file is present and this sentence says the opposite. The guard
|
|
|
|
|
|
missed it in `WORK_CYCLE.md` for the neighbouring reason: it reads backticked
|
|
|
|
|
|
prose, not fenced code blocks. `ClaudeReport.md`'s Round notes said "No rounds
|
|
|
|
|
|
yet" after three rounds, because `ClaudeQAPlan.md`'s after-a-round list never mentioned that
|
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
|
|
|
|
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 15:57:44 -05:00
|
|
|
|
### 2026-08-18 — Renamed to Privacy: Period Tracker
|
|
|
|
|
|
|
|
|
|
|
|
"Period" was always a working name — `PRODUCT_PLAN.md` §55 said so — and the real
|
|
|
|
|
|
identity arrived with the brand guide. Everything that called the project Period
|
|
|
|
|
|
now calls it **Privacy: Period Tracker**.
|
|
|
|
|
|
|
|
|
|
|
|
**The rename was mostly about deciding what NOT to touch.** "Period" is the
|
|
|
|
|
|
product name and the central domain word at the same time. `PeriodRecord`,
|
|
|
|
|
|
`PeriodWriteResult`, `confirmPeriodStart`, the `period_records` table and the
|
|
|
|
|
|
button reading "Started period" all describe a menstrual period rather than a
|
|
|
|
|
|
product — 239 references, every one of them left alone. A find-and-replace would
|
|
|
|
|
|
have turned the data model into nonsense. Sixteen files changed in total.
|
|
|
|
|
|
|
|
|
|
|
|
Also deliberately unmoved, and written down so the absence reads as a decision:
|
|
|
|
|
|
|
|
|
|
|
|
- **The package `dev.privacyllc.period` and `applicationId`.** They already read
|
|
|
|
|
|
correctly under the new name — `privacyllc` is the company, `period` the app —
|
|
|
|
|
|
and changing them would rewrite 68 files, rename the Room schema directory and
|
|
|
|
|
|
break the hardcoded path in `schema-guard.sh`.
|
|
|
|
|
|
- **`period.db`.** Renaming a database file orphans the data on every device
|
|
|
|
|
|
that already has it. The emulator's existing install opened fine afterwards,
|
|
|
|
|
|
which is the proof.
|
|
|
|
|
|
|
|
|
|
|
|
**The repository was renamed in place**, not recreated: `null/Period` →
|
|
|
|
|
|
`null/Privacy-Period-Tracker`, keeping all 30 commits, 27 issues, 8 milestones
|
|
|
|
|
|
and 4 labels. Counted on both sides rather than assumed, because "a rename is
|
|
|
|
|
|
lossless" is exactly the sort of thing that is true until it is not.
|
|
|
|
|
|
|
|
|
|
|
|
**Three names have to agree**, and only two of them are obvious: the Forgejo
|
|
|
|
|
|
repository, the git remote, and the Command Center's `forgejoRepo` mapping. The
|
|
|
|
|
|
third is the silent one — a stale mapping degrades to *"pointing at a repository
|
|
|
|
|
|
that does not exist"* on the next reconcile and the site quietly stops counting.
|
|
|
|
|
|
All three verified, and the reconcile afterwards reported `0 pointing at a
|
|
|
|
|
|
repository that does not exist`.
|
|
|
|
|
|
|
|
|
|
|
|
**The Command Center slug could not be renamed.** `PATCH` accepted a `slug` field
|
|
|
|
|
|
and ignored it; `period` still resolves and `privacy-period-tracker` returns 422.
|
|
|
|
|
|
It is an internal key, invisible on the card, and recreating the project to
|
|
|
|
|
|
change it would throw away its history — so it stays, recorded here rather than
|
|
|
|
|
|
retried later by somebody who assumes it was missed.
|
|
|
|
|
|
|
|
|
|
|
|
**One inconsistency created on purpose.** The canonical name has a space after
|
|
|
|
|
|
the colon; the supplied wordmark does not. `BRAND_GUIDE.md` §10 now says the
|
|
|
|
|
|
space form and says plainly that the drawn mark disagrees. Filed as #28 rather
|
|
|
|
|
|
than papered over, because redrawing a lockup needs an artist.
|
|
|
|
|
|
|
|
|
|
|
|
- **Closed:** nothing — this is a rename, not a deliverable. #28 filed.
|
|
|
|
|
|
- **Next action:** Batch 06 — Privacy and Security, unchanged by the rename. 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.
|
|
|
|
|
|
- **Blockers:** #9, the Command Center webhook, still needs a person. Its body
|
|
|
|
|
|
named the old repository path and has been corrected in the tracker, so
|
|
|
|
|
|
whoever picks it up gets a command that works. The four QA gaps are unchanged
|
|
|
|
|
|
and still the better spend: nobody has looked at a real lock screen, TalkBack has never been run,
|
|
|
|
|
|
text has never been scaled, and nothing has run at `minSdk` 26.
|
|
|
|
|
|
|
2026-08-18 15:33:04 -05:00
|
|
|
|
### 2026-08-18 — Batch 05 shipped, and the app got its real brand
|
|
|
|
|
|
|
|
|
|
|
|
Reminders landed and the milestone closed. **Five of eight batches are done, and
|
|
|
|
|
|
exactly one issue is open in the whole tracker** — #9, the webhook, which needs a
|
|
|
|
|
|
person.
|
|
|
|
|
|
|
|
|
|
|
|
**Notifications, and the two Android behaviours that leak if you trust the
|
|
|
|
|
|
docs.** `NotificationCopy` is a pure function, so every kind × every mode is
|
|
|
|
|
|
tested exhaustively without an emulator — including the action labels, which §31
|
|
|
|
|
|
points out are visible text too. A perfectly discreet body under a button
|
|
|
|
|
|
reading "Started my period" leaks anyway.
|
|
|
|
|
|
|
|
|
|
|
|
Then the device found what the unit tests could not:
|
|
|
|
|
|
|
|
|
|
|
|
1. **A private notification with no public version does not blank the lock
|
|
|
|
|
|
screen — it shows the private text.** `NotificationText` therefore has no
|
|
|
|
|
|
nullable title, and an instrumented test asserts every kind attaches one.
|
|
|
|
|
|
2. **A notification channel is immutable after creation.** Importance and
|
|
|
|
|
|
lock-screen visibility cannot be changed, so one shared channel would have
|
|
|
|
|
|
kept whatever the user's first privacy mode set, forever. Switching from
|
|
|
|
|
|
Direct to Maximum privacy would have *appeared* to work and changed nothing.
|
|
|
|
|
|
One channel per mode now.
|
|
|
|
|
|
|
|
|
|
|
|
**A third guard failed its first proof.** `checkPermissions` — new, and worth
|
|
|
|
|
|
having, because adding WorkManager silently brought four permissions that appear
|
|
|
|
|
|
in the Play listing — read a stale merged manifest because it did not depend on
|
|
|
|
|
|
the task that writes one. An injected `SCHEDULE_EXACT_ALARM` went unnoticed.
|
|
|
|
|
|
That is the schema guard, the boundary guard and now the permission guard: three
|
|
|
|
|
|
for three. **Assume a new guard is broken until it has been watched failing.**
|
|
|
|
|
|
|
|
|
|
|
|
**And a mistake of mine worth keeping.** While proving the pre-commit fix I
|
|
|
|
|
|
committed a throwaway and ran `git reset --hard HEAD~1`, which did not undo the
|
|
|
|
|
|
push the post-commit hook had already made and silently discarded the
|
|
|
|
|
|
uncommitted fix I was mid-proof on. Reconciled forward rather than rewriting
|
|
|
|
|
|
published history. Commit the work before proving the guard.
|
|
|
|
|
|
|
|
|
|
|
|
**The brand arrived mid-session.** The owner supplied a full visual guide and
|
|
|
|
|
|
artwork, which answers #8 — the three marks an agent must not fake now exist
|
|
|
|
|
|
because a person made them. Converted to the required webp with two things
|
|
|
|
|
|
handled rather than blindly transcoded: the sources render their rounded corners
|
|
|
|
|
|
against black, and the lockup is square so a 3:1 crop would have cut the wordmark
|
|
|
|
|
|
in half.
|
|
|
|
|
|
|
|
|
|
|
|
The guide **supersedes §39's colour direction**, and one conflict is real: §39
|
|
|
|
|
|
wants a muted teal fertile window and the guide is plum-and-rose throughout with
|
|
|
|
|
|
a pink for fertility. The guide wins, and it is safe for it to win *because the
|
|
|
|
|
|
calendar's states differ in shape rather than colour* — which is what §43
|
|
|
|
|
|
actually requires. Written down in `docs/design/README.md` rather than left to be
|
|
|
|
|
|
found.
|
|
|
|
|
|
|
|
|
|
|
|
- **Closed:** #8, #24, #25, #26, #27 — and the `Batch 05 — Notifications`
|
|
|
|
|
|
milestone.
|
|
|
|
|
|
- **Next action:** Batch 06 — Privacy and Security. File its issues first; the
|
|
|
|
|
|
milestone is empty. The work is biometric and PIN lock, export, irreversible
|
|
|
|
|
|
delete, the privacy promise in Settings, and making §45's logging rules true in
|
|
|
|
|
|
a release build. Much of the groundwork exists — `deleteAllHealthData` is
|
|
|
|
|
|
written and tested, backup is already excluded in the manifest — so this batch
|
|
|
|
|
|
is largely surfacing what the data layer can already do, plus the designed
|
|
|
|
|
|
Settings screen the Batch 05 working surface is standing in for.
|
|
|
|
|
|
- **Blockers:** One issue, and it needs a person: #9, the Command Center webhook.
|
|
|
|
|
|
Until it is registered an opened `P0` raises no alert at all. The QA gaps are
|
|
|
|
|
|
now the more pressing thing: **nobody has looked at an actual lock screen**,
|
|
|
|
|
|
TalkBack has never been run, text has never been scaled, and nothing has run
|
|
|
|
|
|
at `minSdk` 26. All four are cheap and none has ever been done.
|
|
|
|
|
|
|
2026-08-18 15:01:28 -05:00
|
|
|
|
### 2026-08-18 — Batch 04 shipped: fertility that declines, and a hook that refused every deletion
|
|
|
|
|
|
|
|
|
|
|
|
Three issues closed and the milestone with them. Four of the eight batches are
|
|
|
|
|
|
now done.
|
|
|
|
|
|
|
|
|
|
|
|
**Fertility, and the decision that made it worth shipping.** Ovulation is
|
|
|
|
|
|
estimated a luteal phase before the *predicted* next period — §17 asks for that
|
|
|
|
|
|
direction because the luteal phase is the stable half of the cycle — and the
|
|
|
|
|
|
window opens five days before it and closes one after. The uncertainty is
|
|
|
|
|
|
inherited from the forecast rather than invented.
|
|
|
|
|
|
|
|
|
|
|
|
The first version was arithmetically correct and useless. A user one cycle into
|
|
|
|
|
|
the app was shown a fertile window of **8 Aug – 24 Aug**: seventeen days, over
|
|
|
|
|
|
half a cycle, dressed up as a feature. So the estimate now returns null past a
|
|
|
|
|
|
usable uncertainty and the screen says *"Not enough history to estimate"* with a
|
|
|
|
|
|
reason to keep logging. Three stable cycles later the same user gets 12 Aug – 20
|
|
|
|
|
|
Aug, which is worth reading. Both states were checked on a device.
|
|
|
|
|
|
|
|
|
|
|
|
That is the third instance of one pattern, so it is now written into
|
|
|
|
|
|
`docs/architecture/README.md` as a rule rather than a coincidence: **the app
|
|
|
|
|
|
declines rather than stretches.** Accuracy figures wait for three scored
|
|
|
|
|
|
forecasts; averages wait for two intervals; fertility waits for a forecast tight
|
|
|
|
|
|
enough to hang off.
|
|
|
|
|
|
|
|
|
|
|
|
**§18's prohibition is a type.** `FertilityLikelihood` has LOWER, HIGHER and
|
|
|
|
|
|
UNKNOWN and no fourth value, and a test asserts no label contains a permission
|
|
|
|
|
|
word — so adding "safe" is a deliberate act with a failing test rather than a
|
|
|
|
|
|
slip in a string.
|
|
|
|
|
|
|
|
|
|
|
|
**Another greyscale collision.** The ovulation star was centred and sat directly
|
|
|
|
|
|
behind the numeral; in greyscale the "18" and the mark merged. Ovulation is now
|
|
|
|
|
|
the fertile ring plus a small star low in the cell, which is also the more honest
|
|
|
|
|
|
picture — that day *is* inside the window.
|
|
|
|
|
|
|
|
|
|
|
|
**The hook refused every deletion-only commit.** `secrets.sh` exits 2 for
|
|
|
|
|
|
"nothing was scanned", which is the correct answer for a commit that only
|
|
|
|
|
|
deletes files. The hook treated any non-zero as a refusal and printed *"possible
|
|
|
|
|
|
credential in the staged changes"* over a plain `git rm`. Found while removing a
|
|
|
|
|
|
`bin/` directory Buildship had been writing — a directory full of `.kt` files,
|
|
|
|
|
|
which is the one kind of build output that does not look like build output in a
|
|
|
|
|
|
`git status`. It only got committed at all because a `.gitignore` edit was
|
|
|
|
|
|
staged alongside, which gave the scanner something to read.
|
|
|
|
|
|
|
|
|
|
|
|
**And a mistake worth recording rather than tidying away.** While proving the
|
|
|
|
|
|
fix, I committed a throwaway, then ran `git reset --hard HEAD~1` — which did not
|
|
|
|
|
|
undo the push the post-commit hook had already made, and silently discarded the
|
|
|
|
|
|
uncommitted hook fix I was in the middle of proving. The second proof then
|
|
|
|
|
|
failed for a reason that had nothing to do with the guard. Reconciled forward
|
|
|
|
|
|
rather than force-pushed. **Commit the work before proving the guard, not after.**
|
|
|
|
|
|
|
|
|
|
|
|
- **Closed:** #21, #22, #23 — and the `Batch 04 — Fertility` milestone.
|
|
|
|
|
|
- **Next action:** Batch 05 — Notifications. File its issues first; the milestone
|
|
|
|
|
|
is empty. The work is WorkManager-scheduled reminders (§29, §30) with the three
|
|
|
|
|
|
privacy modes from §28 — and the mode that matters is Discreet, already the
|
|
|
|
|
|
stored default and already chosen in onboarding, so this batch is about the
|
|
|
|
|
|
lock screen actually honouring it. **Pass F cannot be run until this lands**,
|
|
|
|
|
|
and it is the pass that checks the most likely real privacy breach in this
|
|
|
|
|
|
product.
|
|
|
|
|
|
- **Blockers:** Unchanged, both needing a person: the three branding marks (#8)
|
|
|
|
|
|
and the Command Center webhook (#9). The three standing QA gaps are also still
|
|
|
|
|
|
open and are getting more expensive the more UI exists — TalkBack has never
|
|
|
|
|
|
been run, text has never been scaled, and nothing has run at `minSdk` 26.
|
|
|
|
|
|
|
2026-08-18 04:05:58 -05:00
|
|
|
|
### 2026-08-18 — Batch 03 shipped: every screen the app has, and three defects only looking could find
|
|
|
|
|
|
|
|
|
|
|
|
All six Batch 03 issues closed and the milestone with them. Period stopped being
|
|
|
|
|
|
a data layer with a debug surface and became something you can actually use:
|
|
|
|
|
|
onboarding that ends in a forecast, a Today screen whose six states each say
|
|
|
|
|
|
something different and true, two-tap logging, a calendar, and Insights.
|
|
|
|
|
|
|
|
|
|
|
|
**Placeholder artwork, as §42 asks for it.** Every illustration and calendar
|
|
|
|
|
|
marker is a Compose vector path behind a replaceable name — no raster anywhere
|
|
|
|
|
|
under `src/`. The visual language is overlapping circular forms and nothing
|
|
|
|
|
|
else, because §42's forbidden list is a product decision: this app gets opened
|
|
|
|
|
|
in public and a glance over a shoulder should learn nothing. Note the deliberate
|
|
|
|
|
|
contrast with `docs/data/img/`, where a placeholder is still forbidden and #8
|
|
|
|
|
|
stays open; `docs/design/README.md` now explains why the two differ rather than
|
|
|
|
|
|
leaving it looking inconsistent.
|
|
|
|
|
|
|
|
|
|
|
|
**Three defects, none findable by reading:**
|
|
|
|
|
|
|
|
|
|
|
|
1. **Dark mode was broken for the whole of Batch 01.** `PeriodTheme` never
|
|
|
|
|
|
wrapped its content in a `Surface`, so any `Text` without an explicit colour
|
|
|
|
|
|
inherited Material's default — black — and the app's background never
|
|
|
|
|
|
painted. Light mode looked right by accident, because dark-on-cream is what
|
|
|
|
|
|
was wanted anyway. 129 tests were green throughout.
|
|
|
|
|
|
2. **"Period ended" appeared to do nothing.** The logic was correct: a period
|
|
|
|
|
|
that ends today still includes today, so the state genuinely does not change.
|
|
|
|
|
|
The screen was identical afterwards and the button read as broken, which is
|
|
|
|
|
|
worse than being broken somewhere visible.
|
|
|
|
|
|
3. **The today-underline collided with the spotting dot** on the one day that
|
|
|
|
|
|
was both. Invisible in the colour screenshot and obvious the moment it was
|
|
|
|
|
|
converted to greyscale — which is how §43 says to check, and why the check is
|
|
|
|
|
|
written that way rather than trusting that shapes differ because they were
|
|
|
|
|
|
designed to.
|
|
|
|
|
|
|
|
|
|
|
|
Two more came from tests doing their job: the typical-range quartiles were
|
|
|
|
|
|
indexed off `size` rather than `size - 1`, so a regular cycle with one long gap
|
|
|
|
|
|
was reported as "typical range 29–61 days" on the screen whose only job is to
|
|
|
|
|
|
say what has been learned; and Room's own executor meant a test's virtual clock
|
|
|
|
|
|
returned before the writes landed, which read as an empty database rather than
|
|
|
|
|
|
as a timing bug.
|
|
|
|
|
|
|
|
|
|
|
|
**The habit is now five pieces of work old and worth writing down as a rule: the
|
|
|
|
|
|
defects in this project are found by running it.** Three guards were green over
|
|
|
|
|
|
exactly what they claimed to check, dark mode was broken for a whole batch, and
|
|
|
|
|
|
a button looked broken while behaving correctly. None of it was visible in the
|
|
|
|
|
|
source.
|
|
|
|
|
|
|
|
|
|
|
|
- **Closed:** #15, #16, #17, #18, #19, #20 — and the `Batch 03 — Core UX`
|
|
|
|
|
|
milestone.
|
|
|
|
|
|
- **Next action:** Batch 04 — Fertility. File its issues first; the milestone is
|
|
|
|
|
|
empty. The work is estimated ovulation and the fertile window from the
|
|
|
|
|
|
forecast and a configurable luteal-phase assumption (§17, §18), the calendar
|
|
|
|
|
|
and Today states that already have their shapes and their `null` placeholders
|
|
|
|
|
|
waiting, and the not-contraception disclaimer everywhere fertility appears.
|
|
|
|
|
|
`CycleStatus` and `CalendarMarks` both already take fertility parameters and
|
|
|
|
|
|
currently receive null — Batch 04 is largely filling those in rather than
|
|
|
|
|
|
adding surfaces.
|
|
|
|
|
|
- **Blockers:** Unchanged. #8 needs the three branding marks drawn by a person.
|
|
|
|
|
|
#9 needs the Command Center webhook URL and secret, which are not readable
|
|
|
|
|
|
from this machine. Neither blocks Batch 04. Worth doing before much more UI:
|
|
|
|
|
|
run TalkBack once, scale the font to maximum once, and run the app once on a
|
|
|
|
|
|
device at `minSdk` 26 — all three are recorded as standing gaps and none has
|
|
|
|
|
|
ever been done.
|
|
|
|
|
|
|
2026-08-18 03:17:00 -05:00
|
|
|
|
### 2026-08-18 — Batch 02 shipped: the prediction engine, and three bugs the tests found
|
|
|
|
|
|
|
|
|
|
|
|
All five Batch 02 issues closed and the milestone closed with them. The app now
|
|
|
|
|
|
ships the engine PRODUCT_PLAN.md §12 specifies rather than the prototype it was
|
|
|
|
|
|
explicitly labelled as.
|
|
|
|
|
|
|
|
|
|
|
|
**The shape is the design.** `PersonalPredictionEngine` keeps a discrete
|
|
|
|
|
|
probability distribution over candidate start dates, not a date with a margin.
|
|
|
|
|
|
The mode is the forecast, the window is the narrowest span holding 80% of the
|
|
|
|
|
|
mass, and a "Not yet" is the distribution conditioned on what the user said.
|
|
|
|
|
|
§13's requirement — that a "Not yet" updates the date, the window *and* the
|
|
|
|
|
|
confidence rather than shifting a fixed prediction by a day — is not extra work
|
|
|
|
|
|
in that design; it is the only thing that structure can do.
|
|
|
|
|
|
|
|
|
|
|
|
**It is better, as a number.** `EngineComparisonTest` scores both engines over
|
|
|
|
|
|
the §51 fixtures on every build: mean absolute error 0.67 against 1.00, and the
|
|
|
|
|
|
window contained the actual start 9 times out of 9 against 7.
|
|
|
|
|
|
|
|
|
|
|
|
**Coverage is the measure, not width — and measuring taught that.** The first
|
|
|
|
|
|
version of the comparison asserted the new windows must not be wider. It failed,
|
|
|
|
|
|
and it was the assertion that was wrong: where the personal engine is wider, it
|
|
|
|
|
|
is right to be, and the baseline answers a history with a suspected missing
|
|
|
|
|
|
period using a two-day window and misses. A window that misses is a broken
|
|
|
|
|
|
promise rather than a tight forecast.
|
|
|
|
|
|
|
|
|
|
|
|
**Three modelling bugs, each found by a test failing rather than by reading:**
|
|
|
|
|
|
|
|
|
|
|
|
1. Median absolute deviation alone reads a user alternating 25 and 37 days as
|
|
|
|
|
|
perfectly consistent — half her deviations are zero. Twenty disagreeing
|
|
|
|
|
|
cycles came back High, which is exactly the §15 rule about volume not buying
|
|
|
|
|
|
confidence. Spread is now the larger of MAD and mean absolute deviation.
|
|
|
|
|
|
2. Recency weighting assumes the recent past predicts the near future. For a
|
|
|
|
|
|
variable user that is false, and weighting it equally cost three days on the
|
|
|
|
|
|
§51 variable fixture. Recency is now trusted in proportion to how much her
|
|
|
|
|
|
cycles agree — which is a better statement of what recency weighting is
|
|
|
|
|
|
actually for.
|
|
|
|
|
|
3. A fixed one-day floor on trend detection fired on a 42-day-cycle history
|
|
|
|
|
|
whose medians differed by one day. One day is a real trend at 28 and rounding
|
|
|
|
|
|
error at 42, so the floor is relative to the user's own spread.
|
|
|
|
|
|
|
|
|
|
|
|
That is now three consecutive pieces of work — the schema guard, the boundary
|
|
|
|
|
|
guard, and the engine — where the thing that found the defect was **running it,
|
|
|
|
|
|
not reading it**. Worth stating as a habit rather than a coincidence.
|
|
|
|
|
|
|
|
|
|
|
|
**Wired through, not just tested.** `PredictionInput` carries recent absolute
|
|
|
|
|
|
errors and the repository feeds scored errors back in. Without that the app
|
|
|
|
|
|
would store every error it makes and never read one back — measuring accuracy
|
|
|
|
|
|
rather than learning from it, with §12 step 5's widening happening only in a
|
|
|
|
|
|
unit test.
|
|
|
|
|
|
|
|
|
|
|
|
- **Closed:** #10, #11, #12, #13, #14 — and the `Batch 02 — Prediction Engine`
|
|
|
|
|
|
milestone, which closing the last issue does not do.
|
|
|
|
|
|
- **Next action:** Batch 03 — Core UX. The onboarding flow in §19, the designed
|
|
|
|
|
|
Today screen and its six dynamic states in §21–§22, period start and end
|
|
|
|
|
|
logging in §23–§24, the calendar in §26 and Insights in §27. The working
|
|
|
|
|
|
surface built in Batch 01 is deliberately ugly and says so on screen; it is
|
|
|
|
|
|
the thing Batch 03 replaces. Before starting, file the Batch 03 issues — the
|
|
|
|
|
|
milestone exists and is empty.
|
|
|
|
|
|
- **Blockers:** Unchanged and both need a person. #8, the three branding marks,
|
|
|
|
|
|
which an agent must not fake. #9, the webhook, whose URL and secret are not
|
|
|
|
|
|
readable from this machine — until it is registered an opened `P0` raises no
|
|
|
|
|
|
alert at all. Neither blocks Batch 03.
|
|
|
|
|
|
|
2026-08-18 03:01:33 -05:00
|
|
|
|
### 2026-08-18 — Batch 01 foundation: seven of nine issues, and three guards that were wrong
|
|
|
|
|
|
|
|
|
|
|
|
Room, DataStore, the repository layer, period CRUD end to end on a device, and
|
|
|
|
|
|
the module boundary guard. #3 to #7 closed; #8 (branding) and #9 (webhook) are
|
|
|
|
|
|
the two that need a person rather than an agent.
|
|
|
|
|
|
|
|
|
|
|
|
**What was built.** `core/database` with the four entities from §10, DAOs
|
|
|
|
|
|
returning `Flow`, and the schema exported and committed. `core/datastore` for
|
|
|
|
|
|
settings, deliberately separate from the cycle database so Delete My Data cannot
|
|
|
|
|
|
reset a privacy choice the user made. `core/data` as the seam: domain types out,
|
|
|
|
|
|
cycles derived rather than stored, the forecast a function of the data instead
|
|
|
|
|
|
of a field somebody has to refresh. Hilt wiring and a working Today surface that
|
|
|
|
|
|
says "Batch 01 · working surface" so nobody mistakes it for the designed screen.
|
|
|
|
|
|
|
|
|
|
|
|
**Three guards were written, and all three were wrong at first.** This is the
|
|
|
|
|
|
day's real lesson and it is worth carrying forward:
|
|
|
|
|
|
|
|
|
|
|
|
1. `SchemaTest` looked like a Room schema-drift guard. Room regenerates the
|
|
|
|
|
|
schema export during compilation, so both sides of every comparison agreed by
|
|
|
|
|
|
construction — adding a column without bumping the version left it green.
|
|
|
|
|
|
`scripts/schema-guard.sh` asks git instead, which Room cannot overwrite.
|
|
|
|
|
|
2. `checkModuleBoundaries` reported "7 modules checked, no violations" while
|
|
|
|
|
|
checking nothing: the root project is configured before its subprojects, so
|
|
|
|
|
|
every configuration read as empty. Caught by `prove-guard.sh` on its first
|
|
|
|
|
|
run. Collection moved to `afterEvaluate`, and the task now throws rather than
|
|
|
|
|
|
passing when it examined nothing.
|
|
|
|
|
|
3. The repository let `SQLiteConstraintException` escape into
|
|
|
|
|
|
`viewModelScope.launch`, so **tapping the primary button twice killed the
|
|
|
|
|
|
app** — found by hand on an emulator, with 70 unit tests green.
|
|
|
|
|
|
|
|
|
|
|
|
Each was caught by actually trying to break it. None would have been caught by
|
|
|
|
|
|
reading the code, and two of them would have been trusted for months.
|
|
|
|
|
|
|
|
|
|
|
|
**Two more bugs came from wiring the guard into `./gradlew check`**, which ran
|
|
|
|
|
|
Android lint for the first time: `LocalDate.ofInstant` and `LocalDate.EPOCH` are
|
|
|
|
|
|
API 34 and `minSdk` is 26. Both sit on the recalculation path — a crash on every
|
|
|
|
|
|
device below Android 14, invisible to the unit tests and to an API 36 emulator.
|
|
|
|
|
|
|
|
|
|
|
|
**QA.** Round 1 recorded as partial in `docs/qa/`. Passes A and B green, C and D
|
|
|
|
|
|
and H partial, the rest not run and each saying why.
|
|
|
|
|
|
|
|
|
|
|
|
- **Closed:** #3, #4, #5, #6, #7
|
|
|
|
|
|
- **Next action:** Batch 02 — replace `BaselinePredictionEngine` with the engine
|
|
|
|
|
|
§12 specifies: recency weighting, a robust centre, variability-driven windows,
|
|
|
|
|
|
trend detection, and "not yet" as a real conditioning step rather than a floor
|
|
|
|
|
|
on the window. The §51 acceptance tests already exist and must keep passing
|
|
|
|
|
|
against the new engine, which is what makes the replacement demonstrably
|
|
|
|
|
|
better rather than merely different. Before that, one cheap thing worth doing:
|
|
|
|
|
|
run the app once on a device at `minSdk` 26, because nothing here ever has.
|
|
|
|
|
|
- **Blockers:** None for code. #8 needs the three branding marks drawn — an
|
|
|
|
|
|
agent must not fake them, so the project card shows an initials tile until
|
|
|
|
|
|
somebody does. #9 needs the Command Center's webhook URL and secret, which are
|
|
|
|
|
|
not readable from this machine; until it is registered an opened `P0` raises
|
|
|
|
|
|
no alert at all.
|
|
|
|
|
|
|
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
|
|
|
|
### 2026-08-18 — Template adopted; Kotlin/Compose skeleton builds
|
|
|
|
|
|
|
|
|
|
|
|
Period went from a bare directory holding one specification file to a git
|
|
|
|
|
|
repository with the standard documentation tree, a tracker, and a project that
|
|
|
|
|
|
compiles. Adoption followed `Projects/Template/START-HERE-New-Project.md`.
|
|
|
|
|
|
|
|
|
|
|
|
**Documents.** `scaffold.sh` created 19 paths, 0 skipped. The specification moved
|
|
|
|
|
|
from `Docs/period_tracker_product_plan.md` to `docs/planning/PRODUCT_PLAN.md`
|
|
|
|
|
|
unchanged in substance, with a status header added; the capitalised `Docs/` is
|
|
|
|
|
|
gone, since every script and the Command Center expect the lowercase tree. Every
|
|
|
|
|
|
scaffolded document was filled in for Period rather than left with placeholders.
|
2026-08-18 02:18:25 -05:00
|
|
|
|
docs/OPERATIONS.md was deleted — an offline app is not a deployed service.
|
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
|
|
|
|
`docs/DOC_TRUST_MAP.md` was written last and describes what is actually here,
|
|
|
|
|
|
including a section naming what this project deliberately does **not** have.
|
|
|
|
|
|
|
|
|
|
|
|
**Code.** Four Gradle modules: `app`, `core/designsystem`, and `domain/cycle`
|
|
|
|
|
|
and `domain/prediction` as `kotlin("jvm")` 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 is explicitly
|
|
|
|
|
|
not the product — it exists so Batch 02's replacement can be shown to be better
|
|
|
|
|
|
rather than merely different.
|
|
|
|
|
|
|
|
|
|
|
|
**Three things that cost time and are worth knowing next session:**
|
|
|
|
|
|
|
|
|
|
|
|
- **AGP 9 ships Kotlin built in.** Applying `org.jetbrains.kotlin.android` is now
|
|
|
|
|
|
a hard error, not a redundancy. The Compose compiler plugin is still separate.
|
|
|
|
|
|
- **Current AndroidX requires `compileSdk 37`.** Only up to 36 was installed;
|
|
|
|
|
|
`platforms;android-37.0` and `build-tools;37.0.0` were installed into
|
|
|
|
|
|
`~/Android/Sdk`. `targetSdk` stays at 36 — Play's floor from 2026-08-31 — and
|
|
|
|
|
|
the two being different is deliberate, not an oversight to tidy up.
|
|
|
|
|
|
- **Versions were verified, not inherited.** 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 — each checked against
|
|
|
|
|
|
its official source today, which `PRODUCT_PLAN.md` asks for rather than
|
|
|
|
|
|
trusting its own numbers.
|
|
|
|
|
|
|
|
|
|
|
|
**Tracker.** Eight milestones opened, `Batch 01 — Foundation` through
|
|
|
|
|
|
`Batch 08 — Polish`, and nine issues filed under Batch 01 only. Seven milestones
|
|
|
|
|
|
are deliberately empty: the roadmap is genuinely known and worth being visible,
|
|
|
|
|
|
but the work items under it are not, and inventing them would make every tracker
|
|
|
|
|
|
percentage permanently wrong. `forgejo-issue.py check` warns about this, and the
|
|
|
|
|
|
warning is correct about the mechanism and expected here.
|
|
|
|
|
|
|
|
|
|
|
|
- **Closed:** #1, #2
|
2026-08-18 02:18:25 -05:00
|
|
|
|
- **Next action:** Start issue #3 — core/database with Room entities for
|
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
|
|
|
|
`PeriodRecord`, `SpottingRecord`, `PredictionRecord` and `NotYetObservation`,
|
|
|
|
|
|
DAOs returning `Flow`, schema export committed, and a version-1 migration test
|
|
|
|
|
|
that proves the harness works before there is a migration that matters. Its row
|
|
|
|
|
|
goes in `docs/architecture/README.md`'s migration table in the same commit.
|
|
|
|
|
|
- **Blockers:** None for the code. Two things need a person rather than an agent:
|
|
|
|
|
|
the three branding marks (#8), which cannot be drawn here and must not be
|
|
|
|
|
|
faked, and the Command Center webhook (#9), whose URL and secret are not in any
|
|
|
|
|
|
credential file readable from this machine. Without the webhook an opened `P0`
|
|
|
|
|
|
raises no alert at all — it waits for the next reconcile.
|