Files
gacha-event-tracker/docs/superpowers/plans/2026-10-22-version-routines.md
T

11 KiB

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<Record<GameId, number>>, 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: <Tide>, or <Shop name>: <Tide> 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: "<Tide name>" }; 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*.