The data model, PRD F17, README and DESIGN described one global chores switch and a checkbox per game for built-ins. Adds the spec and plan, and ticks the TODO item. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
7.6 KiB
Per-game switches 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: Daily chores and built-in weekly tasks each get a global switch plus per-game exceptions, set from a Choose games disclosure in What you see.
Architecture: A pure PerGame rule (src/shared/perGame.ts) — global answer, on-list, off-list — is the one place visibility is decided. Prefs gain optional exception lists; routineGames becomes the built-ins' on-list unchanged. Consumers (dailyGroups, activeRoutines, routineToAsk, Controls) read the rule through choreRule / routineRule.
Tech Stack: Bun 1.3, React 19, TypeScript strict, Tailwind, bun test.
Spec: docs/superpowers/specs/2026-10-14-per-game-switches-design.md
Global Constraints
- No migration: every existing stored pref keeps its meaning; absent means unanswered.
showRoutinesabsent is treated as off (built-ins stay opt-in).- No new dependency; colours are tokens or game hues only (AGENTS.md § Conventions).
- Commits wait to be asked (AGENTS.md § Conventions); each task ends green and stage-ready, split model → store → UI → docs.
Review Focus
- Reader with
routineGames: ["zzz"], nothing else → ZZZ built-ins still shown (Task 2 carry-over test). - Reader flips global chores off with an exception on Genshin → Genshin's chore stays (Task 1, Task 3).
- Global built-ins answered → strip stops asking (Task 2).
- Hand-edited storage with a game in both lists → shown (Task 1).
- Garbage in new prefs (
choresOff: "x",showRoutines: 1) → sanitised to absent (Task 2).
Task 1: The PerGame rule
Files: Create src/shared/perGame.ts; Test test/per-game.test.ts
Produces: type PerGame, shownFor(rule, game): boolean, setFor(rule, game, value): { on: string[]; off: string[] }, exceptionLine(rule, games, name): string.
- Write tests: truth table (no lists →
all; on-list → true underall:false; off-list → false underall:true; both lists → true);setForadds only differences, removes from the other list, removes when value equalsall;exceptionLinecasesEvery game,All but Genshin Impact,All but 3 games,No game,Only Zenless Zone Zero,Only 3 games, counting only games in the given list. - Run
bun test test/per-game.test.ts→ FAIL (module missing). - Implement:
export interface PerGame {
all: boolean;
on?: readonly string[] | undefined;
off?: readonly string[] | undefined;
}
export function shownFor(rule: PerGame, game: string): boolean {
if (rule.on?.includes(game)) return true;
if (rule.off?.includes(game)) return false;
return rule.all;
}
export function setFor(rule: PerGame, game: string, value: boolean): { on: string[]; off: string[] } {
const on = (rule.on ?? []).filter((g) => g !== game);
const off = (rule.off ?? []).filter((g) => g !== game);
if (value !== rule.all) (value ? on : off).push(game);
return { on, off };
}
export function exceptionLine(rule: PerGame, games: readonly string[], name: (g: string) => string): string {
const odd = games.filter((g) => shownFor(rule, g) !== rule.all);
if (odd.length === 0) return rule.all ? "Every game" : "No game";
const which = odd.length === 1 ? name(odd[0]!) : `${odd.length} games`;
return rule.all ? `All but ${which}` : `Only ${which}`;
}
- Run → PASS;
bun run typecheck.
Task 2: Prefs and routines read the rule
Files: Modify src/client/state/usePrefs.ts (Prefs interface, restorePrefsValue), src/shared/routines.ts (activeRoutines, routineToAsk), src/client/App.tsx (call sites); Test test/routines.test.ts, test/prefs.test.ts
Consumes: Task 1. Produces: Prefs.choresOn?, choresOff?, showRoutines?, routinesOff?; choreRule(p): PerGame, routineRule(p): PerGame (in usePrefs.ts); activeRoutines(rule: PerGame, copiedFrom); routineToAsk(visible, rule: PerGame, asked, answered: boolean).
- Tests: carry-over (
routineRule({...defaults, routineGames:["zzz"]})→activeRoutinesgives ZZZ only;showRoutines: truegives both;routinesOff:["zzz"]under true gives Endfield only);routineToAskreturns null whenanswered, skips effectively-on games; restore sanitises the three new arrays and keepsshowRoutinesonly when boolean. - Run → FAIL.
- Implement:
export function choreRule(p: Prefs): PerGame {
return { all: p.showChores, on: p.choresOn, off: p.choresOff };
}
export function routineRule(p: Prefs): PerGame {
return { all: p.showRoutines ?? false, on: p.routineGames, off: p.routinesOff };
}
Extend the array-sanitising loop to choresOn, choresOff, routinesOff; showRoutines → undefined unless boolean. activeRoutines filters shownFor(rule, r.game); routineToAsk returns null if answered, skips shownFor(rule, game) or asked. App: activeRoutines(routineRule(prefs), copiedFrom), routineToAsk(visible, routineRule(prefs), prefs.routinesAsked, prefs.showRoutines !== undefined); Yes writes routineGames: setFor(rule, game, true).on, routinesOff: …off.
- Run full suite and typecheck → PASS.
Task 3: Chores per game in the strip
Files: Modify src/client/components/Dailies.tsx (dailyGroups, Dailies prop), src/client/App.tsx; Test test/views.test.tsx
- Test:
dailyGroups(["genshin","hsr"], [], NOW, "europe", meta, startOf, (lane) => lane === "genshin")→ onlydailies:genshin. - Run → FAIL (type/behaviour).
- Implement: param
showChores: boolean | ((lane: LaneId) => boolean) = true; checktypeof showChores === "function" ? showChores(lane) : showChores. Same type on theDailiesprop. App passes(lane) => shownFor(choreRule(prefs), lane). - Run → PASS.
Task 4: The disclosure in What you see
Files: Modify src/client/components/Controls.tsx; Test test/controls.test.tsx
- Tests: chores line
Every gameby default andAll but Honkai: Star RailwithchoresOff:["hsr"];Choose gamesbutton present, chips absent until opened (static render); globalShow built-in weekly taskscheckbox offered when a listed game has built-ins, readsshowRoutines, lineOnly Zenless Zone ZeroforroutineGames:["zzz"]; noBuilt-in weekly tasks fortext anywhere; weekly-task switch offered when routines effectively on viashowRoutines: true. - Run → FAIL.
- Implement
PerGameSwitchcomponent: theCheck, then a faint lineexceptionLine(...) · <button aria-expanded>Choose games</button>; when open, aflex flex-wrap gap-1.5of chip buttons (aria-pressed, hue border/text when on,titlepassed per chip). Chores: games = enabled, non-custom lanes; writes{choresOn, choresOff}fromsetFor. Built-ins: games with routines; writes{routineGames: on, routinesOff: off, routinesAsked: [...asked minus g, g]}; global writes{showRoutines}. Replace the per-game routine checkboxes; basetaskPeriodsonshownFor(routineRule(prefs), g)acrossgames. - Run full suite + typecheck → PASS.
- Browser check (AGENTS.md § Commands): build shell, serve on a fresh port, open settings → What you see, open both disclosures; screenshot at 390px and 1280px in dark and light.
Task 5: Docs and changelog
Files: docs/DATA-MODEL.md, docs/PRD.md F17, README.md, docs/DESIGN.md, TODO.md, src/shared/changelog.ts (in a follow-up commit once the feature commit's hash exists).
- Update each sentence the change made false; tick the TODO with a spec link.
bun test,bun run typecheck.