feat(aura-cli): aura runs registry — persist on sweep, list + rank (cycle D / iter 3)

Final iteration of the run-registry cycle (#33): the read surface that makes a
sweep family — and runs across separate invocations — comparable over time
(C18's "compare experiments over time", which has no home in git or Gitea).

- `aura sweep` now persists each point's RunReport to runs/runs.jsonl (append-
  only, one serde_json line per run) in addition to printing it.
- `aura runs list` prints every stored record, in store order.
- `aura runs rank <metric>` prints the stored runs best-first by the metric
  (total_pips desc; max_drawdown / exposure_sign_flips asc); an unknown metric
  is a usage error (exit 2), like every other aura arg error.

sweep_report splits into a production sweep_family() (the pure run) and a
#[cfg(test)] renderer kept for the in-bin goldens; the dispatch gains run_sweep
/ runs_list / runs_rank over default_registry() (runs/runs.jsonl under cwd).
Process goldens run the binary in a temp cwd so persistence never dirties the
repo; /runs/ is git-ignored as a backstop.

Verified end-to-end: two `aura sweep` invocations accumulate 8 records;
`aura runs list` shows all 8; `aura runs rank total_pips` orders them best-first;
`aura runs rank bogus` exits 2 with the known-metrics hint. cargo test
--workspace green (cli_run 11, registry 5, engine 93, …); clippy
--workspace --all-targets -D warnings clean; no stray runs/ in the work tree.

refs #33
This commit is contained in:
2026-06-10 20:37:51 +02:00
parent 2811c060dc
commit 5bded0ca83
5 changed files with 143 additions and 10 deletions
+68 -7
View File
@@ -11,8 +11,9 @@ mod render;
use aura_core::{Firing, Scalar, ScalarKind, Timestamp};
use aura_engine::{
f64_field, summarize, sweep, BlueprintNode, Composite, Edge, FlatGraph, GridSpace, Harness,
OutField, ParamAlias, Role, RunManifest, RunReport, SourceSpec, Target,
OutField, ParamAlias, Role, RunManifest, RunReport, SourceSpec, SweepFamily, Target,
};
use aura_registry::{rank_by, Registry};
use aura_std::{Ema, Exposure, Recorder, SimBroker, Sma, Sub};
use std::sync::mpsc::{self, Receiver};
@@ -222,7 +223,7 @@ fn scalar_as_param_f64(s: &Scalar) -> f64 {
/// the same report. Each point builds a fresh blueprint (fresh sink channels),
/// bootstraps it under the point vector, runs it, and folds the drained sinks to
/// metrics — the per-point closure the engine `sweep` drives disjointly.
fn sweep_report() -> String {
fn sweep_family() -> SweepFamily {
let space = sample_blueprint_with_sinks().0.param_space();
let grid = GridSpace::new(
&space,
@@ -233,7 +234,7 @@ fn sweep_report() -> String {
],
)
.expect("the built-in grid matches the sample param-space");
let family = sweep(&grid, |point| {
sweep(&grid, |point| {
let (bp, rx_eq, rx_ex) = sample_blueprint_with_sinks();
let mut h = bp
.bootstrap_with_params(point.to_vec())
@@ -261,15 +262,72 @@ fn sweep_report() -> String {
},
metrics: summarize(&equity, &exposure),
}
});
})
}
/// Render a sweep family as one `RunReport` JSON line per point. Test helper:
/// production (`run_sweep`) renders *and* persists per point.
#[cfg(test)]
fn sweep_report() -> String {
let mut out = String::new();
for pt in &family.points {
for pt in &sweep_family().points {
out.push_str(&pt.report.to_json());
out.push('\n');
}
out
}
/// The default run registry: an append-only JSONL store under the current
/// working directory. (A project-configured runs-dir via `Aura.toml` is a later
/// refinement.)
fn default_registry() -> Registry {
Registry::open("runs/runs.jsonl")
}
/// `aura sweep`: run the built-in sweep, persist each point's `RunReport` to the
/// registry (the run record, queryable over time — C18), and print each as one
/// JSON line.
fn run_sweep() {
let reg = default_registry();
for pt in &sweep_family().points {
if let Err(e) = reg.append(&pt.report) {
eprintln!("aura: {e}");
std::process::exit(2);
}
println!("{}", pt.report.to_json());
}
}
/// `aura runs list`: print every stored run record, in store (over-time) order.
fn runs_list() {
for report in &load_runs_or_exit() {
println!("{}", report.to_json());
}
}
/// `aura runs rank <metric>`: print the stored runs best-first by `metric`.
fn runs_rank(metric: &str) {
match rank_by(load_runs_or_exit(), metric) {
Ok(ranked) => {
for report in &ranked {
println!("{}", report.to_json());
}
}
Err(e) => {
eprintln!("aura: {e}");
std::process::exit(2);
}
}
}
/// Load the registry or exit 2 with the error. A missing registry loads empty.
fn load_runs_or_exit() -> Vec<RunReport> {
default_registry().load().unwrap_or_else(|e| {
eprintln!("aura: {e}");
std::process::exit(2);
})
}
// --- MACD proof-of-concept (a richer, nested indicator + strategy) -----------
/// The MACD signal as a named composite: price → fast/slow `Ema` → the MACD line
@@ -426,7 +484,8 @@ fn run_macd() -> RunReport {
}
}
const USAGE: &str = "usage: aura run [--macd] | aura graph | aura sweep";
const USAGE: &str =
"usage: aura run [--macd] | aura graph | aura sweep | aura runs list | aura runs rank <metric>";
fn main() {
// Collect argv and match the whole vector: every accepted form is exhaustive,
@@ -437,7 +496,9 @@ fn main() {
["run"] => println!("{}", run_sample().to_json()),
["run", "--macd"] => println!("{}", run_macd().to_json()),
["graph"] => print!("{}", render::render_html(&sample_blueprint())),
["sweep"] => print!("{}", sweep_report()),
["sweep"] => run_sweep(),
["runs", "list"] => runs_list(),
["runs", "rank", metric] => runs_rank(metric),
["--help"] | ["-h"] => println!("{USAGE}"),
_ => {
eprintln!("aura: {USAGE}");