diff --git a/docs/DATA-MODEL.md b/docs/DATA-MODEL.md index 3173eed..9f92dbe 100644 --- a/docs/DATA-MODEL.md +++ b/docs/DATA-MODEL.md @@ -502,6 +502,32 @@ There is deliberately **no `dailyTasks` for a custom game**, so it contributes n reader to invent one is a form to fill in for a reminder they already have. Their events can still be marked as repeating, per event, like any other. +### Occurrence ids + +A reader's event may carry a repeat rule. The rule is what is stored; its +occurrences are derived on read and never written. + + myevent: the rule, as stored + myevent:#YYYY-MM-DD one occurrence, derived + +The suffix is the occurrence's own start day, read in the reader's local +timezone — the same reading `readerInstant` wrote it with and `fields()` shows +back. `#` is outside `[a-z0-9]` and therefore outside `CustomEventId`, so an +occurrence id cannot be written into `customEvents` and cannot survive an +import; `validRecords` drops it. That is the guardrail, and +`test/recurrence.test.ts` pins it in both directions. + +Marks, ignores, progress and daily ticks key off the whole string, so each +occurrence carries its own completion and its own streak. + +**Rescheduling a rule strands its marks.** Moving the anchor start or the +interval re-keys every occurrence; the marks under the old ids stay in +localStorage and stop being reachable. This is not migrated, for the reason +`removeEvent` does not cascade and `useMarkSet.merge` never removes — nothing +else holds a copy, so a silent rewrite is unrecoverable. The edit form counts +what will be stranded and says so first. Renaming a rule costs nothing: the +token is random. + **Deleting.** Removing your own event leaves any marks and logged days behind rather than reaching into three other stores on a single tap; they are inert, and the alternative is a misclick that deletes a streak. Removing a game that still has events is refused and says how many, rather than diff --git a/docs/PRD.md b/docs/PRD.md index bcbba78..b8a8a84 100644 --- a/docs/PRD.md +++ b/docs/PRD.md @@ -323,6 +323,21 @@ Four constraints, each protecting something that already exists: which is the same argument the code already makes for streaks. This is the *only* copy — there is no server to restore from. +A reader's event may also state how it comes round again — every N days, weeks +or months, optionally stopping on a date. The rule is stored; its occurrences +are derived, and each one is an ordinary event everywhere in the app: its own +countdown, its own completion, its own daily checklist. + +**An occurrence need not state its end.** With none, it runs until the next one +opens. That is not the app inventing a date to fill a form — it is entailed by +the interval the reader typed, and it is what separates a rule from the +unbounded rotation § Quality bar refuses to publish. It also means a plain +"resets every Monday" needs no end date at all. + +The lists carry two occurrences of any rule — the one that has not finished and +the one after it — because they answer "what ends soonest". The timeline draws +every occurrence in view, because it answers "what is the rhythm". + **F8 — First-run setup.** Before any events are shown, the reader picks which games they play, and how they want to read them. A calendar full of games they diff --git a/docs/SOURCES.md b/docs/SOURCES.md index 961589b..0efaee5 100644 --- a/docs/SOURCES.md +++ b/docs/SOURCES.md @@ -741,6 +741,16 @@ only exactly-dated material on the page (Abyss and Memorial Arena openings, thre deliberately unread: a recurring rotation with no end is not a deadline, and `endsAt: null` on each would render them live-with-unknown-end forever. +**As of 2026-08-27 the blocker above is addressable.** `src/shared/recurrence.ts` +gives an event a repeat rule, and a rule supplies the boundary the rotation was +missing: an occurrence with no stated end runs until the next one opens, so +`endsAt: null` no longer means live-with-unknown-end forever. Reading +`scheduleBosses` is now a parser change rather than a design question, and it is +Phase B's first target in +`docs/superpowers/specs/2026-08-27-recurring-events-design.md`. Nothing has been +changed on the ingest side yet — `GachaEvent` does not carry a rule, and this +note is what stops the next person re-deriving the reason it does not. + ## What every one of these costs, beyond the source Adding a game is never only a `SOURCES` entry: