guide/06-scenarios.md
Synthetic scenarios
Standing: three worlds are bundled today. Conditions, streams and the drill are proposals.
Synthetic data is how every page is built before a sensor serves it, and how every state is tested after. The rule from the app's first day holds: synthetic data flows through the real pipes. A page never holds a fake value; it asks the same repository and gets an answer in the same shape.
A synthetic choice has three parts.
| Part | What it is | Chosen on the Sensor page as |
|---|---|---|
| World | the data at rest: what was found on one machine, on one day | a name |
| Condition | what is going wrong between the app and its source | none, or one fault |
| Stream | what plays on Live and Events | a script and a speed |
Worlds
A world is one file, crates/fixtures/world/<name>.json: the answers qdash's own handlers gave for every request the pages make, for one machine of the Meridian seed. scripts/cut-world.py cuts it.
| World | What it is | Standing |
|---|---|---|
meridian-day90 | the default; being recut to a quiet laptop (design c131 D15) | bundled |
minimal | one harness, a few sessions | bundled |
fresh | day 0: installed, nothing found yet | bundled |
pilot | priya's laptop on day 90: the machine where things happen | ruled, not yet cut |
first-week, power-user | usage at two extremes | named, not yet cut |
The page lists whatever worlds are bundled. It names none itself.
Conditions
A condition wraps the source. The page underneath does not know; it receives the error a real source would give and draws its real state.
| Condition | What the source does | The state it exercises |
|---|---|---|
| not running | no lock file | "no sensor on this machine" |
| stale | a lock file whose pid is dead | Found fails; "start the monitor" |
| slow | answers after a delay | loading, then content |
| refusing | 403, or 503 on the stream | Refused, with what to change |
| wrong shape | an answer that does not match its type | a named shape error, never a blank |
| lagging | the stream falls behind and is cut | Lost; the gap is shown |
Built in Phase 2 (2026-11-11): nozzle::scenario::Conditioned wraps any nozzle, in six conditions: healthy, not-running, slow (2.5 s), unavailable (REST answers, the stream gets a 503), shape-mismatch (the repository's own decode fails; the stream sends unreadable records), lag-then-cut (600 records, then cut in qdash's words, then a fresh resync). Choose one on Dev › Source or with QPOINT_CONDITION; the footer names it. The drill (c131 D15: injecting one of three bad events) is not built yet.
Streams
A stream script is recorded events in the envelope the sensor sends today (Live Activity), one JSON object per line. It is replayed through the same frame parser a live stream uses, at a chosen speed, with timestamps shifted to now.
Built in Phase 1 (2026-11-11): four generated scripts, not yet recordings. They live in crates/fixtures/src/streams.rs, written to qevents' payload shapes as read from code (../research/qevents-payloads.md). Each is seeded and deterministic, and opens with the monitor's resync (installation.snapshot, then process.snapshot):
quiet, 0.5 a second;coding-session, 8 a second (the default);mcp-heavy, 20 a second;file-storm, 500 a second (the stress case).
They play through nozzle::stream::play, the same subscription the sensor's stream uses. The condition and the stream are one per process (nozzle::scenario). Until the Sensor page (Phase 2), set them with QPOINT_STREAM=<script>[@<rate>] and QPOINT_CONDITION=<healthy|not-running|slow|unavailable|shape-mismatch|lag-then-cut>. The values in the scripts are invented; a recording of a real session replaces them.
| Script | What it is for |
|---|---|
| quiet day | the default: a little of everything, slowly |
| coding session | a harness at work: model requests, tool calls, file reads |
| MCP-heavy | sessions that lean on MCP servers |
| file storm | file.* at a high rate; the stress case for the buffer and for coalescing |
The drill
Design c131 D15: on the pilot world a drill injects one bad event into the stream, so a person can watch what a violation looks like. Three are ruled: code pasted to an outside site, a secrets file opened, and credentials read followed by traffic to an unknown host. On the Sensor page these are the Violations family.
Recording a real session into a scenario
Today, with a sensor running, this captures its stream to a file in the script format:
curl -N http://127.0.0.1:7878/api/events/stream | sed -n 's/^data: //p' > session.jsonl
Use the URL from the lock file if the port is not 7878.
A recording is real data. It holds prompts, file paths and whatever else passed through the sensor while it ran, and installation.details can hold secrets. Never commit a raw recording. Record a session staged for the purpose, read it before it leaves your machine, and scrub what should not travel.
When Live is built, a recording can be dropped onto it to be read like a live stream, and saved from there as a script.
Adding one
- A world: run
scripts/cut-world.pyagainst the seed for another machine or day, add the file tocrates/fixtures/world/, and add its name to the bundled list. The fixtures test checks every answer against its type. - A stream: add the
.jsonlbeside the worlds. Every line must parse as the envelope. - A condition: one case in the wrapper, and a story for the state it produces.