diff --git a/docs/DATA-MODEL.md b/docs/DATA-MODEL.md index 65fe712..2146cd3 100644 --- a/docs/DATA-MODEL.md +++ b/docs/DATA-MODEL.md @@ -319,6 +319,63 @@ would stop a later parser improvement from ever reaching that event. This is the `progress` that changes what the app *shows* rather than recording what the reader did, which is why it lives beside their other notes rather than in the feed. +### Reader-authored key spaces + +PRD F13 lets a reader define their own game and enter their own events. Those records get their own +ID spaces, and the reason is the one this document keeps making: + +``` +mygame: a game the reader defined "mygame:limbus-company" +myevent: an event the reader entered "myevent:k3f9qa2m" +``` + +**A reader's event is never minted as `${game}:${slug}:${date}`.** That space is derived from source +titles, and a reader can type a title identical to a scraped one — same game, same start date, same +slug, same key. Two records under one key means one completion mark, one note and one streak +silently belong to both, and the collision is invisible until someone notices their progress moving +on its own. A random suffix makes that impossible rather than unlikely. + +The randomness buys a second property worth keeping: **renaming your own event does not move its +ID.** A feed event's ID follows its source title, so a wiki renaming an event mints a new ID and +reconciliation has to recognise the near-match to preserve marks. A reader's event has no source to +follow, so its ID is assigned once at creation and never derived from anything they can edit. + +Three first segments are now reserved and **none of them may ever become a `GameId`**: `dailies`, +`mygame`, `myevent`. A test asserts this against `GameId.options`, because the day that stops being +true is the day two key spaces merge silently. + +```ts +"gacha-tracker:v1:customGames" // { [id]: { id, name, hue, at } } +"gacha-tracker:v1:customEvents" // { [id]: { id, game, title, type, summary, + // startsAt, startPrecision, + // endsAt, endPrecision, at, updatedAt } } +``` + +Reader-authored events reuse `progress`, `daily` and `ignored` unchanged, keyed by their `myevent:` +id — everything the reader can say about a scraped event they can say about their own, with no +second set of stores to keep in sync. + +Field rules mirror the feed's, because the reader deserves the same guarantees the parsers are held +to: + +| Rule | Why | +|---|---| +| `endsAt` null pairs with `endPrecision: "unknown"` | The same invariant `GachaEvent` enforces. "I don't know when this ends" is a supported answer for a reader too, and a required one — otherwise entering an unannounced event forces them to invent a date | +| A date with no time is `"day"` precision, a date with one is `"exact"` | So the UI's existing "accurate to the day only" note is honest about their input as well | +| `endsAt` must be after `startsAt` | A backwards interval is a typo whoever made it | +| `hue` must match `#rrggbb` | It reaches a `style` attribute, and an imported file is not necessarily one the reader wrote | +| `regionScoped` is always false | They entered one instant, not a per-region map. Claiming otherwise would fabricate three timestamps out of one | + +There is deliberately **no `dailyTasks` for a custom game**, so it contributes no standing +`dailies:` chore. That list is the routine every player of a tracked game recognises; asking a +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. + +**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 +cascading. + ### Migration from `completions` `completions` used membership to mean "done", which cannot express "started". `progress` replaces it @@ -353,10 +410,26 @@ old key. "dailies:genshin": { "days": ["2026-08-14", "2026-08-15"], "at": "..." } }, "ignored": { "zzz:some-event-i-skip:2026-08-19": { "at": "..." } }, + "customGames": { + "mygame:limbus-company": { "id": "mygame:limbus-company", "name": "Limbus Company", + "hue": "#c74b50", "at": "..." } + }, + "customEvents": { + "myevent:k3f9qa2m": { "id": "myevent:k3f9qa2m", "game": "mygame:limbus-company", + "title": "Walpurgisnacht", "type": "banner", "summary": null, + "startsAt": "2026-08-20T00:00:00.000Z", "startPrecision": "day", + "endsAt": "2026-09-03T00:00:00.000Z", "endPrecision": "day", + "at": "...", "updatedAt": "..." } + }, "prefs": { "region": "europe", "hiddenGames": [], "sort": "ending", "onboarded": true } } ``` +`customGames` and `customEvents` are additive keys, so a v1 export written before F13 simply lacks +them — not an error, just a file from a device that had none. They merge by id like everything else, +and because an event and the game it belongs to travel in the same file, an import never lands an +event whose lane is missing. + Import **merges** every set: a mark present in either the file or the current device survives, and import never removes one. `daily` merges as a **union of days per ID** — every day either side recorded is a day the reader actually played. An export written before daily checklists existed diff --git a/docs/PRD.md b/docs/PRD.md index e7f4365..75bda27 100644 --- a/docs/PRD.md +++ b/docs/PRD.md @@ -114,6 +114,32 @@ The heuristic assumes about an hour of play a day and says so. It never hides or it adds a flag the reader can ignore. **An event with no recorded effort never gets a warning**, because inferring an estimate in order to warn about it would be fabricating their input. +**F13 — Your own games and your own events.** +No feasible adapter set covers everyone. Fourteen games were named in the first release thread and +the reader with the largest collection asked for exactly one thing — *"can you add a custom game +option, we can input our own event description and time frames?"* — and, separately, said they would +wait until "more are added **or we are able to customise it**." That is the tail no source list +reaches, and it needs no scraping, no ToS question and no server. + +So a reader can define a game (a name and a lane colour) and enter events against it, or against a +game the app already tracks when a source missed something. Their events sit in the same lists, +timeline, sort and filters as scraped ones, and everything they can do to a scraped event — done, +doing, effort, note, ignore, daily checklist — works identically. + +Four constraints, each protecting something that already exists: + +- **Their events are visibly theirs.** A hand-entered date is never attributed to a source and never + carries a source link. The reader must be able to tell, at a glance, which dates the app went and + found and which ones they typed. +- **Their events never touch the ingest pipeline.** `sanitize.ts` and `merge.ts` exist for pages we + do not control; a reader's own typing is neither untrusted markup nor a second opinion to + reconcile. Nothing they enter is fetched, parsed, merged, scored or quarantined. +- **Their IDs live in their own key space.** Never `${game}:${slug}:${date}` — see + `docs/DATA-MODEL.md` § Reader-authored key spaces. +- **They are in the backup.** An export that omitted hand-entered events would be a lossy backup, + which is the same argument the code already makes for streaks. This is the *only* copy — there is + no server to restore from. + **F8 — First-run game picker.** Before any events are shown, the reader picks which games they play. A calendar full of games they don't play is worse than an empty one — it buries the thing they came for. Nothing is preselected @@ -144,9 +170,13 @@ proposition is trust in the dates. ## Out of scope for v1 -Accounts and sync; push notifications; per-event checklists or progress tracking; in-game resource -or pull tracking; user-submitted events; native mobile apps (the web app installs to a home screen, -which is enough); localization beyond English. +Accounts and sync; push notifications; in-game resource or pull tracking; native mobile apps (the +web app installs to a home screen, which is enough); localization beyond English. + +Two entries left this list after v1 shipped and readers used it. Per-event checklists became F12 and +the daily strip; **user-submitted events became F13**, on the strength of the release thread — the +reader juggling ten games asked for it twice and asked for nothing else, and no adapter roadmap +answers them. The decision is recorded here rather than left implicit in the code. ## Success criteria