Guide

# Debugging & tooling

When a reactive click "does nothing," the failure is almost always at one of the RPC seams. phlex-reactive ships four introspection surfaces to name it fast — and one guard that turns a forgotten authorize! into a loud error instead of a silent hole.

## The four surfaces

Work top-down — validate the install, inventory the server, find the specific component, scan the live page — and stop as soon as a step names the problem.

| Surface | Command | Answers |
| --- | --- | --- |
| **Doctor** | `bin/rails phlex_reactive:doctor` | is the install wired correctly? |
| **Inventory** | `bin/rails phlex_reactive:actions` / `find` | what actions exist, where, authorized? |
| **Browser inspector** | `import("phlex/reactive/inspect")` | what's reactive on THIS page? |
| **MCP server** | `bin/rails phlex_reactive:mcp` | all of the above, queryable in your editor |

Everything is **read-only** and reports **names, paths, and declared schemas only** — never a token, a secret, or runtime state.

## Doctor — validate the install

The first thing to run. `phlex_reactive:doctor` prints a ✓/✗/? line for every seam and a fix for each failure:

```shell
bin/rails phlex_reactive:doctor
```

It checks that `POST /reactive/actions` routes to the gem (a host catch-all route shadows it otherwise), the generic `reactive` Stimulus controller is registered, the identity verifier round-trips, every declared `action :name` has a public method, every component resolves a stable `#id`, and — **advisory only** — whether each mutating action appears to authorize. Most "nothing happens" reports are a red ✗ here.

## Inventory — what actions exist (server side)

Answer "what reactive actions exist in this app, where are they defined, and is each authorized?" without grepping:

```shell
bin/rails phlex_reactive:actions             # component | action | params | file:line | auth
bin/rails phlex_reactive:actions FORMAT=json # machine-readable
bin/rails "phlex_reactive:find[counter]"     # fuzzy-find one; prints each action's method source
```

`actions` is a table across the whole app; `find` fuzzy-matches one component (exact > prefix > substring > subsequence) and prints each action's **method-definition source** (extracted with Prism). The `auth` column is a **heuristic**: `authorized*` means an authorization call was detected in the body; `unverified` means none was — but a helper may still authorize indirectly, so it's a hint, not a verdict.

## Inspector — what is reactive on this page (browser side)

A standalone, on-demand module (no cost until you import it — it does not touch the hot-path controller). In the browser console on the affected page:

```javascript
(await import("phlex/reactive/inspect")).report()
```

`report()` prints a `console.table` of every reactive root on the page — its `id`, decoded token payload (**component class**, gid, state keys, token version), status (error/busy/dirty), the bound **triggers** (action + event + params + debounce/confirm), client-only ops, computes, and the `name`d fields a dispatch would send. `scan()` returns the same data as a value.

A token that can't be decoded (an older Marshal-serialized payload) shows as `(opaque token)` rather than throwing — the scanner is pure read and never breaks the page.

## The server↔client mapping is by name

This is the payoff of the two inventories. The `component` and each trigger's `action` that `report()` shows in the browser are **exactly the same identifiers** `phlex_reactive:actions` lists on the server. So a bug localizes to a mismatch:

- Does the button on the page carry the **action** you expect?
- Does its **component** match the class you think renders here?
- Is a root showing an `(opaque token)` where it should decode?

You never have to guess which server method a DOM control POSTs to — the name is the join key across both tools.

## verify_authorized — the fail-closed guard

phlex-reactive's security model says *you* authorize inside the action. `verify_authorized` (**ON by default**) enforces it: an action that completes **without any authorization call** raises `Phlex::Reactive::AuthorizationNotVerified` **inside the transaction**, so the mutation **rolls back** — fail-closed, stronger than Pundit's after-the-fact `verify_authorized` (which runs post-commit).

The guard detects a call to any configured authorization method **or** `mark_authorized!`, made anywhere during the action (a helper the action calls counts too). Three ways to satisfy it:

```ruby
# 1. Call your authorization method (the interceptor detects it):
def rename(title:)
  authorize! @todo, :update?
  @todo.update!(title:)
end

# 2. mark_authorized! after a bespoke check the interceptor can't see:
def publish
  raise NotAllowed unless @post.author == Current.user
  mark_authorized!
  @post.update!(published: true)
end

# 3. skip_verify_authorized for a genuinely public action:
class PublicCounter < ApplicationComponent
  include Phlex::Reactive::Component
  skip_verify_authorized              # whole component
  # skip_verify_authorized :filter    # …or just these actions
end
```

Configure the method names your library uses, or turn the guard off entirely (not recommended — you lose the fail-closed net):

```ruby
# Pundit / CanCanCan / ActionPolicy — the default set:
Phlex::Reactive.authorization_methods = %i[authorize! authorize allowed_to?]
# Phlex::Reactive.verify_authorized = false
```

> **It raises a 500, on purpose:** `AuthorizationNotVerified` is a **developer error** (a forgotten `authorize!`), not a client fault — it bubbles as a 500 so your error tracker fires, and it is never masked into a 4xx. The `action.phlex_reactive` instrumentation event tags the outcome `:unverified`. Authorizing inside a custom `around_action` (which runs *outside* the tracking window) triggers a spurious raise — call `mark_authorized!` inside the action instead.

## MCP server — introspect the running app from your editor

For live-app introspection inside Claude Code, run the read-only MCP server (needs the optional `mcp` gem — `gem "mcp"`). Add it to `.mcp.json`:

```json
{ "mcpServers": { "phlex-reactive": { "command": "bin/rails", "args": ["phlex_reactive:mcp"] } } }
```

It exposes five read-only tools — `phlex_reactive_doctor`, `phlex_reactive_components`, `phlex_reactive_actions` (optional `component:` filter), `phlex_reactive_find`, and `phlex_reactive_config` (a redacted summary that never emits the verifier or `secret_key_base`) — the same data as the rake tasks, queryable in conversation.

Entry point is a rake task (not an exe) because introspection needs the booted Rails app. **Constraint:** stdio MCP needs a clean stdout, so an initializer that `puts` breaks the transport.

## The installable Claude skill

One command installs the debugging runbook as a Claude Code skill and wires the MCP server:

```shell
rails g phlex:reactive:claude
```

It copies the `phlex-reactive-debugging` skill into `.claude/skills/` (teaching the doctor → inventory → find → browser `report()` → MCP workflow) and creates `.mcp.json` with the server entry — but only when it's absent; it never rewrites an existing `.mcp.json`, it prints the snippet instead.

For **reference** documentation (how a feature works), point tools at the hosted docs MCP — `https://phlex-reactive.zoolutions.llc/llms.txt`. The local MCP server is for **live-app** introspection.

## Failure table

The endpoint's failure modes and what each means:

| Symptom | Cause | Fix |
| --- | --- | --- |
| **400** | Tampered / stale token — from before a deploy, a `secret_key_base` mismatch, or a `reply.with(...)` stream that skipped the token refresh | Reload for a fresh token; confirm `secret_key_base`; refresh the token in custom streams |
| **403** | The action isn't declared, OR your authorizer raised (registered in `authorization_errors`) | Declare the action — or the 403 is correct |
| **`AuthorizationNotVerified` (500)** | `verify_authorized` is on and the action authorized nothing | `authorize!`, `mark_authorized!`, or `skip_verify_authorized` |
| **404** | The record was deleted while a page still showed it | Expected for a stale page — remove the row or reload |
| **Nothing on click** | The `reactive` controller isn't registered, or a catch-all route shadows the endpoint | `phlex_reactive:doctor` names both |
| **Renders but never updates** | `id` and `data-controller="reactive"` landed on different elements | Spread `**reactive_root` (binds both to one element) |