← library
skill✅ Testing & QAv1 · published 2026-11-10 · updated 2026-11-10

Click Path Audit

Traces every button, toggle and form submit in an area through each state read and write in order, after first mapping what every store action sets and silently resets, to find handlers that work call by call but cancel each other out, end in a state the label doesn't promise, or race. Use when a button "does nothing" with no error, when users report broken controls that debugging didn't find, or after changing a shared state store or refactoring shared state.

By Uplift Duo

Run it as a prompt

Paste this into any AI agent, or fetch it: curl -s https://uplift.page/api/v1/prompts/click-path-audit/raw

prompt.md
# Click Path Audit

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

1. **Scope it:** one page, every caller of one changed store action, or the whole app (one agent per area, after the store map exists).
2. **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.
3. **List the touchpoints:** every click, change, submit and key handler in scope, with file and line.
4. **Trace each handler in order.** For each call: what it reads, writes and resets, and what it triggers (effects, watchers, subscriptions).
5. **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.
6. **Compare the final state with the label.** "New email" must end with the compose view open.
7. **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

```text
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.
```js
setComposeMode(true); // composeMode = true
selectThread(null);   // side effect: composeMode = false
```

**2. Async race.** The final state depends on which request finishes first.
```js
fetchA().then(() => set({ loading: false }));
fetchB().then(() => set({ loading: true }));
```

**3. Stale value.** A captured value is reused after it changed.
```js
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

```text
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.

Install it as a skill

Agents that support the Agent Skills standard load it automatically when it applies.

download
curl -fsSL https://uplift.page/p/click-path-audit/SKILL.md --create-dirs -o .agents/skills/click-path-audit/SKILL.md
or from any MCP client
MCP server: https://uplift.page/mcp
Tool: pull_skill  {"slug": "click-path-audit"}

The full skill

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

  1. Scope it: one page, every caller of one changed store action, or the whole app (one agent per area, after the store map exists).
  2. 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.
  3. List the touchpoints: every click, change, submit and key handler in scope, with file and line.
  4. Trace each handler in order. For each call: what it reads, writes and resets, and what it triggers (effects, watchers, subscriptions).
  5. 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.
  6. Compare the final state with the label. "New email" must end with the compose view open.
  7. 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.

Served from the uplift.page library and refreshed within 5 minutes of every update.