Some UI bugs pass every usual check: the handler exists, each function works, nothing throws, the types match. The button still does nothing, because a later call quietly resets what an earlier one set. This audit reads the code and follows each control's calls in order against a map of what every state action changes. It reads code; browser-verify drives the real UI.
When to use
- A button or toggle "does nothing" and no error shows
- Users report broken controls that debugging didn't find
- After changing a shared store action or refactoring shared state, and before release on key flows
Not for API bugs (use bug-diagnosis), layout, or performance.
Steps
- Scope it: one page, every caller of one changed store action, or the whole app (one agent per area, after the store map exists).
- Map the stores first. For each action or setter in scope (a Zustand store, Redux slice, React context, signal or service), note what it sets and what it resets as a side effect. Flag resets of fields another action owns.
- List the touchpoints: every click, change, submit and key handler in scope, with file and line.
- Trace each handler in order. For each call: what it reads, writes and resets, and what it triggers (effects, watchers, subscriptions).
- Check for the six patterns below: a later call undoes an earlier one; async calls finish in the wrong order; a stale captured value; the label promises an action the handler never takes; a branch that can't be reached; an effect that reverts the change.
- Compare the final state with the label. "New email" must end with the compose view open.
- Report each bug in the format below with a fix. Confirm it with browser-verify, and add a test that would have caught it.
Rules
- Build the store map before auditing any control. The bug is usually invisible without it.
- Follow calls into store actions and effects; don't stop at the handler body.
- Every finding cites file and line and shows the trace. No trace, no finding.
- Reading finds candidates. A bug is confirmed by reproducing it, in a test or the browser.
Store map
STORE: emailStore
setComposeMode(bool) sets {composeMode}
selectThread(t | null) sets {selectedThread, messages, drafts, summary}
RESETS {composeMode: false, composeData: null}
DANGEROUS RESETS (clears state another action owns)
selectThread resets composeMode (owned by setComposeMode)
For a large app, build the map once and share it: every area's audit needs it as input.
The six patterns
Examples use React with a Zustand-style store; the same shapes appear in Vue, Svelte, Angular and plain stores.
1. Sequential undo. A later call's side effect reverses an earlier call.
setComposeMode(true); // composeMode = true
selectThread(null); // side effect: composeMode = false
2. Async race. The final state depends on which request finishes first.
fetchA().then(() => set({ loading: false }));
fetchB().then(() => set({ loading: true }));
3. Stale value. A captured value is reused after it changed.
setCount(count + 1);
setCount(count + 1); // same stale count: adds 1, not 2
4. Missing step. "Save" only validates. "Delete" sets a flag but never calls the API. "Send" posts to a removed endpoint.
5. Dead branch. The real work sits behind a condition that is always false at that moment.
6. Effect interference. The button sets a value, and an effect watching that value sets it straight back.
Report format
CLICK-PATH-001 severity: CRITICAL | HIGH | MEDIUM | LOW
Touchpoint: "New email" button, ThreadList.tsx:42
Pattern: Sequential undo
Trace:
1. setComposeMode(true) sets {composeMode: true}
2. selectThread(null) RESETS {composeMode: false} <- conflict
Expected: compose view opens
Actual: nothing changes; composeMode ends false
Fix: call selectThread(null) first, or stop selectThread resetting composeMode
Example: the New Email button
The handler called setComposeMode(true) and then selectThread(null). The store's selectThread cleared composeMode along with the thread fields. Each call worked alone. Ordinary debugging passed it: the handler was wired, both functions existed, nothing crashed, the types were right. The store map showed the reset, the trace showed the true-then-false sequence, and the final state contradicted the label.
Source
Rewritten from click-path-audit in ECC by Affaan Mustafa (MIT licence, notice in LICENSE.txt), upstream commit d2ef268.