38 KiB
Development log — Privacy: Period Tracker
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
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.
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:
- 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-windowhas no such dependency and screenshots still work throughadb exec-out screencap. adbregularly has three devices attached here, because other projects keep their own emulators running, and a bareadb shellthen fails with more than one device/emulator. The serial has to be resolved from the AVD name; assumingemulator-5554targets somebody else's work.- The debug build's application id carries a
.debugsuffix, somonkey -p dev.privacyllc.periodaborts 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.mdrather 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.
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 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
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.mdnow 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
minSdk26.
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.periodandapplicationId. They already read correctly under the new name —privacyllcis the company,periodthe app — and changing them would rewrite 68 files, rename the Room schema directory and break the hardcoded path inschema-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
minSdk26.
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:
- A private notification with no public version does not blank the lock
screen — it shows the private text.
NotificationTexttherefore has no nullable title, and an instrumented test asserts every kind attaches one. - 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 — Notificationsmilestone. - 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 —
deleteAllHealthDatais 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
P0raises 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 atminSdk26. All four are cheap and none has ever been done.
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 — Fertilitymilestone. - 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
minSdk26.
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:
- Dark mode was broken for the whole of Batch 01.
PeriodThemenever wrapped its content in aSurface, so anyTextwithout 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. - "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.
- 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 UXmilestone. - 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
nullplaceholders waiting, and the not-contraception disclaimer everywhere fertility appears.CycleStatusandCalendarMarksboth 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
minSdk26 — all three are recorded as standing gaps and none has ever been done.
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:
- 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.
- 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.
- 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 Enginemilestone, 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
P0raises no alert at all. Neither blocks Batch 03.
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:
SchemaTestlooked 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.shasks git instead, which Room cannot overwrite.checkModuleBoundariesreported "7 modules checked, no violations" while checking nothing: the root project is configured before its subprojects, so every configuration read as empty. Caught byprove-guard.shon its first run. Collection moved toafterEvaluate, and the task now throws rather than passing when it examined nothing.- The repository let
SQLiteConstraintExceptionescape intoviewModelScope.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
BaselinePredictionEnginewith 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 atminSdk26, 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
P0raises no alert at all.
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.
docs/OPERATIONS.md was deleted — an offline app is not a deployed service.
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.androidis 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.0andbuild-tools;37.0.0were installed into~/Android/Sdk.targetSdkstays 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.mdasks 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
- Next action: Start issue #3 — core/database with Room entities for
PeriodRecord,SpottingRecord,PredictionRecordandNotYetObservation, DAOs returningFlow, schema export committed, and a version-1 migration test that proves the harness works before there is a migration that matters. Its row goes indocs/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
P0raises no alert at all — it waits for the next reconcile.