feat: the redesigned Event Clock

The new reader app replaces the old UI: Checklist with a rail (focus
pills, Next to expire, today's dailies, pinned routines), Timeline in a
new and a classic look, Routines, Archive and a Settings area, in a
Structured (default) and a Relaxed interface. It follows the approved
Round 6 B and Round 7 mockups, corrected screen by screen with the owner.

Reader data moves to a v2 store (gacha-tracker:v2:*). Existing v1 data
is migrated on first load, a recovery copy is written first, and the v1
keys are never removed, so a rollback finds them unchanged. Redeem codes
for Genshin arrive as their own feed.

Also: live's urgency ramp, editable per theme; completion and
section-complete fireworks; a returning-user tour; the design mockups,
specs, plans, reviews and handoff record.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Lucas Winther
2026-10-18 01:55:16 +02:00
co-authored by Claude Opus 5.5
parent ddd9673f86
commit 50c6e91b00
284 changed files with 45100 additions and 1628 deletions
@@ -0,0 +1,360 @@
# Event Clock Redesign Implementation Plan
> **2026-10-17: second implementation rejected by owner.** The later correction/review statements
> below do not mean acceptance. Resume from `docs/handoffs/2026-10-17-redesign-second-rejection-handoff.md`.
> Launch the current server and ask the owner for the biggest differences before further UI changes.
> **Acceptance withdrawn:** the owner rejected the implementation for substantial divergence from
> the approved markup. Completed checkboxes below are historical, not design acceptance.
> Presentation/fidelity gates reopen. Read
> [the replacement-agent handoff](../../handoffs/2026-10-16-redesign-fidelity-handoff.md) first.
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Execution method: **Native**, selected by the owner. Steps use checkbox (`- [x]`) syntax for tracking.
**Goal:** Deliver the approved redesign with Relaxed (Round 7) and Structured (Round 6 B) interfaces, shared functionality, and preserved reader data.
**Architecture:** Keep one React application and one set of domain selectors; interface tokens change presentation. Introduce an account-aware reader-state envelope with explicit migration and reset revisions before connecting new tracking controls. Separate source ingestion from presentation and keep all user state browser-local.
**Tech Stack:** Bun 1.3, React 19, strict TypeScript, Tailwind, Zod; existing dependencies only. Bun tests for domain/storage and server-rendered semantics; scratch Puppeteer and Chrome for built-app interaction and appearance.
**Spec:** [Approved redesign specification](../specs/2026-10-16-ui-redesign-design.md).
## Global Constraints
- Two interfaces: **Relaxed / Structured**, using Round 7 / Round 6 B respectively; Relaxed default.
- Interface, Dark/Light/System, colour scheme and Standard/Compact event rows are independent preferences.
- Keep existing event IDs, daily IDs, task IDs and day-key formats.
- No added runtime dependencies, accounts, push service, SQLite work or prototype JavaScript transplanted into production.
- Unknown end dates remain null. Explicit timestamps, `regionEnds` and reader-entered instants remain authoritative.
- **Event data may be out of date** replaces **Older schedules**; warnings cover followed games, independently of focus.
- Backup retention: seven daily, four weekly, six monthly snapshots, plus one recovery copy; stable UTC basis.
- Changelog is a Settings page, with no footer link or About button.
- Small text must clear 4.5:1 in both modes and UIs.
- Work in the current checkout on `main`, per AGENTS.md. Do not create a branch, commit, push or publish without a separate owner instruction. This overrides skill worktree/commit defaults.
- Update shipped docs alongside implemented behaviour; preserve earlier review artifacts and unrelated untracked files.
## Review Focus
1. Two open tabs edit different progress fields: preserve both edits rather than last whole-envelope write winning (Task 3).
2. An interrupted migration/restore or quota failure: retain recoverable data and prevent startup effects overwriting it (Tasks 3, 10).
3. Reset changes move today's date backward: keep old ticks reachable without marking the new period completed by accident (Tasks 4, 6).
4. A source disappears while a future/unknown-end event remains outstanding: do not declare it expired or lose its detail (Task 8).
5. Old exports and unreadable future fields: preserve what this build cannot interpret; never claim a partial import fully restored data (Tasks 2, 10).
## Structure and shared contracts
Existing entry points stay `App.tsx`, `main.tsx`, `api.ts` and the current feed. New UI lives under
`src/client/components/redesign/`; reader state under `src/client/state/reader/`. Keep shared date
logic in `src/shared/`, without importing browser storage into shared modules.
The following contracts belong to `src/client/state/reader/schema.ts` and are consumed by later
tasks. Implement Zod schemas as `.passthrough()` for records with forward-compatible fields;
validate known fields without stripping unknown ones.
- `AccountId`: opaque `account:<random>` identity, minted once, unrelated to mutable region.
- `ClockRevision`: `{ id: string; effectiveAt: string; region: Region; reset: null | { hour: number; minute: number }; automaticBase: { offsetUtc: number; hour: number; minute: number } }`.
Capture automaticBase when creating a revision; later game metadata updates start a new revision
for current Automatic clocks rather than changing the historical one.
- `TrackingIntent`: `{ events: boolean; dailies: boolean; weeklyMonthly: boolean; cycles: boolean }`.
- `RoutinePlacement`: `{ followed: boolean; checklist: boolean; timeline: boolean }`.
- `Account`: `{ id: AccountId; game: LaneId; region: Region; intent: TrackingIntent; routines: Record<string, RoutinePlacement>; clocks: ClockRevision[]; retiredAt?: string }`.
- `AccountData`: `{ progress: Record<string, unknown>; ignored: Record<string, unknown>; daily: Record<string, Record<string, unknown>>; amounts: Record<string, Record<string, unknown>> }`.
`daily` and `amounts` are keyed first by revision id, then by the original item id. Inside each
item retain existing day/period log shapes. Revision ids never become event ids.
- `ArchiveRecord`: `{ accountId: AccountId; event: DisplayEvent; outcome: "done" | "ignored" | "unfinished"; outcomeAt: string; resolvedEnd: number | null; clockRevision: string; progress: unknown; days: string[]; amounts: Record<string, unknown> }`.
- `SeenEvent`: `{ accountId: AccountId; event: DisplayEvent; firstSeenAt: string; lastSeenAt: string; clockRevision: string }`.
- `ReaderState`: `{ version: 2; revision: number; migratedAt: string; legacyDigest: string; accounts: Record<string, Account>; data: Record<string, AccountData>; archive: Record<string, ArchiveRecord>; seenEvents: Record<string, SeenEvent>; prefs: Record<string, unknown>; customGames: Record<string, unknown>; customEvents: Record<string, unknown>; tasks: Record<string, unknown>; codeSeen: Record<string, string[]>; codeRedeemed: Record<string, Record<string, string>>; eventSeen: Record<string, string[]> }`.
Keys of archive/seenEvents are `JSON.stringify([accountId, event.id])`, preventing collisions
without altering the domain id. `codeSeen` stores known ids per game; redemption stores code ids
per account. `eventSeen` records event ids per account after the relevant main view is visited.
- `ReaderAction`: discriminated operations to edit a specific account, progress field, day tick,
amount, follow/pin, preference, archive outcome or seen marker. Actions carry `at` and an opaque
operation id for conflict/replay handling; do not expose arbitrary whole-state replacement.
- `ReaderStore`: `{ getSnapshot(): ReaderState; subscribe(listener: () => void): () => void; dispatch(action: ReaderAction): Promise<SaveResult> }`.
Async dispatch permits Web Lock serialization; UI event callbacks await or explicitly void it,
and the store publishes save failures through its hook rather than dropping rejected writes.
- `SaveResult`: `{ ok: true } | { ok: false; reason: "unavailable" | "quota" | "invalid" | "conflict" }`.
Browser keys: `gacha-tracker:v2:reader`, `gacha-tracker:v2:migration-recovery`,
`gacha-tracker:v2:restore-pending`, `gacha-tracker:v2:backups`, `gacha-tracker:v2:restore-recovery`.
Never delete the existing `gacha-tracker:v1:*` keys. A backup contains reader state and legacy raw
values, excluding these backup/recovery keys and service-worker caches.
## Tasks
### Task 1: Shared navigation and the two interface shells
**Files:** Create `src/client/components/redesign/AppNavigation.tsx`, `SettingsLayout.tsx`,
`ChangelogPage.tsx`, `src/client/redesign.css`; modify `App.tsx`, `styles.css`,
`state/usePrefs.ts`, `components/ChangelogSheet.tsx`, `scripts/build-static.ts`; test
`test/redesign-shell.test.tsx`, `test/prefs.test.ts`.
**Interfaces:** Export `AppPage = "soon" | "timeline" | "routines" | "archive" | "settings"`
and `SettingsPage = "general" | "games" | "appearance" | "lists" | "time" | "sources" | "backup" | "about" | "changelog"`.
`AppNavigation({ page, onPage })` and `SettingsLayout({ page, onPage, children })` use native buttons.
Add normalized optional prefs `interface: "relaxed" | "structured"` and `density: "standard" | "compact"`.
- [x] Add failing semantic/prefs tests: missing/invalid interface reads Relaxed, stored Structured survives; navigating to Settings does not overwrite `prefs.view`; Changelog renders as a page without modal semantics.
- [x] Run `bun test test/redesign-shell.test.tsx test/prefs.test.ts`; expect failure for new behaviour.
- [x] Implement shared navigation/settings shell. Extract changelog content for page and retained legacy-sheet test compatibility. Scope layout/type tokens by `data-interface`; carry actual Round 7/6 B proportions and offline font fallbacks. Show settings only on its page, not below every list.
- [x] Run named tests and `bun run typecheck`; expect pass. Browser-check both shells at 390/1440px before later features. Update DESIGN/ARCHITECTURE for landed shell only.
### Task 2: Reader schemas, legacy migration and full-state export
**Files:** Create `state/reader/schema.ts`, `migration.ts`, `export.ts`; modify `state/export.ts`;
test `test/reader-migration.test.ts`, `test/reader-export.test.ts`.
**Interfaces:** `migrateLegacy(raw: Record<string, string>, games: readonly LaneId[], at: string, mint: () => AccountId): ReaderState`;
`parseReaderExport(value: unknown): ReaderState | null`; `buildReaderExport(state: ReaderState, legacy: Record<string, string>, at: string): object`.
Export format `gacha-tracker-export`, version `2`, full-state field `reader`, raw preservation field
`legacy`; existing version-1 exports remain accepted by a legacy conversion path.
- [x] Add failing tests from representative v1 saves: completion-only migration, daily/routine/task amounts, hidden games/order/region/theme/view, custom recurrence, unknown goal/task fields and orphan ids. Assert no id/day label changes and no routines enabled beyond the old effective rules.
- [x] Run `bun test test/reader-migration.test.ts test/reader-export.test.ts`; expect missing-feature failure.
- [x] Implement schemas/contracts above and deterministic mapping of each game's old state to one initial account in saved region. Default pins reproduce existing visible routines. Migrate all game identities referenced by logs/tasks/custom records, including games missing from today's feed. Retain unattributable records in legacy raw storage, with no guessed game.
- [x] Test repeat startup uses saved migration result without minting identities again; v2 export round-trip retains unreadable extra fields; malformed known fields reject without writes. Run named tests and typecheck; expect pass. Document schemas/keys in DATA-MODEL.
### Task 3: Persisted reader store and concurrency
**Files:** Create `state/reader/reducer.ts`, `store.ts`, `useReader.ts`; modify `state/storage.ts`,
`main.tsx`, `App.tsx`; test `test/reader-store.test.ts`.
**Interfaces:** `reduceReader(state: ReaderState, action: ReaderAction): ReaderState`;
`createReaderStore(storage: Storage, initial: ReaderState): ReaderStore`;
`useReader(): { state: ReaderState; dispatch: ReaderStore["dispatch"]; saveError: SaveResult | null }`.
Add `tryWriteJson(storage: Storage, key: string, value: unknown): SaveResult` without breaking old `writeJson` callers.
- [x] Write failing tests for two stores sharing storage: different progress-field edits both survive; stale same-field edit reports conflict; repeated operation is idempotent; removal is a timestamped operation, not a missing-key merge. Inject throwing/quota storage.
- [x] Run `bun test test/reader-store.test.ts`; expect failure.
- [x] Implement read-latest/rebase operations under a Web Lock when available, a revision check/retry fallback otherwise, and storage-event synchronization. Expose conflict instead of silently overwriting. Persist migration recovery before the first reader write. Bootstrap recovery before mounting React; prevent old hook effects writing migrated data. Reconcile post-migration v1 changes by recorded timestamps on next startup, retain deletions in new-state tombstones and preserve raw old values.
- [x] Bind App to the store with `useSyncExternalStore`; retain domain helpers from legacy hooks, remove their mounted persistence effects when v2 is active. Test failed migration write leaves legacy bytes identical and does not mark migration complete. Run named tests, all existing store tests and typecheck; expect pass.
### Task 4: Revision-aware reset clocks and timezone formatting
**Files:** Create `src/shared/readerClock.ts`, `state/reader/clock.ts`,
`state/displayTime.ts`; modify `shared/time.ts`, `daily.ts`, `goal.ts`, `task.ts`;
test `test/reader-clock.test.ts`, `test/display-time.test.ts`.
**Interfaces:** `resolveReaderClock(account: Account, at: number): { revision: string; region: Region; resetHour: number; resetMinute: number; offsetUtc: number }`;
`appendClockRevision(account: Account, reset: ClockRevision["reset"], region: Region, at: string, id: string): Account`;
`formatInZone(instant: number | string, withTime: boolean, zone: string | null): string`.
Pass an optional resolved clock through existing date helpers; absent preserves every old result.
- [x] Write failing tests for Automatic parity across all registered game/server clocks, 05:30 override, revert, explicit `regionEnds`, day-precision starts/ends, custom instants, built-in `clock` and custom task timing. Check two ticks with the same day label in different revisions remain independent/reachable when reset moves backward.
- [x] Run `bun test test/reader-clock.test.ts test/display-time.test.ts`; expect failure.
- [x] Implement injected clock context, leaving stored ids and historical labels unchanged. Revision selection uses action timestamp for logs and the current clock for current deadlines. Capture resolved offsets in history if automatic metadata changes, so past readings stay reproducible. Formatting uses Intl IANA zones, invalid stored zone falls back to Automatic without altering day keys.
- [x] Run named tests plus `test/time.test.ts`, `daily.test.ts`, `goal.test.ts`, `task.test.ts`, `period.test.ts`; expect pass. Document reset revision semantics and displayed-time separation.
### Task 5: Account-aware game settings and real source credits
**Files:** Create `components/redesign/GameSettings.tsx`, `TrackingChoices.tsx`,
`state/reader/accounts.ts`; modify `App.tsx`; test `test/reader-accounts.test.ts`, `test/game-settings.test.tsx`.
**Interfaces:** `createAccount(game: LaneId, region: Region, at: string, id: AccountId): Account`;
`GameSettings({ accounts, sources, onAction, gameMeta })`; `TrackingChoices({ intent, availability, onChange })`.
Source credits consume `EventFeed.sources`, not event rows; never import ingestion adapters into the client bundle.
- [x] Add failing tests: added server begins events-only with empty logs/pins; changing an account region retains its identity/history; duplicate same game/server setup is explained; retiring a server preserves its history. All type choices stay enabled when unavailable and explain why.
- [x] Run `bun test test/reader-accounts.test.ts test/game-settings.test.tsx`; expect failure.
- [x] Implement Setup/Codes/About tabs, default-server selection, fresh server tracking flow and reset hour/minute/Automatic/revert. Colour control edits the game, not an account. Source links include every matching health entry with thanks, including zero-event sources. Codes tab initially shows an honest unavailable state.
- [x] Run named tests and typecheck; expect pass. Browser-check adding a second server without copying the first account's marks.
### Task 6: Routine library, independent pins and direct dailies
**Files:** Create `components/redesign/RoutinesPage.tsx`, `RoutineGroup.tsx`,
`DailyPanel.tsx`, `state/reader/routines.ts`; modify `Dailies.tsx`, `EditPanel.tsx`,
`YourOwn.tsx`, `TaskForm.tsx`, `AmountEditor.tsx`, `App.tsx`; test `test/reader-routines.test.ts`,
`test/redesign-routines.test.tsx`.
**Interfaces:** `routineRows(state: ReaderState, accountId: AccountId, now: number): RoutineRow[]`;
`RoutineRow` contains original item id, account id, title, cadence, current GoalStatus, source,
placement and resolved clock revision. Follow/pin changes dispatch ReaderAction.
- [x] Add failing tests for stop/re-follow preserving logs; independent Checklist/Timeline pins; Make it mine copies the selected account's history only; selected type intent survives empty availability; new built-ins are offered, never automatically followed.
- [x] Run `bun test test/reader-routines.test.ts test/redesign-routines.test.tsx`; expect failure.
- [x] Implement library/Create your own, active routine rows, daily panel/grouped choice and three disclosure policies. Share one display preference between Routines and Appearance. Focus expands groups without saving a different policy. Native numeric inputs use step one and visible browser steppers. Keep custom event/game editing separate from built-in routine management.
- [x] Test the same daily appears exactly once; Always expanded has no collapse button; focus-All restores policy; old-revision history remains accessible in catch-up. Run named tests, existing tasks/dailies/forms tests and typecheck; expect pass. Browser-check one-action daily ticks for two accounts.
### Task 7: Account-aware Checklist and Timeline
**Files:** Create `state/reader/eventRows.ts`, `components/redesign/ChecklistPage.tsx`,
`TimelinePage.tsx`, `RoutineTimelineRows.tsx`; modify `EventRow.tsx`, `EventDetail.tsx`,
`Timeline.tsx`, `NextUp.tsx`, `App.tsx`; test `test/reader-event-rows.test.ts`, `test/redesign-timeline.test.tsx`.
**Interfaces:** `ReaderEventRow = RowEvent & { accountId: AccountId; region: Region; clockRevision: string }`;
`readerEventRows(state: ReaderState, events: readonly DisplayEvent[], now: number): ReaderEventRow[]`.
Use composite React keys/open-selection `{ accountId, eventId }`, preserving the original event id for domain state.
- [x] Add failing tests for simultaneous regions with different end times, per-account Done/Ignore and goals, custom repeated occurrences, outstanding headline/counts and focus. Standard/Compact contain identical event ids/order. Title-only Standard rows omit description-sized decoration.
- [x] Run `bun test test/reader-event-rows.test.ts test/redesign-timeline.test.tsx`; expect failure.
- [x] Extract view coordination from App; retain existing sort/zoom/recurrence semantics. Implement routine full rows and optional reset strips immediately beneath their game's events. With strips and Ending soonest, order game groups by next deadline, maintaining adjacency; without strips retain existing ending queue.
- [x] Run named tests, `test/views.test.tsx`, `lens.test.ts`, `sort.test.ts`, `zoom.test.ts`, `recurrence.test.ts`; expect pass. Browser-check both interfaces, densities, long titles, empty scope, unknown ends and Timeline scrolling.
### Task 8: Durable archive, completion confirmation and restoration
**Files:** Create `state/reader/archive.ts`, `components/redesign/ArchivePage.tsx`,
`ArchiveDetail.tsx`, `state/useArchiveTransition.ts`; modify view/detail wiring;
test `test/reader-archive.test.ts`, `test/archive-ui.test.tsx`.
**Interfaces:** `captureSeen(state: ReaderState, rows: readonly ReaderEventRow[], at: string): ReaderState`;
`reconcileArchive(state: ReaderState, events: readonly DisplayEvent[], now: number): ReaderState`;
`archiveOutcome(state: ReaderState, accountId: AccountId, eventId: string, outcome: ArchiveRecord["outcome"], at: string): ReaderState`.
- [x] Add failing tests: save Done/Ignore immediately; Undo restores prior progress/ignore state; expired unfinished snapshots retain descriptions/source/logs; missing-but-unexpired and unknown-end events remain outstanding; hidden/unfollowed catalogue events do not produce history. Retired accounts remain inspectable.
- [x] Run `bun test test/reader-archive.test.ts test/archive-ui.test.tsx`; expect failure.
- [x] Capture feed events on successful reads before feed replacement, reconcile known expiry, and resolve missing live details from seen snapshots. Implement 1.5-second row confirmation then fade/collapse and eight-second Undo, using cancellable timers. Reduced motion removes animation. Archive Restore does not make an expired event live.
- [x] Test legacy marks with no metadata are preserved and labelled incomplete, unknown ends remain null, repeated Done is idempotent, repeated custom occurrences archive separately. Run named tests and typecheck; expect pass. Browser-verify timing, navigation during delay and Undo.
### Task 9: Archive story statistics and history timeline
**Files:** Create `state/reader/archiveStats.ts`, `components/redesign/ArchiveStory.tsx`,
`ArchiveTimeline.tsx`; modify ArchivePage; test `test/archive-stats.test.ts`.
**Interfaces:** `archiveStats(records: readonly ArchiveRecord[], now: number): ArchiveStats`;
`ArchiveStats` includes monthly/overall eligible/done/rate, current/best completion run, check-ins,
unfinished, known target shortfalls and per-game outcome totals.
- [x] Add failing tests with done/ignored/unfinished/unresolved records, equal timestamps, absent goals, multi-account same dates and empty history. Assert done/(done+unfinished), ignored excluded, no NaN or invented near-miss values.
- [x] Run `bun test test/archive-stats.test.ts`; expect failure.
- [x] Implement pure totals with stable account/event tie-breaks for equal timestamps. Group done by outcomeAt and unfinished by resolved end; future/unknown ends are ineligible. Sum distinct check-ins per event/account; do not label that unique play days. Default body per-game; only real source versions get version labels. Add optional history timeline.
- [x] Run named tests and typecheck; expect pass. Browser-check compact story panel in both interfaces/themes with small and empty archives.
### Task 10: Backup retention, validated restore and recovery
**Files:** Create `state/reader/backups.ts`, `restore.ts`, `components/redesign/BackupPage.tsx`;
modify bootstrap/export wiring; test `test/reader-backups.test.ts`, `test/reader-restore.test.ts`.
**Interfaces:** `retainBackups(snapshots: readonly BackupSnapshot[], now: number): BackupSnapshot[]`;
`BackupSnapshot = { id: string; at: string; payload: object }`;
`prepareRestore(storage: Storage, payload: unknown, at: string): SaveResult`;
`finishPendingRestore(storage: Storage): SaveResult`; `undoRestore(storage: Storage): SaveResult`.
Use Task 2 export validation. Recovery and pending transaction keys are excluded from payload recursion.
- [x] Add failing retention tests spanning 400 simulated days: disjoint seven daily/four seven-day/six calendar-month buckets, month/year/leap boundaries, latest-per-bucket, missing days and timezone changes. Assert at most seventeen retained snapshots.
- [x] Add failing restore tests for malformed/unknown-version exports, quota at each write, interrupted startup, current-state recovery, undo and reader state with unreadable preserved fields. Run both named files; expect failure.
- [x] Implement rolling retention on app use and debounced successful data changes; no unchanged-state snapshot loop. Write recovery and prepared envelope before replacing live reader state. Bootstrap completes validated pending restore before mounting stores, and preserves pending data on failure. Never claim success after failed persistence.
- [x] Implement View backup → contents preview → Restore this backup; manual Back up now, export/import and Undo last restore. Preserve progress-only versus Import all semantics, including account identity mapping and imported custom dependencies; warn before overwriting ambiguous account mappings.
- [x] Run both named files, export tests and typecheck; expect pass. Browser-test real download/upload, restore and storage-failure feedback. Document local-only lifetime and no backup on unused days.
### Task 11: Appearance, custom colours and Region & time pages
**Files:** Create `state/appearance.ts`, `components/redesign/AppearancePage.tsx`,
`TimeSettings.tsx`; modify `state/theme.ts`, `state/gameMeta.tsx`, `scripts/build-static.ts`,
`redesign.css`; test `test/appearance.test.ts`, `test/appearance-ui.test.tsx`.
**Interfaces:** `appearanceTokens(prefs: Record<string, unknown>, theme: Theme): AppearanceTokens`;
`readableOnSurfaces(hue: string, backgrounds: readonly string[], minimum: number): string`.
Token values cover both interface styles, schemes and themes; minimum is 4.5 for small text.
- [x] Add failing tests for interface/scheme/density independence, malformed saved palette, same background/text, colours legible on page and panels, game overrides shared across accounts and Reset to default. Include System appearance changes without losing custom selections.
- [x] Run `bun test test/appearance.test.ts test/appearance-ui.test.tsx`; expect failure.
- [x] Implement Slate/Warm/Plum/Custom, separate light/dark base edits and derived contrast-safe rendered colours. Apply pre-paint appearance using the reader envelope so reload does not flash a different UI. Reuse Task 4 timezone formatting throughout details/archive/forms; custom-entry copy names its interpretation zone.
- [x] Run named tests, existing theme tests and typecheck; expect pass. Browser-check custom equal colours, semantic urgency, native colour/number controls, IANA search and daylight saving.
### Task 12: Shared updates, data sources and About/Changelog
**Files:** Create `state/reader/updates.ts`, `components/redesign/UpdatesPanel.tsx`,
`DataSourcesPage.tsx`, `AboutPage.tsx`, `FeedFooter.tsx`; modify `Colophon.tsx`,
`WhatsNew.tsx`, `App.tsx`; test `test/redesign-updates.test.tsx`, `test/reader-updates.test.ts`.
**Interfaces:** `updateItems(state: ReaderState, feed: EventFeed, now: number): UpdateItem[]`;
`UpdateItem` is a discriminated union of event-newness, code-newness, release and source-age rows.
`FeedFooter({ sources, now, online, onRefresh, onAbout })` always retains feed-age disclosure.
- [x] Add failing tests: followed-games source scope independent of focus; one fresh sibling does not conceal old source; never-fetched/unchanged/overdue/unknown reasons; code/events/release markers clear independently; no empty updates panel.
- [x] Run `bun test test/redesign-updates.test.tsx test/reader-updates.test.ts`; expect failure.
- [x] Move notices into shared compact rows above Checklist/Timeline, using exact source-age copy from spec. Show source detail timestamps honestly; do not invent failure causes absent from feed. Preserve Check for new data's result/offline semantics. Mark seen event ids only after the relevant content has been presented; initial migration seeds already-known event ids to avoid announcing the whole calendar.
- [x] Extract full colophon to About, retain all credits unfiltered, and keep short age/About footer without Changelog. Lists contains actual list settings, no Done/ignored archive-link row. Run named tests, existing refresh/changelog tests and typecheck; expect pass.
### Task 13: New and returning reader onboarding
**Files:** Create `components/redesign/SetupWizard.tsx`, `ReturningTour.tsx`,
`state/reader/onboarding.ts`; modify `Welcome.tsx`, `App.tsx`;
test `test/redesign-onboarding.test.tsx`, `test/reader-onboarding.test.ts`.
**Interfaces:** `setupReader(draft: SetupDraft, existing: ReaderState, at: string, mint: () => AccountId): ReaderState`;
`SetupDraft` contains default region, interface/theme/scheme/timezone, selected games, per-game
TrackingIntent, showUpcoming and preferred `View`. Saved tour marker `redesignTourSeen: "2026-10-redesign"` is separate from changelog markers.
- [x] Add failing tests: initial events-only intent and upcoming off; unavailable cycles selectable; initial supported selections opt in deliberately; additional server uses fresh setup; new-user defaults never overwrite migrated answers. Skip/Back/finish preserve state and return to original main view.
- [x] Run `bun test test/redesign-onboarding.test.tsx test/reader-onboarding.test.ts`; expect failure.
- [x] Implement short preference/game/tracking/view steps and inline type explanations. Upcoming control appears only in tracking step. Persist completed setup once. Returning welcome/tour is offered once on either preferred main view; skipping records dismissal, with a replay link in General.
- [x] Run named tests, existing welcome/prefs tests and typecheck; expect pass. Browser-check both entry paths, keyboard, backtracking and reload after completion.
### Task 14: Deterministic Genshin codes source and published contract
**Files:** Create `src/shared/codes.ts`, `src/ingest/codes/game8.ts`, `registry.ts`,
`build.ts`, `scripts/build-codes.ts`, `fixtures/genshin/game8-codes-2026-10-16.html` and
matching expected JSON; modify refresh orchestration, build scripts, package scripts, static server
and service-worker feed handling; test `test/codes.test.ts`, `test/adapters/game8-codes.test.ts`,
`test/codes-refresh.test.ts`.
**Interfaces:** `RedeemCode = { id: string; game: GameId; code: string; reward: string | null; expiresAt: string | null; firstObservedAt: string; sourceUrl: string }`;
`CodesFeed = { schemaVersion: 1; codes: RedeemCode[]; sources: SourceHealth[] }`;
`parseGame8Codes(html: string, ctx: ParseContext): Omit<RedeemCode, "firstObservedAt">[]`;
`buildCodesFeed(previous: CodesFeed | null, parsed: readonly Omit<RedeemCode, "firstObservedAt">[], at: string): CodesFeed`.
Use code identity `JSON.stringify([game, code])`, preserving literal code case/punctuation from the source.
- [x] Read INGESTION/SOURCES and the real Game8 page/robots through the approved source workflow. Fetch at most once after checking existing cache and six-hour floor; pin actual bytes and independently count the page. If access is refused, do not bypass it; record that condition and seek owner-provided bytes for the fixture rather than fabricate data.
- [x] Add failing tests from pinned bytes: active versus expired sections, unknown reward/expiry, duplicate codes, HTML entities, changed page shape, stable first-observed date across unchanged rebuilds, and failed fetch retaining the previous feed.
- [x] Run `bun test test/codes.test.ts test/adapters/game8-codes.test.ts test/codes-refresh.test.ts`; expect failure.
- [x] Implement pure parser plus a separate `data/codes.v1.json` feed. Reuse robots, conditional-fetch, SnapshotStore, host spacing and failure reporting; do not weaken event Adapter's GachaEvent return contract. The unified refresh scheduler must share host spacing across both source kinds. Offline builds use real fixtures only. Codes do not alter existing event feed ids or event counts.
- [x] Run named tests and offline build; expect pass. Update INGESTION/SOURCES/ARCHITECTURE and workflow copy/build paths so the feed is deployed and cached. Network fetch is not part of tests.
### Task 15: Code pages, discovery notices and redemption
**Files:** Create `components/redesign/CodesPage.tsx`, `state/reader/codes.ts`;
modify `api.ts`, GameSettings and UpdatesPanel; test `test/reader-codes.test.ts`, `test/codes-ui.test.tsx`.
**Interfaces:** `newCodes(feed: CodesFeed, seen: readonly string[], game: GameId): RedeemCode[]`;
`fetchCodesFeed(signal?: AbortSignal): Promise<CodesFeed>`.
Viewing writes `codeSeen[game]`; redemption writes only `codeRedeemed[accountId][codeId]`.
- [x] Add failing tests: viewing clears newness without redemption; redeeming one account leaves another unredeemed; expired known code does not become new again; unavailable codes feed leaves events usable; code arrival does not clear new events/release notices.
- [x] Run `bun test test/reader-codes.test.ts test/codes-ui.test.tsx`; expect failure.
- [x] Bind game Codes page and in-app notices to the real feed, with copy control, source/reward/known-expiry fields and per-account redeemed state. Show honest empty/unavailable/stale states, never sample codes. Update refresh comparison to account for code changes separately from event-feed build stamps.
- [x] Run named tests and typecheck; expect pass. Browser-check feed update, Copy, view-seen behaviour and independent two-server redemption; verify exports/backups include state.
### Task 16: Built-app verification, documentation and final independent review
**Files:** Update `docs/PRD.md`, `DATA-MODEL.md`, `ARCHITECTURE.md`, `DESIGN.md`,
`PRODUCT.md`, affected AGENTS guidance, `TODO.md`; record evidence in
`docs/design-reviews/2026-10-16-redesign-implementation.md`. Keep browser tooling in `/tmp`.
**Interfaces:** All previous tasks' contracts; no new domain API.
- [x] Run `bun run typecheck`, `bun test`, `git diff --check`; expect all pass. Read full failures, not only the command exit status. Confirm tests run without docs/public build output, using the repository's Docker-layout check when needed.
- [x] Run `bun run build:css`, `bun run build:js`, `bun run build:static`; serve on a fresh distinctive port with an existing/copied real feed. Inspect Chrome screenshots for 390/1440px, both interfaces, both themes, Standard/Compact, 200% zoom and reduced motion. No horizontal page overflow, clipped panels or console errors.
- [x] Browser journeys: legacy-save migration; first and returning onboarding; two servers; reset override/backward-day/revert; direct dailies; routine pin/disclosure; event detail/edit; completion/Ignore/Undo; archive/history/stats; source-age scenarios; custom palettes/timezone; real code discovery/redemption; export/import/restore/undo; offline reopening and update prompt.
- [x] Perform a fresh whole-change review using the native execution skill's final reviewer. Review data-loss paths, source honesty, storage failures, shared contracts and all five Review Focus items. Fix critical/important findings with reproducing tests and rerun the suite; record remaining minor findings explicitly.
- [x] Finalize shipped docs to match actual behaviour and both visual systems. Report changes, evidence and limitations. Leave work uncommitted and unpublished until the owner requests those actions; no merge/branch cleanup is needed on main.
## Execution record and handoff
Use `.superpowers/sdd/2026-10-16-ui-redesign/progress.md` for task status, verification evidence
and rulings. A task is complete only after its named checks pass. User selected Native execution;
do not ask for the method again. The writing-plans skill requires owner review of this concrete
plan before implementation begins. After that review, execute all tasks continuously, with status
updates and no further per-task approval prompts. The final review is the only planned delegated
work; implementation stays native.
Self-review: all approved spec sections map to Tasks 1–15; migration, failure recovery and source
integration have explicit gates. Shared contracts are declared above and used consistently.
Task 14 has an honest external prerequisite: real source bytes and policy validation. A refused
source is not permission to invent data or declare the codes feature finished.
Completion evidence: [implementation review](../../design-reviews/2026-10-16-redesign-implementation.md).
All 16 feature/verification tasks are complete, uncommitted. The ledger records staged activation,
wrapper-file substitutions, review fixes and the stronger IndexedDB fallback in place of an unlocked
revision retry. Some expanded render checks passed immediately after their already-implemented
behaviour; they are coverage evidence, not claimed RED runs. No commit/push/publication performed.
## Fidelity correction follow-through — 2026-10-17
The original completion statement above describes the rejected first implementation. The subsequent
correction follows the approved reference composition and the owner's new Structured-default ruling.
See `docs/design-reviews/2026-10-17-redesign-fidelity.md` for matched screenshots, independent findings,
resolved verdicts and final functional verification. Final review cleared the reported corrections;
owner acceptance is not inferred from that review. All changes remain uncommitted and unpublished.
Settings now places the replay control under Default view, replacing the previous General page.
@@ -0,0 +1,267 @@
# Event Clock redesign: Relaxed and Structured
Date: 2026-10-16. Status: approved requirements; both the initial implementation and the 2026-10-17 fidelity correction were rejected by the owner. Current continuation instructions: `docs/handoffs/2026-10-17-redesign-second-rejection-handoff.md`. Launch the current server, then ask the owner for the biggest differences before further UI changes.
Owner amendment, 2026-10-17: Structured is the default; preserve explicit saved Relaxed.
## Intent and approval record
Make several games' events and routines easier to scan and act on, while retaining trustworthy
deadlines and every existing reader choice. The owner approved the Round 7 visual direction,
then explicitly chose **two UIs**, reversing the preceding single-UI decision. The second is
**Round 6 B**, confirmed in conversation. Both have the same capabilities and data.
Visual references:
- [Relaxed: Round 7](../../../design/mockups/round7/converged.html)
- [Structured: Round 6 B](../../../design/mockups/round6/b-rail.html)
- [Accumulated review decisions](../../design-reviews/2026-10-16-redesign-round4.md),
[Round 5](../../design-reviews/2026-10-16-redesign-round5.md),
[Round 6](../../design-reviews/2026-10-16-redesign-round6.md),
[Round 7](../../design-reviews/2026-10-16-redesign-round7.md)
The prototypes are visual/interaction references, not production code. Sample codes, archive
history, source failures and backups must never become real reader data.
## Product choices
Settings → Appearance offers **Interface: Relaxed / Structured**, with preview thumbnails and
short descriptions. Relaxed is the proposed default for new and upgraded installs; existing
Dark/Light/System choices remain intact. Relaxed uses Round 7's separated panels, softer surfaces,
Bricolage Grotesque/Figtree typography and generous gutters. Structured uses Round 6 B's ruled
layout and Barlow typography. Both put the current-style game focus pills first in the left rail.
Switch immediately, remember the choice, and preserve the current page, focus and progress.
Interface, Dark/Light/System, colour scheme and Standard/Compact event rows are independent
preferences. Standard is the default row density. Content determines row height: missing
descriptions reserve no blank line and receive no fabricated replacement text.
Use a shared component hierarchy with interface-specific tokens and narrowly scoped styles.
Duplicating the app's React tree or maintaining two separate stores would double behavioural
maintenance. A spacing-only switch would not reproduce the two approved visual directions.
## Navigation, settings and updates
Main pages: Checklist, Timeline, Routines, Archive, Settings. Preserve the existing stored
Checklist view id `soon` and Timeline id `timeline`. Visiting a utility page must not overwrite
the preferred main view. Settings has its own navigation, beginning with General, and includes
Your games, Appearance, Lists, Region & time, Data & sources, Backup, About and Changelog.
Keep game focus behaviour from `GameFocus.tsx`: All/game pills, outstanding counts, tap the
selected game to return to All, Next through games and back to All. Hide the picker for one game.
Focus never changes followed games. Game ordering remains the reader's order, otherwise A–Z.
Checklist and Timeline share a compact updates area. New code notices, events new since the
reader's last visit, release news and source-age notices have distinct labels and state. Do not
clear one by acknowledging another. Omit empty update kinds and the entire area when empty.
Replace **Older schedules** with **Event data may be out of date**. Supporting copy names the
affected followed games: “For games you track: Genshin Impact, …”. **Details & reasons** opens
Data & sources. Show this row only when `freshness()` reports an affected followed game;
temporary focus does not narrow it. Do not add a large warning banner or repeat it on every row.
Reuse the real 48-hour freshness rule and oldest-source-per-game semantics. The footer always
states feed age from source data, never `generatedAt`, with Check for new data. The button requests
the published feed; it cannot force wiki scraping. Source details retain last successful data,
last confirmed check, content-change time, source links and known reasons. A recent check with
unchanged content is different from an overdue check. If no failure reason is recorded, say
“The reason for the delay is unavailable”; do not infer blocking or network failure. Offline
status is distinct from source age.
About retains the colophon, Lucas Winther attribution, contributor acknowledgements, all source
communities and studios, reset-clock reference, disclaimer and project links. Credits are not
filtered by followed games. Changelog is a Settings page, with no footer link or About button.
Existing release notices link to that page.
## Routines and game setup
Routines owns follow/unfollow, built-ins, Create your own routine, Make it mine, editing and
independent Checklist/Timeline pins. A follow controls tracking; a pin controls placement.
Stopping a routine preserves its history. Reader-authored games, events and task forms remain
fully functional; prototype placeholders do not satisfy this requirement.
Routines → Checklist display controls Separate daily panel / Grouped with routines, also exposed
in Appearance as the same preference. Separate is the proposed default. A daily tick appears in
one place, is reachable with one action, and remains distinct per tracked server.
Routine group choices: Collapsed by default, Expanded by default, Always expanded. Default is
Collapsed. A single-game focus expands its groups; returning to All restores the chosen policy.
Always expanded removes collapse controls. Numeric inputs retain native steppers and `step=1`.
Game settings contains Setup, Codes and About. Setup offers Events / Dailies / Weeklies &
monthlies / Cycles, plus individual routine choices. These types compose; events are visibility,
not a routine. Save type intent separately from current follows. Unsupported or currently empty
types remain selectable with an explanation. New built-ins matching saved intent are offered
for review, never silently followed on an established install.
“Track another server” explains: “Play on more than one server? Add your other server to track
both accounts separately.” It creates fresh choices with Events selected initially, and asks
all tracking questions again. It copies no ticks, follows or pins. Changing an existing server
is a different action and preserves that account's recorded history.
Game colour is shared across its servers, editable with picker/hex/preview/Reset to default.
About lists every registered data source for that game with links and thanks to its editors,
including event, banner and pass sources even when a source currently contributes no events.
## Appearance and clocks
Slate, Warm, Plum and Custom schemes are available in each interface. Custom edits background,
surface, text and interface accent separately for light and dark. Retain chosen base colours;
derive readable rendered text, game hues and semantic colours against their actual surfaces.
Keep game identity distinct from urgency. Small text must clear 4.5:1 in both modes and UIs.
Fonts must have usable offline fallback; do not make rendering depend on Google Fonts loading.
Display timezone defaults to Automatic, resolved from the browser. Allow searchable named IANA
zones and Revert to Automatic. Display timezone changes formatting, never server periods or
stored day keys. Custom event entry states the timezone in which its date/time is interpreted.
Per tracked game/server, reset timing defaults to Automatic and permits a custom server-local
hour/minute, with Revert to Automatic. Reuse real game/server offsets, regional hours and
built-in clock overrides. Explicit timestamps, `regionEnds` and reader-entered instants remain
authoritative; only reset-based/day-precision boundaries use the resolved reset clock. A routine
with its own explicit clock keeps it. No change to the global games table is needed for reader
overrides.
Reset changes are effective from their saved timestamp. Preserve a clock revision history per
tracked account and attach the revision to new logs. Previously stored day/period labels are
never re-keyed or silently interpreted under a different clock. Legacy logs belong to the initial
automatic-clock revision. Historical summaries must retain access to all revisions; transition
overlap is displayed as recorded history rather than merging or dropping ticks. Reverting to
Automatic starts another revision and preserves earlier records. Test the boundary immediately
before and after a change, including a change that moves the current game-day backward.
## Onboarding
New-user setup starts with events only. Ask default game server, appearance and timezone, then
games and What do you want to track?, then preferred Checklist/Timeline. **Show events that
haven't started** belongs in the tracking step and begins off. Explain types inline, especially
cycles: routines repeating on a game's cadence, such as every fourteen days. Explain “no active
window” separately from “no supported built-in”. Initial setup may follow currently supported
items deliberately selected by type; subsequent additions require review. Defer pin decisions
until readers can see the views. Do not change old installs to the new-install defaults.
Returning readers receive a one-time release-specific welcome, available in either main view,
with Take the quick tour / Keep browsing. The tour visits Routines, Checklist, Archive and
Settings, with Back and Skip, then returns to the reader's preferred main view. Persist a marker
separate from changelogSeen. Opening or skipping the tour changes no games, routines or progress.
## Archive and statistics
On Done or Ignore, persist the outcome and event snapshot immediately. Show “Completed · Moved
to Archive” or “Ignored · Moved to Archive” in the row for about 1.5 seconds, then fade/collapse.
Keep Undo for about eight seconds. Reduced motion removes the fade, retaining the confirmation.
Archive Restore is always available; restoring an ended event restores its recorded status,
but does not pretend it is running again.
Retain event metadata, description, source, dates/precision, region-specific ends, account,
progress and outcome timestamp. Unknown end dates remain null. Cache snapshots of seen events
before feed replacement so expired unfinished events can be recorded when they leave the live
feed. Missing from a feed is not proof of expiry: retain an outstanding snapshot until its known
deadline passes; unknown-end events remain unresolved and must not become unfinished by guess.
Only archive events for tracking periods the reader actually followed, not hidden catalogue games.
Default Archive body is per-game, with version groups only where the source states a version.
History timeline is an optional alternate body. The story panel shows monthly and overall
completion rate, current/best completion run, event check-ins, unfinished counts and per-game
outcome bars. Rates use done / (done + ended unfinished), excluding ignored and unresolved.
Use outcome time for done and resolved end time for unfinished when grouping by month. Completion
runs order those eligible outcomes by that time; ignored entries neither extend nor break them.
Event check-ins sum recorded distinct days per event/account, not unique calendar days of play.
Near-miss detail reports an actually recorded goal shortfall, only where a known target exists;
no invented threshold or estimated play progress. Label all statistics as recorded archive history.
Old marks whose events are no longer available remain preserved but cannot acquire fabricated
titles, deadlines or descriptions. Explain that earlier history may be incomplete.
## Persistent state, migration and backup
Keep existing event IDs, daily IDs, task IDs and day-key formats. Introduce stable tracked-account
identities around game/server choices; do not turn a region change into a different event ID.
Legacy progress, daily logs, ignores, task amounts and routine choices are associated once with
each game's initial account at the reader's saved region. A migration marker records the mapping.
Retain legacy key spaces and unknown/unreadable records; migration is idempotent and does not
duplicate logs on reload. Additional accounts get independent state. Existing global preferences,
game visibility/order, custom content and view survive. Migration must work for readers returning
after months and for old exports, not only the current developer browser.
New state includes account configurations/logs, routine follows/pins and tracking intent, archive
snapshots, reset revisions, appearance/timezone preferences, update-seen markers and code
redemptions. Document concrete schemas and keys in DATA-MODEL with the storage implementation.
Use a validated versioned full-state envelope for backup and restore; accept existing version-1
exports through a tested legacy reader. Preserve the existing distinction between progress import
and Import all. A new export must carry all new reader-owned state. Do not recursively include
backup history inside each backup.
Automatic backups run on app use and after settled data changes. Use a stable UTC calendar basis,
independent of display timezone. Keep latest snapshot per UTC day for today and the six previous
days; older snapshots fold into four consecutive seven-day windows; older snapshots then fold
into six calendar-month buckets measured backward from the oldest weekly window. Buckets are
disjoint; retain the newest snapshot in each and discard anything older. A partially covered
calendar month at the weekly boundary retains only eligible older snapshots. Maximum seventeen
retained snapshots, plus one pre-restore recovery copy. Never invent snapshots for unused days.
Backup rows say **View backup**. Preview shows date and contents; **Restore this backup** is the
explicit action. Validate the entire payload before changing live state, save current data as a
recovery copy, and support Undo last restore. Make multi-key restore recoverable using a prepared
transaction envelope; startup must resolve an interrupted restore before normal store writes.
Storage writes must report success/failure rather than relying on today's silently failing
`writeJson`. If persistence/quota prevents a recovery copy or complete restore, leave current
data intact and offer export. Failed backup retention writes must not delete the last good copy.
Explain that browser-local backups disappear if browser storage is cleared; export is the copy
outside this browser. No accounts or remote sync.
## Codes and source boundaries
Codes belong in game settings and notify in-app in both main views. Discovery/newness is per game
and independent from redemption per tracked account. Viewing a game's codes clears its new
indicator, never redemption. Only declare a code new when present in a successfully read feed,
not because its redemption checkbox is clear. Preserve first-observed metadata across builds.
The supplied Genshin Game8 codes URL is a source candidate, not a shipped feed. A deterministic
codes source is a separate ingestion sub-project with its own source/robots review, fixture,
parser/schema and refresh integration. It must follow existing scraping rules and publish only
source-stated code/reward/expiry facts; unknown expiry stays unknown. The UI implementation
includes a real empty/unavailable state and never ships sample codes as real. Integrating this
source is required to finish the codes feature, but must not delay validating the shared UI shell.
## Implementation boundaries and delivery order
Use production React components, pure state/selectors and existing Bun tooling. Extract page
coordination from App and settings from Controls as needed for the actual new page boundaries;
avoid unrelated refactors. No added runtime dependencies, accounts, push service, SQLite work,
parser rewrites or prototype JavaScript transplanted into production.
Deliver in independently verifiable stages:
1. Shared shell, both interfaces, settings/navigation, typography and responsive layouts.
2. Validated state/envelope, account migration and reset revision machinery with recovery tests.
3. Routines, game setup, independent pins, direct dailies and clock-aware views.
4. Archive retention/outcomes/statistics and automatic backup/restore.
5. Onboarding, shared updates/source details, credits/Changelog, themes/timezone/custom colours.
6. Deterministic code ingestion and real code notices/redemption, then complete end-to-end review.
The detailed implementation plan must define file-level tasks and concrete storage schemas before
stage 2. Each stage preserves a runnable app and documents what has actually landed. No staged
delivery is a claim that the complete redesign is finished.
## Verification and documentation
Test migrations from realistic legacy saves and exports, idempotence, independent account progress,
reset changes/reverts at boundaries, null ends, vanished feed rows, archive denominator/ordering,
backup folding across calendar boundaries, quota failures and interrupted restores. Test code
newness independently from redemption and source-age scope independently from focus. Existing
date/parser/ID tests must remain green. Tests create their own fixtures and never require public
build output, repository docs or network.
Run typecheck and the full offline suite; build CSS/JS/static shell with production JSX settings.
Check actual built app in Chrome on a new port for each rebuild at phone and desktop widths,
both interfaces, both themes, Standard/Compact, keyboard/focus, reduced motion and 200% zoom.
Exercise custom palette contrast on all used surfaces, no horizontal page overflow, one-action
dailies, timeline reset adjacency, both onboarding paths, archive Undo/Restore and backup recovery.
Verify offline reopening and the real update flow without losing progress.
Update PRD, DATA-MODEL, ARCHITECTURE, DESIGN, PRODUCT and affected AGENTS guidance with each
implemented stage. Source integration also updates INGESTION/SOURCES. The shipped documents
continue to describe shipped behaviour until that implementation exists. Add changelog/release
notes under the repository's normal commit workflow when authorized; do not commit or push merely
because this specification has been written.