# C22 — The playground is a trace explorer; sinks are the recording mechanism **Guarantee.** The World is a *program* (C21): nothing is displayable until it runs, and its harnesses are transient machinery (built, run, discarded — C19). The only durable, displayable substance is the **recorded trace**, captured by **sinks** — pure consumer nodes (C8) that persist a stream (equity, a bias / exposure level, a node's output) into the run registry (C18). **Displayable = exactly what a sink recorded**; with no sink, only the input params + summary metrics remain. The **playground** is therefore an **execution viewer / trace explorer**, and it plays *any* harness (the harness *player*, not a harness, never bound to a default one): before a run it shows the program *structure* (graph-as-data, C9) + param knobs; during a run, live sink streams; after a run, recorded traces + metrics from the registry — including meta-views (stitched walk-forward, sweep surfaces, multi-strategy / instrument comparison). Observability is **explicit and selective** — you instrument what you want to see; the choice of sinks is part of the experiment. The engine ships sample blueprints and a scaffolded starter so a newcomer has something to run at once; a fresh project is empty until built, run, and instrumented. The shipped visual face is **web-from-disk** — disk-persisted traces rendered as self-contained HTML chart pages (not egui). **Forbids.** A scene-editor model that assumes a persistent, populated world; constructing or wiring topology in the UI (topology is Rust + hot-reload, C9/C17 — the UI reflects the live graph-as-data and tunes runtime params via sliders, C12); retaining un-sinked data; binding the playground to a single / default harness. The "never a scene editor" forbid is precisely **no persistent scene-at-rest** (a World is a program) — it does *not* forbid a future blueprint *editor* over the World-owned topology data (C24); that data format would be the substrate such an editor round-trips, not a new mechanism. **Why.** A program has no scene-at-rest to inspect; what persists is what it records. Making sinks the one recording-and-observability mechanism keeps "what can I see?" answerable by "what did I instrument?", and keeps the engine UI-agnostic (C14). Live param tuning (runtime values, no topology change — C12 / C19) gives the interactive feel without a wiring DSL. The visual face is **orthogonal and optional**: the blueprint data format (C24) is required by C21 (generation / structural search) and C18 (reproduction) *at the CLI level, autonomously*, independent of any visual surface — so the playground's form (web or egui, viewer or editor, or not built at all) is downstream, and topology authoring does not live in it. Generalized by the role model (C25): every artifact's canonical, complete form is **text**, every operation is executable headless, and any visual surface is a **stateless projection** that reads/writes that canonical form and adds no semantics; read-only projections (this trace explorer) rank far above write editors, because the Blockly-litmus discipline (C25) already keeps each vocabulary palette-generatable without one. ## Current state **Sinks at the substrate.** A recorded trace is exactly what a recording (tap) node pushed out of the graph — there is no engine recording registry; the constructing World holds each recording node's destination. `Harness::run` returns `()` and recording is a node-side concern, so one run records *many* streams (one per tap) rather than exactly one row. Recorded streams are sparse and timestamped (a record per fired cycle, tagged `ctx.now()`), matching a trace of timestamped events (C18). **Encoding / storage / rendering split (C14).** A drained tap is encoded as a columnar (SoA, C7) `ColumnarTrace` — struct→JSON only — in `aura-engine` (`report.rs`); file I/O is `aura-registry::TraceStore`, persisting each run under `runs/traces//` beside the run registry's `runs.jsonl`; rendering is `aura-cli` (`render_chart_html` + the vendored `chart-viewer.js`). The engine stays headless. **Single run.** `aura run ` persists every tap the blueprint declares to the trace store under the run's own name, on both shapes: a `bias`-output strategy (`aura-runner::member::run_signal_r`, R-wrapped) and a bare measurement blueprint with ≥1 declared tap but no `bias` (`aura-runner::measure::run_measurement`). A tap-free run writes nothing to `runs/` and its stdout stays byte-identical. The CLI `--trace ` flag is **retired** on `run` (and on `mc`): it parses but is refused at dispatch (exit 2) — naming a trace is the family / campaign path's job, not a per-run flag. **Families.** `aura sweep|walkforward --real … --trace ` persists *each member* via the campaign path: the dissolved verb is translated into a content-addressed process + campaign document whose `presentation.persist_taps` requests the tap vocabulary, run through the one campaign executor; `aura-runner::runner::persist_campaign_traces` writes each member under a depth-2 fan-out `runs/traces////`. Every written member is independently re-run once in non-reduce trace mode over its own recorded window, and its re-derived metrics are asserted equal to the recorded member's — a **C1 drift alarm** that refuses (exit 1) rather than persist a silently-wrong trace. The **synthetic** sweep / walk-forward path refuses `--trace` (exit 2); **Monte-Carlo is excluded from trace persistence** (`mc --real` refuses `--name`/`--trace`, exit 2 — the real-data R-bootstrap campaign itself runs, but records no per-member family traces; the synthetic `--seeds` family's realization argument, C12, does not carry over to one real series). `TraceStore::ensure_name_free` makes name resolution a total function, refusing cross-kind reuse of one name by both a run and a family. **Viewer.** `aura chart [--tap ] [--panels]` classifies the name on disk (`TraceStore::name_kind`: top-level `index.json` → a single Run; member subdirs → a Family; else NotFound, which then tries campaign-name resolution), reads the traces back, and aligns all taps on a synthetic union-timestamp spine via the post-run `join_on_ts` (C3 — a post-run join, not an in-graph merge; C1 — pure, no live external call), emitting a self-contained uPlot page (vendored, like the graph render). A **single run** overlays all its taps, each on its own y-scale (or the one `--tap` selects); a **family** overlays **one tap** (default `equity`, `--tap` to pick) of **every member** on a **single shared** y-scale — members measure one identical quantity, so a shared scale is what makes them comparable. One mechanism yields three correct readings: sweep / MC members share the data window (a true overlay), walk-forward members are disjoint OOS windows (null-complementary → the stitched curve). A **run-context header** (`ChartMeta` → `window.AURA_TRACES.meta` → the pure `buildHeader`) renders chips for kind / name / members / tap / broker / window / seed / commit / bound-params; a family's window is the **span** `(min from, max to)` across members. Serve-time **min-max decimation** (`decimate`, `CHART_DECIMATE_BUCKETS = 2000`) thins the page to a few-thousand spine slots (full recorded data stays on disk, view-only), and the reduction is **tap-aware** (`reduce_for_tap` / `ReduceKind`): an unbounded cumulative curve (`equity`) keeps the per-bucket min/max **envelope** so drawdowns survive, while a bounded level series (`exposure` / the C10 bias level, ∈ [-1,+1]) reduces by per-bucket **mean** so its net / duty-cycle level shows instead of collapsing to a solid ±1 band. `--panels` renders stacked panels instead of the overlay. **Newcomer.** `aura new ` scaffolds a **data-only** project — a paths-only `Aura.toml`, a runnable starter blueprint `blueprints/signal.json` over the std vocabulary (an SMA-cross → `Bias` strategy), `.gitignore`, and a project `CLAUDE.md` — no crate, no build step (`aura nodes new` attaches a native node crate, C16/C17). The engine also ships example blueprints under `crates/aura-cli/examples/r_{sma,breakout,channel,meanrev}.json`. Honest gap against the Guarantee's "populated trace immediately": these shipped blueprints are *strategies* (a `bias` output, no declared taps), so `aura run` prints summary R-metrics to stdout but writes no on-disk trace — a **chartable** trace today comes from a blueprint that declares taps (single run) or from a `--real … --trace` family campaign. **Deferred.** Live sink streams *during* a run are not built — taps are buffer-then-drain (collected, then written after the run), and there is no local replay server or replay clock; the shipped face is post-run static HTML. Reworking tap draining into live subscribers (stream-to-disk / fold / live consumer) is #283. Embeddable chart / graph fragments for hosting in external pages are unbuilt (#150). The programmable analysis meta-level — families as composable orchestration operators — is C21's open direction, not this contract's. > History: [c22-playground-traces.history.md](c22-playground-traces.history.md) ## See also - [C27](c27-declared-taps.md) — declared taps: the named sinks a run persists - [C8](c08-node-contract.md) — sinks are pure consumer nodes - [C21](c21-world.md) — the World is the program the playground plays - [C24](c24-blueprint-data.md) — the topology data a future editor would round-trip - [C25](c25-role-model.md) — visual surfaces as stateless projections - [C14](c14-headless-two-faces.md) — headless core, two faces (the encode / store / render split) - [C18](c18-registry.md) — the run registry the trace store sits beside - [C9](c09-fractal-composition.md) — topology grown in Rust; the structure the viewer reflects - [C10](c10-bias-r-cost.md) — the bounded bias / exposure level the viewer reduces by mean