Record what recurrence changed, and what it unblocked
Three sentences were false once a rule could bound an occurrence. DATA-MODEL gains the derived occurrence key and what a reschedule costs; F13 gains the rule and the reason an occurrence need not state its end. SOURCES' arustats note stays, with the answer beside it: the reason scheduleBosses is unread was a design gap, that gap is closed, and reading it is now a parser change. Nothing on the ingest side has moved — this note is what stops the next person re-deriving why. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
c5c578ae7e
commit
c93f85fb6d
@@ -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
|
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.
|
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:<token> the rule, as stored
|
||||||
|
myevent:<token>#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
|
**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
|
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
|
deletes a streak. Removing a game that still has events is refused and says how many, rather than
|
||||||
|
|||||||
+15
@@ -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
|
which is the same argument the code already makes for streaks. This is the *only* copy — there is
|
||||||
no server to restore from.
|
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.**
|
**F8 — First-run setup.**
|
||||||
Before any events are shown, the reader picks which games they play, and how they want to read
|
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
|
them. A calendar full of games they
|
||||||
|
|||||||
@@ -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
|
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.
|
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
|
## What every one of these costs, beyond the source
|
||||||
|
|
||||||
Adding a game is never only a `SOURCES` entry:
|
Adding a game is never only a `SOURCES` entry:
|
||||||
|
|||||||
Reference in New Issue
Block a user