diff --git a/docs/development/architecture.md b/docs/development/architecture.md
index 6264517..76b0a1c 100644
--- a/docs/development/architecture.md
+++ b/docs/development/architecture.md
@@ -6,30 +6,7 @@ title: Architecture
Three processes, two transports.
-```mermaid
-flowchart TB
- subgraph Go["sanderling (Go)"]
- Bundler[Bundler esbuild] --> Verifier[Verifier goja + LTL]
- Verifier <--> Runner[Runner]
- Runner --> Trace[Trace writer JSONL + PNG]
- Runner <--> Driver[Driver iface]
- end
-
- subgraph Sidecar["Maestro Sidecar (JVM)"]
- Maestro[maestro-client]
- end
-
- subgraph Device["Emulator"]
- subgraph App["Android app (debug)"]
- SDK[sanderling-sdk pause / hierarchy logs / coverage]
- end
- end
-
- Driver -- gRPC --> Maestro
- Maestro -- UIAutomator --> App
- Runner -- Unix socket --> SDK
- Trace --> Runs[(runs/)]
-```
+
## Processes
@@ -48,6 +25,10 @@ flowchart TB
The split exists for one reason: only real UI events need the cost of crossing process and OS-API boundaries. Introspection is cheap, frequent, and lives on a fast local socket directly to the app.
+## Inspect UI
+
+`sanderling inspect` is a separate mode of the same Go binary. It serves an embedded React bundle and reads `runs/` from disk, streaming file-watcher events over SSE so the UI updates as new steps land. It has no connection to the sidecar or the SDK; it only consumes the trace artifacts.
+
## Per-step cycle
The heart of the system is:
diff --git a/docs/index.md b/docs/index.md
index b280ead..a4d626d 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -1,21 +1,27 @@
---
-title: sanderling
+title: Sanderling Manual
---
-# sanderling
+# Sanderling Manual
Autonomous property-based testing for mobile apps. Specs in TypeScript. Core in Go. Drives the app under test through Maestro and an in-app SDK.
Alpha: Android emulator only. Scope of v0.1.0 is tracked in [issue #4](https://github.com/priyanshujain/sanderling/issues/4).
-## Manual
- [Getting started](./manual/getting-started.html)
- [Writing specs](./manual/writing-specs.html)
- [Runs](./manual/runs.html)
+- [Inspect](./manual/inspect.html)
- [CLI reference](./manual/cli.html)
## Development
- [Design principles](./development/design-principles.html)
- [Architecture](./development/architecture.html)
+
+---
+
+
+
+> sanderling, a wading bird that probes the shoreline for bugs that lie beneath.
diff --git a/docs/manual/getting-started.md b/docs/manual/getting-started.md
index 2fd82f9..14c7b43 100644
--- a/docs/manual/getting-started.md
+++ b/docs/manual/getting-started.md
@@ -18,22 +18,10 @@ Run `sanderling doctor` to check the host environment.
### CLI
-macOS arm64:
-
```sh
-curl -L https://github.com/priyanshujain/sanderling/releases/latest/download/sanderling__darwin_arm64.tar.gz | tar xz
-./sanderling version
+curl -fsSL https://raw.githubusercontent.com/priyanshujain/sanderling/master/install.sh | bash
```
-Linux amd64:
-
-```sh
-curl -L https://github.com/priyanshujain/sanderling/releases/latest/download/sanderling__linux_amd64.tar.gz | tar xz
-./sanderling version
-```
-
-Pre-built for `darwin/arm64`, `darwin/amd64`, `linux/amd64`, `linux/arm64`.
-
### Spec package (npm)
```sh
@@ -50,21 +38,22 @@ dependencies {
## Your first run
-The repo ships a working sample at `examples/folio`. From that directory:
+The repo ships a working sample at `examples/folio`, a Kotlin Multiplatform app with a TypeScript spec under `sanderling/spec.ts`. Install `just`, then from `examples/folio`:
```sh
-npm install
-(cd android && ./gradlew installDebug)
-sanderling test \
- --spec spec.ts \
- --bundle-id app.folio \
- --platform android \
- --duration 2m
+just install # build and install the folio APK on a booted emulator or device
+just test # run the spec
```
-Pass `--avd ` only when no device is connected and you have multiple AVDs; otherwise sanderling uses the connected device or boots the single AVD it finds.
+With no device connected and multiple AVDs, pick one:
-When the run ends, the trace lands in `runs//`:
+```sh
+AVD=Pixel_7 just test
+```
+
+Persistent settings can live in a `.env` alongside the justfile (`AVD=Pixel_7`, `DURATION=5m`, and so on).
+
+When the run ends, the trace lands in `sanderling/runs//`:
```
runs/2026-04-18T12-34-56/
@@ -73,6 +62,6 @@ runs/2026-04-18T12-34-56/
└── meta.json
```
-Open the screenshots directory to scrub visually, or read `trace.jsonl` step by step.
+Browse it with `sanderling inspect` (see [inspect](./inspect.html)), or read `trace.jsonl` step by step.
Next: [writing specs](./writing-specs.html).
diff --git a/docs/manual/inspect.md b/docs/manual/inspect.md
index 788fdea..0990471 100644
--- a/docs/manual/inspect.md
+++ b/docs/manual/inspect.md
@@ -4,24 +4,15 @@ title: sanderling inspect
# sanderling inspect
-`sanderling inspect` is a local web UI for exploring runs produced by `sanderling test`. It reads `runs//meta.json` and `runs//trace.jsonl` and renders each step with its action, screenshot, snapshots, residual formulas, and exceptions.
+Local web UI for exploring runs produced by `sanderling test`. Reads `runs//meta.json` and `runs//trace.jsonl` from disk.
```
sanderling inspect [run-or-runs-dir] [--port N] [--no-open] [--dev]
```
-The positional argument can be either a runs directory or a single run directory (auto-detected by the presence of `meta.json`). When omitted, it defaults to `./runs`.
+The positional argument can be a runs directory or a single run directory (auto-detected by `meta.json`). Defaults to `./runs`.
-## Layout
-
-The detail page uses a phone-dominant grid:
-
-- **Actions** (left): vertical step list. Steps with violations are marked with a red dot; steps with exceptions have a dashed-outline marker.
-- **Screenshot** (center): the device screenshot for the current step. The runner's resolved tap target is overlaid as a red rectangle, the tap point as an outlined circle. Swipes show an arrow from start to end.
-- **Snapshots** (top right): the current step's snapshots flattened into dotted-path rows. Values that changed since the previous step are highlighted; hover to see the previous value.
-- **Properties** (middle right): one row per property with status (violated / pending / holds) and an expandable residual formula.
-- **Exceptions** (bottom right): SDK-captured uncaught throwables. Stack traces expand inline.
-- **Timeline** (bottom): per-property swimlane across all steps; click a cell to seek.
+
## Keyboard shortcuts
@@ -31,31 +22,28 @@ The detail page uses a phone-dominant grid:
| `k`, `Left` | Previous step |
| `Shift+j`, `Shift+Right` | Jump 10 forward |
| `Shift+k`, `Shift+Left` | Jump 10 back |
-| `g` | First step |
-| `G` | Last step |
+| `g` / `G` | First / last step |
| `.` | Next step with a violation |
-## URLs
+Arrow keys inside a tablist or listbox yield to those widgets. Use `j`/`k` when focus is on one.
-- `/` — run index (auto-refreshes via SSE when new runs land)
-- `/runs/:id` — redirects to step 1
-- `/runs/:id/steps/:n` — direct deep link
+## Deep links
-## Theme
+`/runs/:id/steps/:n` links to a specific step. Use it in issues or PRs when pointing at a violation.
-Defaults to the system color scheme via `prefers-color-scheme`. The `light`/`dark` button in the toolbar toggles a manual override stored in `localStorage`.
+The run index auto-refreshes over SSE as new runs land, so `sanderling inspect` and `sanderling test` can run side by side.
## Development
-Two-process loop:
+Two-process loop while iterating on the UI:
```
-make web-dev # bun + vite, http://127.0.0.1:5173
+make web-dev # bun + vite on http://127.0.0.1:5173
make inspect-dev # sanderling inspect --dev, proxies non-API to 5173
```
-For a single binary with embedded assets:
+Single binary with the bundle embedded:
```
-make sanderling # builds web/dist, copies to internal/inspect/dist, then go build
+make sanderling
```
diff --git a/docs/manual/writing-specs.md b/docs/manual/writing-specs.md
index a6781e6..a6dae99 100644
--- a/docs/manual/writing-specs.md
+++ b/docs/manual/writing-specs.md
@@ -7,7 +7,7 @@ title: Writing specs
A spec has three parts: extractors, properties, and actions.
```ts
-import { extract, always, actions, weighted, Tap, taps, swipes } from "@sanderling/spec";
+import { extract, always, now, actions, weighted, Tap, taps, swipes } from "@sanderling/spec";
// 1. Extractors pull values from each observed state.
const loggedIn = extract((s) => !!s.ax.find("id:home-tab-bar"));
@@ -105,39 +105,29 @@ Session state (tokens, keychain, prefs) persists through the rest of the run. If
## Pattern: conditional properties
-Use gating extractors the same way inside properties. Express "only check X when Y holds":
+Use gating extractors the same way inside properties. Express "only check X when Y holds" with `now(...).implies(...)`:
```ts
const loggedIn = extract((s) => !!s.ax.find("id:home-tab-bar"));
export const properties = {
- cartPersistsWhenLoggedIn: always(() => {
- if (!loggedIn.current) return true;
- return state.snapshots.cart_count !== undefined;
- }),
+ cartPersistsWhenLoggedIn: always(
+ now(() => loggedIn.current).implies(now(() => cartCount.current !== undefined)),
+ ),
};
```
-When `implies` ships in v0.1.0, this becomes:
+`implies`, `and`, `or`, and `not` are methods on any formula. Combine them freely.
-```ts
-cartPersistsWhenLoggedIn: always(() =>
- implies(loggedIn.current, () => cartCount.current !== undefined)
-),
-```
-
-## Pattern: eventually (once the operator lands)
+## Pattern: eventually
`always` asserts something holds at every step. `eventually` asserts it holds at some step, usually with a time bound:
```ts
-// v0.1.0+
-loginSucceedsWithin30s: eventually(
- () => loggedIn.current
-).within(30, "seconds"),
+loginSucceedsWithin30s: eventually(() => loggedIn.current).within(30, "seconds"),
```
-Useful for liveness checks: the loading spinner eventually goes away, the deep link eventually lands on `/home`.
+`within` takes `"milliseconds"`, `"seconds"`, or `"steps"`. Useful for liveness checks: the loading spinner eventually goes away, the deep link eventually lands on `/home`.
## Pattern: snapshot-backed properties
diff --git a/install.sh b/install.sh
new file mode 100755
index 0000000..7498575
--- /dev/null
+++ b/install.sh
@@ -0,0 +1,75 @@
+#!/usr/bin/env bash
+#
+# Installs the sanderling CLI on macOS or Linux.
+#
+# Usage:
+# curl -fsSL https://raw.githubusercontent.com/priyanshujain/sanderling/master/install.sh | bash
+#
+# Environment:
+# SANDERLING_VERSION Tag to install (default: latest release)
+# SANDERLING_INSTALL Install prefix (default: $HOME/.sanderling); binary lands in $prefix/bin
+
+set -euo pipefail
+
+REPO="priyanshujain/sanderling"
+PREFIX="${SANDERLING_INSTALL:-$HOME/.sanderling}"
+BIN_DIR="$PREFIX/bin"
+
+os="$(uname -s)"
+arch="$(uname -m)"
+case "$os" in
+ Darwin) os=darwin ;;
+ Linux) os=linux ;;
+ *) echo "sanderling: unsupported OS '$os' (need Darwin or Linux)" >&2; exit 1 ;;
+esac
+case "$arch" in
+ x86_64|amd64) arch=amd64 ;;
+ arm64|aarch64) arch=arm64 ;;
+ *) echo "sanderling: unsupported arch '$arch' (need amd64 or arm64)" >&2; exit 1 ;;
+esac
+
+version="${SANDERLING_VERSION:-}"
+if [ -z "$version" ]; then
+ # /releases/latest skips pre-releases; fall back to /releases for the
+ # newest tag of any kind so alphas remain installable.
+ version="$(curl -fsSL "https://api.github.com/repos/$REPO/releases/latest" 2>/dev/null \
+ | awk -F'"' '/"tag_name":/ {print $4; exit}')"
+fi
+if [ -z "$version" ]; then
+ version="$(curl -fsSL "https://api.github.com/repos/$REPO/releases?per_page=1" \
+ | awk -F'"' '/"tag_name":/ {print $4; exit}')"
+fi
+if [ -z "$version" ]; then
+ echo "sanderling: could not resolve a release tag" >&2; exit 1
+fi
+
+stripped="${version#v}"
+tarball="sanderling_${stripped}_${os}_${arch}.tar.gz"
+base="https://github.com/$REPO/releases/download/${version}"
+
+tmp="$(mktemp -d)"
+trap 'rm -rf "$tmp"' EXIT
+
+echo "sanderling: downloading $tarball ($version)"
+curl -fsSL -o "$tmp/$tarball" "$base/$tarball"
+curl -fsSL -o "$tmp/checksums.txt" "$base/checksums.txt"
+
+if command -v sha256sum >/dev/null 2>&1; then
+ ( cd "$tmp" && grep " $tarball\$" checksums.txt | sha256sum -c - >/dev/null )
+elif command -v shasum >/dev/null 2>&1; then
+ ( cd "$tmp" && grep " $tarball\$" checksums.txt | shasum -a 256 -c - >/dev/null )
+else
+ echo "sanderling: no sha256 tool available, skipping checksum verification" >&2
+fi
+
+tar -xzf "$tmp/$tarball" -C "$tmp"
+mkdir -p "$BIN_DIR"
+mv "$tmp/sanderling" "$BIN_DIR/sanderling"
+chmod +x "$BIN_DIR/sanderling"
+
+echo "sanderling: installed $version to $BIN_DIR/sanderling"
+
+case ":$PATH:" in
+ *":$BIN_DIR:"*) ;;
+ *) echo "sanderling: add $BIN_DIR to PATH, e.g. 'export PATH=\"$BIN_DIR:\$PATH\"'" ;;
+esac