diff --git a/docs/superpowers/plans/2026-10-22-version-routines.md b/docs/superpowers/plans/2026-10-22-version-routines.md new file mode 100644 index 0000000..3254e01 --- /dev/null +++ b/docs/superpowers/plans/2026-10-22-version-routines.md @@ -0,0 +1,116 @@ +# Version routines Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Built-in routines can reset with a game's version, from a hand-kept, cited table of version +timers; WuWa's three shop-pull lines are the first. + +**Architecture:** A new `src/shared/versions.ts` holds `VERSIONS`, `VERSION_DAYS` and a pure +`versionWindow(game, now)`. Built-in routines gain a `{ unit: "version" }` period (never in the stored +`PeriodSchema`); `taskStatus` resolves it through `versionWindow` and reports it as scope `cycle`. The +About tab of a game in *Your games* shows the version row. + +**Tech Stack:** Bun, TypeScript, zod, React 19, `bun test`. + +**Spec:** `docs/superpowers/specs/2026-10-22-version-routines-design.md` + +## Global Constraints + +- The stored `PeriodSchema`, `TaskSchema`, `gacha-tracker:v1:tasks` and the export format do not change. +- A routine resets only at a **recorded** `end`; an estimate is displayed, never acted on (decision 3). +- Version `start`/`end` are ISO UTC instants; `end` is when the next version's stock appears. +- WuWa 3.7: `end` = `2026-11-11T10:30:00Z`, `next` = `"3.8"`; `VERSION_DAYS.wuwa` = 42. +- Version routines are amount measures only; scope reported as `"cycle"`. +- The period key is the start instant's UTC date, bare `YYYY-MM-DD` (`AmountLog` keys are free + strings, but `PinnedRoutine` and the catch-up code compare `periodStart` with day keys, so a date + stays comparable). +- Captions (exact): `resets with 3.8 · 11 Nov`; `resets with next version (3.9) · expected ~23 Dec`; + `resets with next version · expected ~23 Dec`; past the estimate append `, not yet confirmed`. +- Every figure cites an https page; an in-game figure's note says `Confirmed in game, YYYY-MM-DD.` + +## Review Focus + +- **The moment of the switch:** one minute before `end`, the old count is shown; at `end` exactly, 0/7. Pinned in Task 1 and Task 2. +- **Past the estimate with no recorded end:** the count holds and the key does not move. Task 1 tests the same key on 2027-02-01. +- **A routine row with `daysLeft` null:** the chip shows `n/7` with no `· 0d`. Task 2 `goalCount` test. +- **Catch-up "Last version:":** without a `periodOf` for version, the Dailies catch-up would throw. Task 2 skips amount catch-up for version items, with a test. +- **The timeline band for an open-ended version:** `routinePeriod` must not throw or draw a zero-width band. Task 2 test: it ends at the estimate. + +--- + +### Task 1: Version table and window + +**Files:** +- Create: `src/shared/versions.ts` +- Modify: `src/shared/period.ts` (add `RoutinePeriodSchema`, `RoutinePeriod`) +- Test: `test/versions.test.ts` + +**Interfaces:** +- Produces: + - `period.ts`: `export const RoutinePeriodSchema = PeriodSchema.or(z.object({ unit: z.literal("version") }))`; `export type RoutinePeriod`. + - `versions.ts`: `interface GameVersion` (fields as the spec § Shape), `VERSIONS: readonly GameVersion[]`, `VERSION_DAYS: Partial>`, + `interface VersionWindow { number?: string; key: string; start: number; end?: number; estimate?: number; nextNumber?: string }`, + `versionWindow(game: string, now: number, versions = VERSIONS, days = VERSION_DAYS): VersionWindow | undefined`, + `versionCaption(w: VersionWindow, now: number): string` (the exact caption strings; dates as `asOfLabel` minus the year, e.g. `11 Nov`). + +- [ ] **Step 1: Write failing tests** in `test/versions.test.ts`: + - table invariants over `VERSIONS`: per game sorted, non-overlapping; `end > start`; recorded `end` equals the next entry's `start`; https source; both instants parse; `checked` is a date. + - `versionWindow("wuwa", Date.parse("2026-10-22T14:30:00Z"))` → `{ number: "3.7", key: "2026-09-30", end: Date.parse("2026-11-11T10:30:00Z"), nextNumber: "3.8", estimate: undefined }`. + - at `2026-11-11T10:29:00Z` → number `"3.7"`; at `2026-11-11T10:30:00Z` → `{ number: "3.8", key: "2026-11-11", end: undefined, estimate: Date.parse("2026-12-23T10:30:00Z"), nextNumber: undefined }`. + - at `2027-02-01T00:00:00Z` → still `number: "3.8"`, `key: "2026-11-11"`. + - a fixture table with one entry and no `end` → window is that entry, `estimate = start + 42d`. + - `versionWindow("genshin", now)` → `undefined`; with `days` lacking the game → `estimate` undefined. + - `versionCaption`: `"resets with 3.8 · 11 Nov"`; `"resets with next version (3.9) · expected ~23 Dec"` (fixture with `next: "3.9"`); `"resets with next version · expected ~23 Dec"`; past the estimate → `"…expected ~23 Dec, not yet confirmed"`; no estimate → `"resets with next version"`. +- [ ] **Step 2: Run** `bun test test/versions.test.ts` — expect FAIL (module missing). +- [ ] **Step 3: Implement.** First entry per the spec § First entry. Before writing it, check a page for 3.7's start (Game8 / Fandom / Prydwen). If one states it, cite it; otherwise use `2026-09-30T10:30:00Z` and say in `note` that the start is 42 days before the in-game end. Comment on `VERSION_DAYS.wuwa` citing 3.7's length and AGENTS.md's six weeks. Window rules exactly as the spec § Shape. +- [ ] **Step 4: Run** `bun test test/versions.test.ts` — PASS. +- [ ] **Step 5: Commit** `feat(versions): a cited table of version timers, and the window holding now`. + +### Task 2: Routines can follow a version + +**Files:** +- Modify: `src/shared/routines.ts` (schema period → `RoutinePeriodSchema`), `src/shared/task.ts` (`TaskShape.period: RoutinePeriod`; `taskStatus`; `taskCaption`), `src/shared/goal.ts` (`goalCount`), `src/client/state/reader/routines.ts`, `src/client/components/Dailies.tsx`, `src/client/components/redesign/TimelinePage.tsx`, `src/client/components/redesign/RoutinesPage.tsx`, `src/client/components/redesign/RoutineDrawer.tsx` (period line), plus whatever else `bun run typecheck` names. +- Test: `test/task.test.ts`, `test/routines.test.ts`, `test/reader-routines.test.ts`, `test/redesign-routines.test.tsx` + +**Interfaces:** +- Consumes: Task 1's `RoutinePeriod`, `versionWindow`, `versionCaption`. +- Produces: `taskStatus` accepts `task.period: RoutinePeriod`; for `version` it returns `scope: "cycle"`, `periodStart: window.key`, `daysLeft`/`open` = `Math.ceil((end - now) / DAY)` when `end` is recorded, else `null`. `taskCaption(task, now?)` uses `versionCaption` for version. A `routineVersioned(r): boolean` helper in `routines.ts`. + +- [ ] **Step 1: Write failing tests:** + - `task.test.ts`: a wuwa version routine (amount 7) with `amounts = { "2026-09-30": { value: 3, at } }` at 2026-10-22T14:30Z → `count 3, required 7, scope "cycle", periodStart "2026-09-30", daysLeft 20`; at `2026-11-11T10:30Z` → `count 0, periodStart "2026-11-11", daysLeft null, open null`. + - `goalCount({ scope: "cycle", count: 3, required: 7, daysLeft: null, open: null, ... })` → `"3/7"`; with `daysLeft: 20` → `"3/7 · 20d"`. + - `routines.test.ts`: every routine with `period.unit === "version"` has `measure.kind === "amount"`; `RoutineSchema` accepts `{ unit: "version" }`; `TaskSchema` still refuses it. + - `reader-routines.test.ts`: a version routine row has `cadence "cycle"`. + - Dailies catch-up renders no "Last version" editor for a version item (render test in the existing Dailies/edit tests' style). + - `routinePeriod` for an open-ended version row ends at the estimate; with a recorded end, at `end`. + - `redesign-routines.test.tsx`: *Make it mine* is absent for a version routine. +- [ ] **Step 2: Run** those files — FAIL. +- [ ] **Step 3: Implement.** In `taskStatus`, branch on `version` before `dayKey`. `goalCount`'s cycle form drops `· Nd` when both `daysLeft` and `open` are null. Cadence: `version` → `"cycle"`. Dailies: amount catch-up returns `null` for a version period. `routinePeriod`: version → `[window.start, end ?? estimate ?? now + DAY]`. RoutinesPage: hide *Make it mine* when `routineVersioned`. RoutineDrawer's read-only period line: the caption. Run `bun run typecheck` and fix every site it lists the same way (`version` never reaches `periodOf`). +- [ ] **Step 4: Run** `bun test` and `bun run typecheck` — all PASS. +- [ ] **Step 5: Commit** `feat(routines): a routine can reset with its game's version`. + +### Task 3: WuWa's shop pulls + +**Files:** +- Modify: `src/shared/routines.ts` (three entries after Tower of Adversity), `test/routines.test.ts` (the "each game's, as sourced" wuwa expectation) + +- [ ] **Step 1: Find the source.** One page describing the shop's Tide stock (prefer one naming the shop and its currency). Record its date as `asOf`, `checked: "2026-10-22"`. Title `Shop: `, or `: ` if the page names the shop. +- [ ] **Step 2: Update the test** expecting, after Tower of Adversity, the three rows with `{ unit: "version" }` and `{ kind: "amount", target: 7, unit: "" }`; Radiant and Forging `pulls: "premium"`, Lustrous `"standard"`. Run — FAIL. +- [ ] **Step 3: Add the entries** (ids `routine:wuwa:shop-radiant-tide`, `-forging-tide`, `-lustrous-tide`; note ends `Confirmed in game, 2026-10-22.`, ≤140 chars). Run `bun test` — PASS. +- [ ] **Step 4: Commit** `feat(routines): Wuthering Waves' shop pulls, each version`. + +### Task 4: The version row in Your games + +**Files:** +- Modify: `src/client/components/redesign/GameSettings.tsx` (About group, after the reset rows) +- Test: `test/game-settings.test.tsx` + +- [ ] **Step 1: Failing test:** WuWa's About tab at 2026-10-22T14:30Z shows a row `Version 3.7` with `ends 11 Nov 2026` (formatted with `formatInZone` in the reader's zone), `(in 19d)`, a link to the source, and the note. At 2026-11-12 it shows `Version 3.8` and `next version expected ~23 Dec 2026 (estimate)`. Genshin's About has no version row. +- [ ] **Step 2: Run** — FAIL. **Step 3: Implement** with `versionWindow` and the existing `trow` markup. **Step 4: Run** — PASS. +- [ ] **Step 5: Commit** `feat(settings): show a game's version timer on its About tab`. + +### Task 5: Docs, browser check, changelog + +- [ ] **Step 1:** `docs/DATA-MODEL.md` (version period built-in only; amounts under the version's start date), `docs/PRD.md` F17, AGENTS.md bullet (versions hand-kept and cited; estimates shown, never acted on; add each version's end when known), TODO.md (Oscillated Coral partly addressed; follow-ups from spec § Not in this change; "record WuWa 3.8's end"), spec status → shipped with commit range. Commit `docs: version routines`. +- [ ] **Step 2:** `bun run build` and drive the app (run skill): WuWa routines show `0/7 · 20d` and the caption; About shows the version row. Fix anything found. +- [ ] **Step 3:** Changelog entry in `src/shared/changelog.ts` in a follow-up commit naming the feature commits, matching recent `chore(changelog): …` commits; `bun test test/changelog*.ts*`.