# Sandbox preview & branch verification

This is the single step an unsupervised agent must not skip: proving the flow renders and every
branch resolves *before* shipping it. Two tools do it, both fully local, both needing no account,
no server, and no tokens.

## Preview it in a browser: `weir dev`

```
weir dev --specPath weir/flows/onboarding.json --serve
```

`weir_dev` serves the spec in-browser with mocked native behavior for every **stock** screen type.
The mock provides fake permission dialogs, a mock localized product catalog (including an
intro-offer product and a non-USD tier), a safe-area/notch overlay, and keyboard-inset simulation —
so a browser shows what the device shows for stock screens. (A `custom` screen has no browser
preview — see [Quickstart](/docs/quickstart/#3-preview-it).) With `--serve` it prints an `http://`
URL; without it, a `file://` path.

### Force a native-call outcome with a persona

A persona pins the outcome of each native call, so you can look at one specific path:

```
--persona "notifications=deny,purchase=cancel"
```

The persona string is comma-separated `key=value` pairs. The keys are the native-call decisions:

| Key | Values | Controls |
|---|---|---|
| `notifications` | `grant` | `deny` | the system notification-permission prompt result |
| `tracking` | `grant` | `deny` | the ATT prompt result (iOS; `unavailable` on Android) |
| `health`, `camera`, `location` | `grant` | `deny` | the matching permission prompt result |
| `purchase` | `success` | `cancel` | `fail` | how a paywall `purchase.start` resolves |
| `variant` | `<experimentId>:<variantId>` | force this preview onto one arm of an experiment declared in the flow's `experiments[]` |
| `userId` | any string | pin the bucketing key an experiment's arm assignment hashes — normally left unset |

Any key not in this table (a typo'd permission type, for example) is rejected with the list of
valid keys — a persona string is validated, not silently ignored. Any decision you leave out uses
the mock's default. A persona changes only native-call outcomes — it does not answer select/input
screens for you.

### Force a specific experiment arm

A screen with a `variant` gate only renders for users assigned to one of its `showFor` arms (see
[Reference → Experiments and the variant gate](/docs/reference/#experiments-and-the-variant-gate)).
Both `weir_dev` and `weir_walk` pick the default `userId` ("`eval-user`") unless told otherwise,
so an ungated preview/walk always resolves to whichever arm that userId happens to bucket into —
usually control. To see a specific arm, force it directly:

```
weir dev --specPath weir/flows/onboarding.json --persona "variant=paywall_copy_v1:treatment" --serve
weir walk --specPath weir/flows/onboarding.json --variant paywall_copy_v1:treatment --outDir .eval-out/walk
```

`weir_walk` also accepts `variant` as a top-level field (applied to every persona at once) instead
of repeating it inside each persona string. Either form searches for a `userId` that actually
buckets into the requested arm and pins it — it does not change the flow spec's gate. An unknown
experiment or variant id fails with the full list of ids the flow spec actually declares.

## Walk every branch headlessly: `weir walk`

```
weir walk --specPath weir/flows/onboarding.json --outDir .eval-out/walk
```

`weir_walk` drives the same mocked preview with Playwright for one or more personas, screenshots
every screen on each persona's path (including the fake dialogs), and writes one
`walk-report.json` per persona. This is how you confirm a *specific* path resolves — not just that
the tool exited 0.

### Drive typed answers to reach a conditional path

To reach the branch a specific answer triggers, drive the select/input answers, not just the
native-call outcomes. Pass answers as repeated `--answerMaps` JSON values, one map per persona:

```
--answerMaps '{"goal":"maintain"}' --answerMaps '{"goal":"cut"}'
```

The first map takes the `maintainNote` branch; the second falls through to `weightInput`. Run both
to prove both arms of `goalSelect.next` render. Remember array fields need a repeated flag — two
`--answerMaps` flags, not one flag with two values.

## Read the output — do not just check the exit code

### `walk-report.json` fields

Each report records what the persona actually did:

| Field | Meaning |
|---|---|
| `persona` / `personaConfig` | the persona's label and its full parsed config (permissions, purchase outcome, answers, forced variant) |
| `path` | the ordered list of screen ids this persona visited |
| `screens` | one entry per visited screen: index, screen id, type, and the screenshot file path |
| `events` | every native-call event the walk made (the raw envelopes, not just `complete`/`dismiss`) |
| `appliedAnswers` | the subset of your `answerMaps` that were actually submitted through a real DOM control |
| `variables` | the value of every flow variable when the walk ended |
| `branchDecisions` | each `next.branches` evaluation and the target it chose |
| `reachedComplete` | whether the walk reached `complete()` |

Check three things: the `path` includes the screens that answer's branch should reach, the
`branchDecisions` show the condition you expect firing, and `variables` has the interpolated
variables set to real values (not blank).

### Inspect the screenshots

The screenshots land under the `--outDir` you passed, one PNG per screen per persona
(`.eval-out/walk/**/*.png`). Open them. A coding agent should read the PNGs directly as images —
this is the step that catches a screen that validates but renders wrong (an undefined variable
showing blank, a branch that never gets visited, copy overflowing the safe area). The CLI prints a
placeholder line for image blocks and the file path; use the path to open the real PNG.

## Troubleshooting the walk

- **"My branch never gets visited."** The persona's `--answerMaps` did not drive the answer the
  branch needs. Check `branchDecisions` in the report: it shows which condition evaluated and why
  the other target won. Confirm the answer key matches the screen's `variable` id.
- **"A variable is undefined downstream."** A screen interpolates `{{x}}`, but no screen on this
  persona's `path` wrote `x`. Either the writing screen is on a different branch, or `x` is only
  `computes`d by a loader with no `default` (it renders blank in preview). Give it a `default`, or
  make sure the persona's path reaches the screen that sets it.
- **The walk exits 0 but a screen looks wrong.** Exit 0 means the walk completed, not that the
  screens are correct. Reading the screenshots is the only check for that.

Once every branch renders and `walk-report.json` shows the paths and variables you expect, ship
it — [Quickstart](/docs/quickstart/) (`weir_release`, which runs the full `weir_conform` gate
before it publishes).
