Restore from an encrypted export #58
Labels
No Label
P0
P1
P2
release-blocker
No Milestone
No project
No Assignees
1 Participants
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: null/Privacy-Period-Tracker#58
Loading…
Reference in New Issue
No description provided.
Delete Branch "%!s(<nil>)"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What is true now.
DataExporterwrites an encrypted archive and nothing in the tree can read one back. A user who changes phone starts the prediction model from zero, and the export — offered as the thing that makes her history hers — is a one-way door.What to do. A parse counterpart in
core/exportmirroringExportDocument.render, and an import flow beside the export screen writing throughCycleRepository. The user chooses merge or replace explicitly; the app must not guess. Prediction recalculates once after the import completes, not per record.Traps. Importing must go through
confirmPeriodStartand friends rather than touching DAOs, or the forecast-lineage and scoring invariants inCycleRepositoryare bypassed silently. Every imported start is historical, so the backfill guard should refuse to score any of it — an import must not manufacture accuracy figures. Same no-logging discipline the exporter documents (§45): nothing about the contents in a log, an exception message, or a crash report.Verify: a round-trip unit test in
core/export— render then parse reproduces everyPeriodRecordandSpottingRecordfield — plus a repository test asserting an import scores nothing and triggers exactly one recalculation.Landed in
0ae92bd.The parse counterpart.
core/export/ExportReader.ktover a strict hand-writtenJson.kt, mirroringExportDocument.renderin the same module.ExportRoundTripTest(17 tests) — render→parse reproduces everyPeriodRecordandSpottingRecordfield including the open period, the wholeExportedSettingsby equality, and the committedgolden-v1.jsonstill reads. Refusals are typed:NotOurFile,NewerFormat(v),Damaged; unknown keys ignored (rule 2), unknownsourcebecomesIMPORTEDrather than a guess.The traps, each held by a named test. Importing goes through
CycleRepository.importHistory, not a DAO — and deliberately not throughconfirmPeriodStart, whose scoring, not-yet clearing and per-record snapshot are all wrong for an archive.CycleRepositoryImportTest:an import scores nothing(with an archive dated today, the case the backfill guard alone does not catch),the forecast is recalculated exactly once, however long the archive(counting engine, 4 records → 1 call),an import leaves exactly one standing forecast,every imported record says it was imported.Merge vs replace is explicit.
ImportMode.MERGE|REPLACE, no default. Replace runs through the newPeriodDatabase.replaceEverything, which empties the tables inside the same transaction as the writes —replace cannot leave the user with neither historyproves it, and prove-guard confirms it: removingwithTransactionfromreplaceEverythingreddens exactly that one test.§45.
./gradlew checkgreen includingcheckNoHealthLogging; no failure carries a cause.DataImporterTest.an archive cannot change the app lockholds the one setting a file must never move.On PeriodQA (emulator-5554, debug build of
0ae92bd): Settings → Restore from a file → Add to what is here → Downloads → records-2026-08-19.json → banner reads "Restored. 4 periods and 2 spotting days added."; Insights then shows 3 cycles (29/29/57) with accuracy still unscored; the same file a second time reads "Everything in that file was already recorded here, so nothing changed."uiautomator dumptranscripts, not screenshots (FLAG_SECURE).One correction to this issue's own text: the export is not encrypted and says so in its own prose —
ExportCopy.PROTECTION_BODYand the archive'saboutThisFile. "Restore from an encrypted export" was wrong when filed; what shipped reads the plaintext archive the app actually writes.