Files
Aura/docs/design/contracts/c22-playground-traces.md
T
claude 8688a60ded docs(ledger): split the design ledger into an INDEX map, per-contract live files, and history sidecars
The single-file ledger had grown to 2968 lines / ~42k tokens, mixing
current design law with accreted history: 59 cycle-stamped realization
blocks, 18 [HISTORY] passages, 22 supersession markers, and the C10 /
C22 / C24 reframe sagas layered several supersessions deep. A
code-grounding audit (31 agents, adversarially verified) confirmed 11
defects stated as current truth: stale crate homes from the C28 #288
roster split (cost nodes, PositionManagement, PositionEvent, Session),
the renamed InputSpec->PortSpec, the pre-#241 project model in C16 and
the open-threads section, a stale HarnessKind retirement deferral in
C24, and three C28-internal inconsistencies.

New shape, per the ailang precedent:

- INDEX.md stays the sole addressable entry point: foundation, external
  components, a C-id-keyed contract map (one line per contract), and
  only the genuinely open architectural threads.
- contracts/cNN-<slug>.md carries each contract's current truth only:
  Guarantee / Forbids / Why with ratified refinements integrated, plus
  a code-anchored Current state. All confirmed defects are fixed here;
  crate anchors were re-verified against the tree.
- contracts/cNN-<slug>.history.md (18 sidecars) and INDEX.history.md
  preserve every superseded block verbatim, stamps and issue refs
  intact, under a frozen-record banner. Nothing was deleted: superseded
  design intent remains an addressable working-tree artifact, off the
  per-cycle audit walk.
- Ledger discipline is now stated in INDEX.md: live files are edited in
  place at cycle close, superseded text moves verbatim to the sidecar,
  and a supersession marker in a live file is itself an audit finding.

Every contract file was verified against its old text by an independent
zero-loss pass (statement-by-statement) plus a code-accuracy spot check;
C-ids and contract titles are unchanged, so existing C-id citations in
code, tests, and issues resolve as before.
2026-07-21 16:40:36 +02:00

145 lines
9.8 KiB
Markdown

# 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/<name>/` 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 <blueprint.json>` 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 <NAME>` 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 <SYM> … --trace <NAME>` 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/<name>/<cell>/<member>/`. 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 <name> [--tap <t>] [--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 <name>` 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