diff --git a/AGENTS.md b/AGENTS.md index e536c39..b89b4b4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -952,6 +952,10 @@ event's length. It adds **no schema field**, so the feed contract is untouched. on or off. Store an override only when it *disagrees* with detection — recording agreement would freeze today's guess and stop a better parser from ever reaching that event. Neither control ever deletes a mark or a logged day, so both are reversible. +- **A check-in goal makes an event a daily, and meeting it never makes the event done.** Some events + want five days of seven (`progress.goal`, `src/shared/goal.ts`, PRD F12). A met week stays in the + strip as done this week without a tick being invented; only the reader marks the event done. A + goal this build cannot read is kept in storage and treated as none — never stripped on load. ## Events the reader entered themselves diff --git a/docs/DATA-MODEL.md b/docs/DATA-MODEL.md index 5f3328e..7a8ff26 100644 --- a/docs/DATA-MODEL.md +++ b/docs/DATA-MODEL.md @@ -214,7 +214,7 @@ malformed row surfaces at the boundary rather than deep in the UI. Namespaced, versioned, and small. Nothing here ever goes to the server. ```ts -"gacha-tracker:v1:progress" // { [eventId]: { status?, effort?, note?, at } } +"gacha-tracker:v1:progress" // { [eventId]: { status?, effort?, daily?, note?, goal?, at } } "gacha-tracker:v1:daily" // { [id]: { days: ["2026-08-15", ...], at } } "gacha-tracker:v1:ignored" // { [eventId]: { at } } — "stop showing me this" "gacha-tracker:v1:prefs" // { region, hiddenGames[], knownGames[]?, gameOrder[]?, @@ -280,9 +280,19 @@ Namespaced, versioned, and small. Nothing here ever goes to the server. | `effort` | `"quick"` \| `"short"` \| `"long"` \| `"grind"` \| absent | How much work they reckon it is | | `note` | free text | Anything worth remembering | | `daily` | `true` \| `false` \| absent | Whether it repeats daily, overruling detection | +| `goal` | `{ period: "week", measure: "days", required: 1–7, weekStartsOn: 1–7 }` \| `{ period: "event", measure: "days", required }` \| absent | A check-in goal: N distinct game-days per week or across the event (PRD F12) | -An entry with none of the three set is deleted rather than kept, so the store stays a set of things -the reader actually said something about. +An entry with none of these set is deleted rather than kept, so the store stays a set of things the +reader actually said something about. `isEmpty` in `useProgress.ts` must name every field: one left +out is a record marked only with that field, silently dropped on the next write. + +**`goal` is read where it is used and never stripped.** It is stored typed `unknown` and read through +`readGoal` (`src/shared/goal.ts`), which returns undefined for anything that does not parse. Nothing +removes an unreadable goal from the record — the likeliest one is a newer shape (weekly tasks add +`measure: "amount"`) met by an older cached bundle, and dropping it on load would delete it on the +next write. A goal makes the event a daily regardless of `daily` and detection; setting one clears a +stored `daily: false`, and any `daily` answer clears the goal, so the two never contradict in +storage. **`effort` is load-bearing, not decorative.** Combined with the time remaining it answers "can I still finish this?" — the same two days is comfortable for a `quick` event and hopeless for a @@ -324,6 +334,12 @@ counting toward `streakOf` and `dailySummary`, and is simply not drawn — the s outside the window the feed now claims. Nothing prunes the log against either window, because a fortnight's streak exists on that device and nowhere else. +**A check-in goal counts days in this same log; it stores no days of its own.** `goalStatus` +recomputes from `daily` on every read, so a backfill, a removed tick or a week rolling over needs no +bookkeeping and clears nothing. Weeks are cut in day-key space by `periodOf` +(`src/shared/period.ts`): the seven keys from the chosen weekday, so a week rolls exactly where the +game's day does, per region and per reset override. A day key later than today never counts. + **Not every game has a server per region.** `GameMeta.resetOffsets` records the regions where a game's server clock differs from `REGION_RESET_UTC_OFFSET`. Endfield is the case this exists for: it has two server groups rather than three, and Europe is served off the Americas machine on a fixed @@ -596,7 +612,8 @@ the earlier copy there discards every edit made after it, in both directions an restoring a backup taken before an evening's work would roll that evening back, and importing an old file onto a device with newer progress would roll the device back. Taking the maximum of the two timestamps keeps the merge order-independent and idempotent either way, and removes nothing. -`mergeProgress` is pure and `test/progress.test.ts` pins it. An export written before daily checklists existed +`mergeProgress` is pure and `test/progress.test.ts` pins it. Check-in goals ride `progress`, so they need no export key of their own +and merge with the record they belong to. An export written before daily checklists existed simply has no `daily` key, which is not an error. Losing a user's marks to a bad import is unrecoverable, so the merge is deliberately one-directional. A file whose `format` is unrecognised is refused outright rather than partly applied. diff --git a/docs/DESIGN.md b/docs/DESIGN.md index 1119345..4b889c8 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -404,6 +404,13 @@ selected tab fills with `raised` and turns ink; unselected tabs are faint and go - **State:** `aria-pressed` carries the state. On hover a chip lifts 1px and gains a 1px hue ring, and an unpressed chip's label goes to ink. On press it scales to 0.96. A chip with nothing waiting sits at 50% opacity: still reachable, but not competing for attention. +- **Goal met, not ticked today** (a dailies chip whose event has a check-in goal): full-strength hue + border, label and check — the check drawn dashed — on the *outstanding* ground, with no wash. The + wash belongs to a real tick only, so "done this week" never reads as "done today". A goal chip's + count is the goal's (`3/5 wk`) and replaces the streak; one counter per pill. +- **Check-in goal control** (detail sheet, under the daily checklist): the status pills' style — + Off / Each week / Across the event — then a small number field and, for weeks, a weekday select, + both on `ground` with a hairline border like the notes field. ### Event row (signature) The unit of the checklist. It is one full-bleed target that opens the event and does nothing else. diff --git a/docs/PRD.md b/docs/PRD.md index 3b9bfec..f038c89 100644 --- a/docs/PRD.md +++ b/docs/PRD.md @@ -297,6 +297,20 @@ Three rules hold it honest: logged, keeps counting toward the streak and the totals, and is simply off-screen. Nothing removes a day the reader did not remove themselves. +**Some events want a number of days, not every day.** Genshin's “To Temper Thyself and Journey +Far” asks for five check-ins out of seven each week. Ticking it as a daily calls two unticked days a +failure and keeps it outstanding when the week is already done. So a repeating event can carry a +**check-in goal**: N days each week (weeks starting on a weekday the reader picks, at the game's own +reset), or N days across the whole event. It is always the reader's input — nothing is read off a +title — and a goal makes an event a daily even with detection off. + +- **A met week stays in the strip** as done this week, counted complete in the header, and comes + back when the next week opens. No tick is invented for it, and only a real tick celebrates. +- **Meeting a goal never marks the event done.** A count may not have claimed the rewards. An + event-wide goal reached offers *Mark event done*; the reader decides. +- **The target is never lowered.** A short first or last week keeps it and says when too few days + are left; an unknown end leaves the days left unknown. + **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 diff --git a/docs/superpowers/specs/2026-10-03-check-in-goals-design.md b/docs/superpowers/specs/2026-10-03-check-in-goals-design.md index a2d7ba7..421a5e1 100644 --- a/docs/superpowers/specs/2026-10-03-check-in-goals-design.md +++ b/docs/superpowers/specs/2026-10-03-check-in-goals-design.md @@ -2,7 +2,7 @@ **Date:** 2026-10-03, revised and approved 2026-10-13 -**Status:** approved; spec 1 of 2. Spec 2 is +**Status:** implemented 2026-10-13; spec 1 of 2. Spec 2 is `2026-10-13-periodic-tasks-design.md`, which reuses the period engine defined here. ## Request and intended outcome