Hidden state machines in Chorslo
Seven places where a mode lives in scattered flags, timers or status text. Six have a failure you can reach today. Each gets a version with named states that removes it.
Claude audit of main at 9bbcb99. Every failure below is traced by reading the code. Nothing was run or reproduced on a wall.
- A flag the code tracks on its own
- A timer guessing when something ends
- Reachable today
- A named state
- StreaksA broken streak keeps showing its old numberS · low risk
- Token counterNever settles after a reward gameM · low risk
- Fun taskAn abandoned fun task returns in the next routineS · medium risk
- Wake-upA failed save celebrates, then leaves a dead buttonS to M · medium risk
- Mission screenDone is refused on every tap after a reloadM · medium risk
- Parent accessReturning to the admin tab restarts 26 parent checksM · higher risk
- Mission day9 statuses, 4 different lists of "finished"L · highest risk
Streaks
A broken streak keeps showing its old number.
Today
The wall shows the count and never reads the date. The product doc says a broken streak "just stops showing."
Explicit
One rule decides alive or broken. The wall and the save both use it, so the wall only shows a streak that is alive.
Question for you: "yesterday" means the calendar day. A weekday-only routine reads Friday as two days ago on Monday, so its streak restarts every week and can never reach 7. Is that what you want?
Token counter
After a reward game, the token counter never settles.
Today
Until that kid's next earn, the wall skips app updates and replays the count-up whenever the balance changes.
Explicit
Events move it forward. The seven timers and their guessed durations go away.
Fun task
An abandoned fun task comes back in the next routine.
Today
The timer returns with a fresh countdown, and Done submits the old task. The closed stale-task ticket fixed this for regular tasks only.
Explicit
One rule covers every screen that holds a task. A failed save goes back to the task with Done still there.
Wake-up
A failed wake-up save still celebrates, then leaves a dead button.
Today
At 10:00 the gate lapses and Good Morning covers the whole wall until someone dismisses it.
Explicit
A failed save returns to a live button. Good Morning needs a saved wake-up.
Mission screen
Exit an untimed Mission, reload the wall, and Done is refused on every tap.
Today
The Mission is paused on the server. The wall forgot the kid exited, so it shows Done with no Resume. Starting and ending Screen Time does the same as a reload.
Explicit
One function names the screen. The stage and the update check read the same answer.
Parent access
Coming back to the admin tab restarts 26 separate parent checks.
Today
While they run, Token Log blanks and reloads and Store setup drops unsaved edits. A network blip reads as signed out.
Explicit
One answer, read by every page. A recheck keeps the page on screen. A failed check gets a retry, with no login redirect.
Your call before any build: when a recheck fails, does the page stay up or come down?
Mission day
A Mission day has 9 statuses, and "finished" is spelled 4 different ways.
Today
No failure traced. The cost lands on the next change: two expiry lists already leave out selected, and the write to completed accepts any starting status.
Explicit
The 16 allowed moves, written once. Anything else fails loudly. This table is the audit's reconstruction and needs a check against production rows first.
Also found
| Where | Today | Explicit version | Size |
|---|---|---|---|
| Today's task list | A task added or a day changed at 3pm reaches the wall at the next reload or midnight. Cleanup is hand-written in six database functions. | One sync function that every routine edit calls. | M |
| Token balance | Three separate calculations of the expected balance. On Nov 7 the app's copy stopped at 1,000 rows and showed a false mismatch. | One total in the database, read by the banner, the Fix button and the script. | S to M |
| Give and take tokens | No request ID, so a retry after a dropped response can apply twice. The "retry the same request" pattern is hand-built about a dozen times elsewhere. | One shared command hook, then a request ID on the token commands. | S, then M |
| Purchases | The browser picks which accept command to run, and every refusal comes back as a bare "false". | One resolve command that names its refusal reason. | S to M |
| Mission cancel | "Cancel the run, give the Mission back" is copied in six database functions. Two more skip the second half. | One cancel function. | M |
| Routine editor | New routines save in order with retry. Saved routines send unordered writes and fail silently. | One save queue for both. | M |
| Reward games | Seven games each rebuild wait, play, settle, report once. Pick a Box can restart its reveal on a background refresh. | A stable callback first, then one shared lifecycle. | S, then M |
| Strip view | The "which screen" ladder is written twice and kept in sync by a comment and tests that read source text. | One function and a switch that must cover every case. | S |
| Overlay queue | Steps are typed. The queue mechanics live inside the page component. | A small reducer with real tests. | M |
| Routine completion | "Finished on time" exists only in one tap's response. A lost response means no celebration, streak or reward game. | A stored completion row. | M to L |
| Routine delete | The docs say never delete a routine directly. The database allows it and deletes the streaks with it. | Revoke delete and guard the two lifecycle columns. | S |
| Station channel | Free text. A typo means the frame quietly never updates. | A database check on the two valid values. | S |
Three parts of the repo already work this way and are the pattern to copy: the Screen Time alert reducer (screenTimeAlertContracts.ts), the Done button flow (taskDoneFlow.ts) and kid archiving (admin_kid_lifecycle). About 60 other places were checked and left alone.