Forecast snapshots lose their lineage, so a retro-logged period teaches the app nothing #46

Closed
opened 2026-08-20 16:13:28 -05:00 by null · 0 comments
Owner

Found by tracing the confirm-score-feed-back loop end to end at 93ec5b7.

What is true now. CycleRepository.snapshotForecast stamps clock.instant() on every replacement snapshot. Every recordNotYet, editPeriod and deletePeriod therefore moves the standing forecast's generatedAt forward. scoreOutstanding measures its backfill guard from that date, so a period logged even one day retroactively after any of those actions trips the guard, is not scored, and is then deleted unscored by the snapshotForecast call that follows — the app collects no error sample from precisely the cycle it got wrong. Two related faults in the same method: the engine.predict(...) ?: return early-return skips deleteUnscored(), so deleting the last period leaves a stale unscored snapshot that a later confirm scores against an unrelated fresh history; and scoreOutstanding's KDoc claims a backfill-skipped forecast 'survives untouched', which the next statement contradicts.

What it costs. Silent, plausible-looking accuracy figures with missing samples — the failure mode §16 is least able to notice, because nothing looks broken.

What to do. Treat the standing unscored snapshot as a lineage: read unscored().firstOrNull()?.generatedAt before deleting, move deleteUnscored() above the engine call, and insert the replacement with the inherited origin (?: clock.instant()). Rewrite the two comments to match.

Traps. Simpler predicates ('score if this is the new latest start') cannot be told apart from onboarding's ascending backfill at a fixed clock and break the existing backfilled history does not fabricate accuracy figures test — that test is the tripwire; if a change reddens it, the change is wrong, not the test. Accepted trade-off to document: a retro-logged start scores the final revision of the forecast, so a user's own contradicting 'Not yet' inflates the error by at most that contradiction.

Verify: a period logged retroactively still scores the forecast the user was shown and deleting the last period retires the standing forecast in core/data/src/test/kotlin/dev/privacyllc/period/core/data/CycleRepositoryTest.kt — both red before the fix, green after.

Found by tracing the confirm-score-feed-back loop end to end at 93ec5b7. **What is true now.** `CycleRepository.snapshotForecast` stamps `clock.instant()` on every replacement snapshot. Every `recordNotYet`, `editPeriod` and `deletePeriod` therefore moves the standing forecast's `generatedAt` forward. `scoreOutstanding` measures its backfill guard from that date, so a period logged even one day retroactively after any of those actions trips the guard, is not scored, and is then deleted unscored by the `snapshotForecast` call that follows — the app collects no error sample from precisely the cycle it got wrong. Two related faults in the same method: the `engine.predict(...) ?: return` early-return skips `deleteUnscored()`, so deleting the last period leaves a stale unscored snapshot that a later confirm scores against an unrelated fresh history; and `scoreOutstanding`'s KDoc claims a backfill-skipped forecast 'survives untouched', which the next statement contradicts. **What it costs.** Silent, plausible-looking accuracy figures with missing samples — the failure mode §16 is least able to notice, because nothing looks broken. **What to do.** Treat the standing unscored snapshot as a lineage: read `unscored().firstOrNull()?.generatedAt` before deleting, move `deleteUnscored()` above the engine call, and insert the replacement with the inherited origin (`?: clock.instant()`). Rewrite the two comments to match. **Traps.** Simpler predicates ('score if this is the new latest start') cannot be told apart from onboarding's ascending backfill at a fixed clock and break the existing `backfilled history does not fabricate accuracy figures` test — that test is the tripwire; if a change reddens it, the change is wrong, not the test. Accepted trade-off to document: a retro-logged start scores the final revision of the forecast, so a user's own contradicting 'Not yet' inflates the error by at most that contradiction. Verify: `a period logged retroactively still scores the forecast the user was shown` and `deleting the last period retires the standing forecast` in core/data/src/test/kotlin/dev/privacyllc/period/core/data/CycleRepositoryTest.kt — both red before the fix, green after.
null added this to the Batch 09 — Prediction Science & Hardening milestone 2026-08-20 16:13:28 -05:00
null added the
P1
label 2026-08-20 16:13:28 -05:00
null closed this issue 2026-08-20 16:13:57 -05:00
Sign in to join this conversation.
No Label
P0
P1
P2
release-blocker
No project
No Assignees
1 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: null/Privacy-Period-Tracker#46
No description provided.