diff --git a/docs/history/DEVELOPMENT_LOG.md b/docs/history/DEVELOPMENT_LOG.md index 0795163..c97cfe8 100644 --- a/docs/history/DEVELOPMENT_LOG.md +++ b/docs/history/DEVELOPMENT_LOG.md @@ -32,6 +32,95 @@ written and stay true. It is exempt from review for the same reason a receipt is ## Entries +### 2026-08-21 — The export stops being a one-way door; the tracker is empty + +`#58` was the last open issue in the repository, and it closed the loop the +export left open: the app offered a copy of everything and nothing could read +one back. A user changing phone started the prediction model from zero, and the +archive's own prose — *"Keep this file. A future version of the app will be able +to read it back"* — was a promise nothing checked. + +**What shipped** (`0ae92bd`). `ExportReader` mirrors `ExportDocument.render` in +the same module, over a hand-written strict `Json.kt`: no library, because +`core/export` has two dependencies and their narrowness is the point, and strict +because a parser that accepts a trailing comma is a parser that will one day +accept somebody else's file as a cycle history. Refusals are typed — +`NotOurFile`, `NewerFormat(v)`, `Damaged` — and unknown *keys* are ignored, +which is rule 2's other half and the only reason the format can ever grow. + +`CycleRepository.importHistory` is a deliberate third write path rather than a +loop over `confirmPeriodStart`, and the reason is worth carrying forward: an +archive is history the app never forecast, so scoring any of it fills §16's +figures with numbers the app invented about itself. The backfill guard catches +most of that by date and **not all of it** — a file exported this morning +carries this morning's period, which would sail through. The test names that +case explicitly. + +**Two things learned the hard way, both recorded so nobody re-derives them:** + +- **A test premise can be wrong in the direction of passing.** The first + atomicity test fed the importer a file naming the same day twice, expecting + the UNIQUE index to abort mid-import. It never threw: the importer's own + dedupe skipped the duplicate, so the test was measuring a successful import. + Rewritten to fail the *last* step — the recalculation — via a counting engine + that can be told to throw. +- **A comment can be confidently wrong about a library.** The first version of + `replaceEverything`'s KDoc said `clearAllTables` "ends the transaction it is + called in", which sounded right and was not: nesting it under Robolectric + rolled back cleanly. What is actually true, checked against room-runtime 2.8.4 + bytecode rather than the documentation, is that `performClear` opens with + `assertNotSuspendingTransaction()` and runs its work through + `runBlockingUninterruptible` — a blocking coroutine of its own — and finishes + with a `VACUUM` SQLite forbids inside a transaction. So the function stays, on + a claim that can be checked, and Delete My Data keeps `deleteEverything` and + its VACUUM. Replacing a history is not erasing one. + +**Recorded decisions.** The archive cannot touch the app lock: it carries +`biometricUnlock` and the importer does not apply it, because there is no PIN in +the file and there cannot be. Merge leaves a date already recorded on this phone +exactly as it is rather than merging field by field — §14 forbids a file +rewriting a record she made here. And the choice between add and replace is +asked **before** the picker, not after the file is read where the app could have +said how much was in it: the screen that would ask is the screen a re-lock +destroys. + +**One correction to a thing filed yesterday.** `#58` was titled "Restore from an +encrypted export" and its body said `DataExporter` "writes an encrypted +archive". It does not, and never did — `ExportCopy.PROTECTION_BODY` and the +file's own `aboutThisFile` both say so in as many words. The issue was wrong when +filed; what shipped reads the plaintext archive the app actually writes. + +**Device evidence** (PeriodQA, emulator-5554, debug build of `0ae92bd`): Settings +→ Restore from a file → Add to what is here → Downloads → +`records-2026-08-19.json` gives *"Restored. 4 periods and 2 spotting days +added."*; Insights then shows three cycles with accuracy still unscored; the same +file a second time gives *"Everything in that file was already recorded here, so +nothing changed."* `uiautomator dump` rather than screenshots — FLAG_SECURE +returns black. + +**Also in this commit:** `LockSettingsViewModelTest`'s hang budget went from 60 +seconds to three minutes. It passed alone and failed when `:app`, `:core:data`, +`:core:export` and `:core:datastore` ran in one invocation — three real PBKDF2 +derivations at 210,000 iterations. The budget catches a stuck coroutine, not a +slow one, and a flaky guard is one people learn to ignore. + +- **Closed:** #58. `Batch 10 — Show what you already know` closed with it. + **Every issue in the tracker is now closed**, and Batches 09, 10, 11 and 12 + all landed since yesterday morning. +- **Next action:** A release, or the next batch — and the decision between them + is Kaspa's. What argues for cutting one now: no known defect is outstanding, + both P0 release-blockers found yesterday are fixed and verified on a device, + and `./gradlew check` is green including `checkNoHealthLogging` and + `checkPermissions`. What argues against: `SECURITY_CHECKLIST.md`'s device rows + have been *written* rather than *walked* for the biometric-only and reminder + paths, and nothing in this project has ever run on an API 26 device, which is + its own `minSdk`. Recommendation: walk the checklist on a 26 image and on + PeriodQA before the image is cut, then release. +- **Blockers:** Unchanged and both need a person, not an agent — #8's three + branding marks, which must not be faked, and #9's Command Center 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-20 (evening) — Two release-blockers, and the trap that was reported Kaspa reported one bug — *"on the settings page, if you go to app lock you can't