Every period app shows a date in the future. Ours shows a window, and that window is not stored anywhere.

When I started on Mori, the obvious schema looked like this:

  • records: the days a person actually marked
  • prediction: next_start, next_end, confidence, computed_at

That second table is where things go bad, and it took me a while to see why.

Derived data goes stale in boring ways. Someone corrects a date from last month. Now the saved prediction is wrong until a job recomputes it. If that job is async, there's a moment when the home screen and the calendar disagree. Nobody files that as "cache invalidation"; they file it as "this app is confused".

It grows the delete surface. Our delete flow already covers records, clustered cycles, exports, notification schedules and the install identity. If predictions are persisted, they become one more thing to delete, and one more thing that can survive a bug and leave a future date on a phone someone just wiped. The honest version of "delete everything" gets harder every time you save a derived value.

It hides the input. A stored prediction was computed at some point, from some set of cycles, on some date. None of that survives. When confidence looks off, you can't reproduce it.

So the domain function takes the whole history plus an explicit as-of date:

buildPrediction({ cycles, settings, asOfDate }) 

Same inputs, same output, every time. There is no clock inside. Open the app tomorrow and the answer changes because asOfDate changed, not because a background job ran. The home screen, the calendar, the cycle ring and the optional reminder all call that one function.

The cost is recomputing on read. It's a loop over a handful of date ranges on one device, so the cost is noise. What we give up is the ability to say "this is what we predicted on Oct 3". I'm fine with that. A prediction is an estimate built from history, and history changes.

There's a second reason this matters. The phone is the only copy by default: no account, no cloud table to repair from. Every stored value is a second source of truth waiting to drift. So we store facts — the dates someone marked — and recompute interpretations.

The app is Mori (moricycle.com), a period tracker that works offline and doesn't ask for an account. Predictions are estimates from your own history, and the UI says so instead of pretending otherwise.

Written with AI assistance.