Cycle 0010, the Walking-skeleton milestone's closing seam: an `aura run`
subcommand that bootstraps a built-in sample harness (synthetic source →
SMA-cross → Exposure → SimBroker → recording sinks), runs it
deterministically, and prints the cycle-0009 metrics+manifest report as
canonical JSON to stdout.
The crux the cycle resolves: no recording sink node ships today (recording
lives only as #[cfg(test)] fixtures), and Harness::run returns () with a sink
as the only data-out path. So this cycle first ships a reusable
aura-std::Recorder (a pure consumer holding an mpsc::Sender, purity-preserving
per C7), then wires the CLI on top.
Load-bearing decisions, user-approved at the brainstorm gate:
- Recorder ships in aura-std (universal block, C16), not engine or CLI-local.
- Sample harness is authored as a plain Rust constructor over the raw
Harness::bootstrap API (C17/C20) — the open experiment-builder DSL thread is
deliberately NOT committed this cycle.
- Zero-dependency CLI: hand-parsed args (no clap); manifest commit from
option_env!("AURA_COMMIT") defaulting to "unknown" (build.rs git capture
deferred).
Grounding-check PASS (all load-bearing codebase assumptions ratified by green
tests). Non-goals: experiment-builder DSL, aura new, Aura.toml schema, the
data-server source (#7), build.rs git capture.
refs #8
13 KiB
aura run — end-to-end sample-harness CLI — Design Spec
Date: 2026-06-04 Status: Draft — awaiting user spec review Authors: orchestrator + Claude
Cycle 0010. Tracker: Gitea issue #8 (Brummel/Aura), milestone Walking skeleton — the skeleton's closing seam. Consumes the cycle-0009 report surface (#6) and the cycle-0007 signal-quality nodes (#4/#5).
Goal
aura run bootstraps a built-in sample harness (source → SMA-cross signal →
Exposure → SimBroker → recording sinks), runs it deterministically (C1), and
prints the structured metrics + manifest (#6) as canonical JSON to stdout —
the headline C14 "run a sim, emit structured metrics" move, end-to-end, from a
real binary for the first time. The sample harness ships with sinks so a
newcomer sees a populated trace immediately (C22).
The crux this cycle resolves
There is no shipped recording sink node today — recording (C22 / cycle 0006)
exists only as #[cfg(test)] fixtures in harness.rs and report.rs. Since
Harness::run returns () and a sink is the only data-out path, a runnable
CLI is impossible without first shipping a reusable recording node. This cycle
therefore ships one (aura-std::Recorder) as its first component, then wires the
CLI on top.
Non-goals (out of scope)
- The experiment-builder API / fluent
HarnessBuilderDSL. The ledger lists it under "Open architectural threads not yet resolved"; this cycle does not commit it. The sample harness is authored in Rust as a plain constructor function over the existing rawHarness::bootstrap(nodes, sources, edges)API — that is "harness wiring in Rust" (C17/C20). The elaborate builder is a later cycle. aura newscaffolder and theAura.tomlschema — deferred architectural threads, untouched.- The data-server source (#7). The sample runs on a built-in synthetic stream; the source is swappable at the ingestion boundary (C3) when #7 lands.
- A git-commit build script. The manifest
commitis filled fromoption_env!("AURA_COMMIT")(defaults to"unknown"); capturing the real HEAD viabuild.rsis deferred. - An argument-parsing dependency (clap/…). The workspace is deliberately
zero-dependency;
aura runhand-parsesstd::env::argsfor one subcommand. - Refactoring the existing
#[cfg(test)]Recorder fixtures to use the shipped node. They stay as historical snapshots; the shippedRecorderis the go-forward reusable one. De-duplication is a possible later tidy.
Architecture
Two deliverables, in dependency order:
-
aura-std::Recorder— a reusable recording node (the glossary sink role): a pure consumer (output: vec![], C8) that holds anmpsc::Sender<(Timestamp, Vec<Scalar>)>as its out-of-graph destination and, on every fired cycle, sends(ctx.now(), row)whererowis the newest value of each declared input column.mpsc(std) keeps aura-std zero-external-dep and avoids theRc/RefCellinterior mutability the purity invariant (C7) forbids — the same destination shape the test fixtures already use. Supports all four scalar kinds (a sink must be able to record any base column, C22), though the CLI uses it only for f64. -
aura-cligains arunsubcommand:fn sample_harness() -> (Harness, Receiver<…>, Receiver<…>)— composes theaura-stdnodes (Sma/Sub/Exposure/SimBroker+ twoRecorders) viaHarness::bootstrap, returning the harness and the two sink receivers (equity, exposure). This is the Rust-authored harness (C17/C20);aura-clilegitimately depends on bothaura-engineandaura-std, so the engine stays domain-free (it never names SMA/exposure — cycle-0007 note).fn run_sample() -> RunReport— bootstrap →runon a built-in synthetic price stream → drain both sinks →f64_field→summarize→ pair with aRunManifest→ return theRunReport. Pure and deterministic (C1): same build → same report.fn main()— hand-parseargs:run⇒println!("{}", run_sample().to_json()), exit 0; anything else ⇒ a one-line usage message to stderr, exit 2.
aura-cli adds aura-std to its dependencies (it currently depends only on
aura-engine).
Concrete code shapes
User-facing: the invocation and its output (the Step-4 evidence)
$ aura run
{"manifest":{"commit":"unknown","params":{"sma_fast":2,"sma_slow":4,"exposure_scale":0.5},"window":[1,7],"seed":0,"broker":"sim-optimal(pip_size=0.0001)"},"metrics":{"total_pips":<e.g. 3>,"max_drawdown":<e.g. 2>,"exposure_sign_flips":<e.g. 1>}}
$ echo $?
0
$ aura
aura: usage: aura run # (to stderr)
$ echo $?
2
The exact metric values are pinned by the Testing strategy below (they follow from the chosen synthetic stream); the JSON shape is the cycle-0009 documented schema. The synthetic stream is chosen to rise then reverse, so the demo trace is non-trivial (a sign flip and a drawdown), per C22's "populated trace".
Delivered: aura-std::Recorder (the sink node)
use aura_core::{Ctx, Firing, InputSpec, Node, NodeSchema, Scalar, ScalarKind, Timestamp};
use std::sync::mpsc::Sender;
/// A recording sink (the glossary *sink* role, C8/C22): a pure consumer that,
/// each fired cycle, sends `(ctx.now(), row)` — the newest value of each input
/// column — to an out-of-graph `mpsc` destination it holds. Returns `None`
/// (records, forwards nothing). `mpsc` keeps the engine's purity invariant (C7):
/// no `Rc`/`RefCell`. Returns `None` (filters) until every input column is warm.
pub struct Recorder {
kinds: Vec<ScalarKind>,
firing: Firing,
tx: Sender<(Timestamp, Vec<Scalar>)>,
}
impl Recorder {
/// A recorder over `kinds.len()` input columns of the given kinds, each with
/// the given firing policy, sending recorded rows to `tx`.
pub fn new(kinds: &[ScalarKind], firing: Firing, tx: Sender<(Timestamp, Vec<Scalar>)>) -> Self { /* … */ }
}
impl Node for Recorder {
fn schema(&self) -> NodeSchema { /* inputs: one InputSpec per kind (lookback 1, self.firing); output: vec![] */ }
fn eval(&mut self, ctx: Ctx<'_>) -> Option<&[Scalar]> {
// read newest of each input column by kind; None until all warm;
// tx.send((ctx.now(), row)); return None.
}
}
Delivered: the CLI seam (aura-cli/src/main.rs)
use aura_engine::{f64_field, summarize, Edge, Harness, RunManifest, RunReport, SourceSpec, Target};
use aura_std::{Exposure, Recorder, SimBroker, Sma, Sub};
use aura_core::{Firing, Scalar, ScalarKind, Timestamp};
use std::sync::mpsc::{self, Receiver};
/// The built-in synthetic price stream: rises then reverses so the demo trace
/// carries a sign flip and a drawdown (C22 populated trace).
fn synthetic_prices() -> Vec<(Timestamp, Scalar)> { /* deterministic ticks t=1.. */ }
/// Bootstrap the sample signal-quality harness with two recording sinks
/// (equity on the SimBroker, exposure on the Exposure node). Rust-authored
/// wiring (C17/C20) over the raw bootstrap API — no builder DSL this cycle.
fn sample_harness() -> (Harness, Receiver<(Timestamp, Vec<Scalar>)>, Receiver<(Timestamp, Vec<Scalar>)>) { /* … */ }
/// Run the sample harness and fold it into a RunReport (drain → f64_field →
/// summarize → RunManifest). Deterministic (C1): same build → same report.
fn run_sample() -> RunReport { /* … */ }
fn main() {
let mut args = std::env::args().skip(1);
match args.next().as_deref() {
Some("run") => println!("{}", run_sample().to_json()),
_ => {
eprintln!("aura: usage: aura run");
std::process::exit(2);
}
}
}
Implementation-shape note (secondary)
The only changed existing file is crates/aura-cli/src/main.rs (stub → the
above) plus crates/aura-cli/Cargo.toml (+aura-std, +aura-core path deps)
and crates/aura-std/src/lib.rs (+mod recorder; pub use recorder::Recorder;).
No aura-engine / Harness / node-contract change.
Components
Recorder(aura-std/src/recorder.rs) — the shipped sink node.synthetic_prices— the built-in deterministic source stream.sample_harness— the Rust-authored sample harness builder (aura-cli).run_sample— bootstrap→run→drain→reduce→report (aura-cli).main— arg dispatch + stdout/stderr/exit-code policy.
Data flow
synthetic_prices → source → SMA(2)/SMA(4) → Sub → Exposure → SimBroker;
the price also taps directly into the broker's price slot (cycle-0007 wiring).
Two Recorders tap the SimBroker equity output and the Exposure output; each
holds an mpsc::Sender. After Harness::run returns, run_sample drains both
receivers (rx.try_iter().collect()), projects field 0 with f64_field,
summarizes the two streams, pairs the metrics with a RunManifest
(commit from option_env!, params/window/seed/broker from the sample's
known configuration), and returns the RunReport. main prints its to_json.
Error handling
- Unknown / missing subcommand → one-line usage to stderr,
exit(2). (runis the only verb this cycle.) run_sampleis total: the sample harness is fixed and valid, soHarness::bootstrapcannot fail (the test pins this);f64_field/summarizeare the cycle-0009 contracts (f64-only sinks, so no kind-mismatch panic on the wired columns). NoResultplumbing in the happy path.Recorder::evalreturnsNoneuntil every input column is warm, then sends; a dropped receiver makestx.senderror, which is ignored (the World owns the receiver lifetime, exactly as the fixtures do).
Testing strategy
aura-stdRecorderunit test: drive a tiny harness (or the node directly) so the recorder captures a known f64 stream; assert the drained(Timestamp, Vec<Scalar>)rows match, and that it returnsNoneduring warm-up (pure consumer). Mirrors the fixture's proven behaviour, now on the shipped node.aura-clirun_sampleunit test (#[cfg(test)] mod testsinmain.rs): callrun_sample()twice; assert the twoRunReports are equal (determinism, C1) and that the metrics equal the hand-computed values for the chosen synthetic stream (pinstotal_pips/max_drawdown/exposure_sign_flips, and a non-zero drawdown + ≥1 sign flip so the demo trace is non-trivial).aura-cliCLI integration test (tests/cli_run.rs): spawn the built binary viastd::process::Command::new(env!("CARGO_BIN_EXE_aura")):run→ exit 0, stdout is exactlyrun_sample().to_json()+ newline (assert the manifest+metrics keys + a parseable single-line object);- no args → exit 2, stderr contains
usage.
Gates (profile commands): cargo test --workspace,
cargo clippy --workspace --all-targets -- -D warnings,
RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps.
Load-bearing decisions flagged for review
- The recording sink ships as
aura-std::Recorder(notaura-engine, notaura-cli-local). Rationale: it is a universal block (C16 — aura-std's remit), domain-free, and reusable by any World/harness;aura-enginestays the bare engine (Harness + report, no nodes). It holds anmpsc::Sender— the purity-preserving destination the fixtures already validated. - Minimal harness authoring: a plain Rust constructor function, no builder
DSL. This satisfies C17/C20 ("wiring is Rust") and closes the skeleton
without prematurely committing the open experiment-builder-API thread. If you
want this cycle to instead introduce a first slice of a real
HarnessBuilderAPI, say so at review — that is a materially bigger cycle and I'd recommend splitting it out. - Zero-dependency CLI: hand-parsed args, no clap;
commitviaoption_env!("AURA_COMMIT")defaulting to"unknown"(nobuild.rsgit capture yet). Both keep the cycle minimal and the workspace dep-free; both are trivially upgraded later.
Acceptance criteria
aura runbootstraps and runs the sample harness end-to-end and exits 0.- The sample harness includes (two) sinks; a run records displayable traces (equity + exposure), drained by the World.
- The run emits the structured metrics + manifest from #6 as canonical JSON on stdout.
- Harness wiring is Rust (a constructor function over the bootstrap API); the
CLI's
mainholds no strategy logic (it only selectsrunand prints). - A reusable
aura-std::Recordersink node ships and is unit-tested. - An integration test drives the real binary (
run→ exit 0 + JSON stdout; bad args → exit 2 + usage stderr). cargo test --workspace, clippy-D warnings, andcargo doc -D warningsare clean; the workspace stays zero-(external-)dependency.