Privacy-Period-Tracker/docs/history/DEVELOPMENT_LOG.md

12 KiB
Raw Blame History

Development log — Period

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 — 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 — 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.

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.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
  • Next action: Start issue #3 — core/database with Room entities for 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.