feat(aura-cli): aura mc + family-aware runs + window_of migration (0045 iter 2)
Iteration 2 of spec 0045 — the CLI surface for orchestration families. window_of migration: every manifest-window scan (run_sample, sweep_family, walkforward_family, sweep_over, run_oos, run_macd) now reads the window from Source::bounds() via window_of(&sources) instead of prices.first()/.last(). Behaviour-preserving (the walk-forward roller boundaries are bar-aligned, so the producer-supplied window equals the old first/last) — pinned by the unchanged run_sample (1,7) and walk-forward determinism tests. aura mc: new built-in Monte-Carlo family on the CLI (monte_carlo over a built-in seed set + the sample harness re-seeded per draw), persisted via append_family and rendered as one member line per seed (carrying the family_id) plus an mc_aggregate line. McAggregate is not Serialize, so the aggregate line is built from its three MetricStats blocks. family-aware runs: aura sweep / walkforward / mc now persist to the family store via append_family with an optional --name (per-kind default), printing the assigned family_id per member. aura runs families lists family headers (id, kind, member count); aura runs family <id> [rank <metric>] lists/ranks one family's members as a unit. An unknown family id is an empty family (exit 0); an unknown metric is a usage error (exit 2). Two pre-existing cli_run.rs E2E tests were adapted to the new family contract (a required consequence of sweep/walkforward switching from the flat store to the family store — a plan gap caught at implement time): the sweep-output test now asserts the family-wrapper, and the runs list/rank flow became the family list / per-family-rank flow (which also exercises the per-name counter sweep-0/sweep-1 end-to-end). A new E2E test drives aura mc through the binary. Known cosmetic detail: the family-wrapped sweep/mc/walkforward stdout nests the report via serde_json::json! (to_value -> alphabetical keys, leading "broker"), while runs family / runs list print via RunReport::to_json() (declaration order, leading "commit"). Both valid JSON; key order is not load-bearing and the stored families.jsonl uses to_string (declaration order), so the round-trip is unaffected. Verification: cargo test --workspace green; cargo clippy --workspace --all-targets -D warnings clean. refs #70
This commit is contained in:
@@ -199,7 +199,7 @@ fn graph_emits_self_contained_html_viewer() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sweep_prints_four_json_lines_and_exits_zero() {
|
||||
fn sweep_prints_four_family_json_lines_and_exits_zero() {
|
||||
let cwd = temp_cwd("sweep4");
|
||||
let out = Command::new(BIN).arg("sweep").current_dir(&cwd).output().expect("spawn aura sweep");
|
||||
assert!(out.status.success(), "exit status: {:?}", out.status);
|
||||
@@ -207,9 +207,11 @@ fn sweep_prints_four_json_lines_and_exits_zero() {
|
||||
let stdout = String::from_utf8(out.stdout).expect("utf-8 stdout");
|
||||
assert_eq!(stdout.lines().count(), 4, "stdout was: {stdout:?}");
|
||||
let first = stdout.lines().next().unwrap();
|
||||
// each line is a full RunReport; commit is the real git HEAD (volatile), so
|
||||
// pin the first odometer point's manifest params, not the commit value.
|
||||
assert!(first.starts_with("{\"manifest\":{\"commit\":\""), "got: {stdout}");
|
||||
// each line is now a family member: the assigned `family_id` plus the nested
|
||||
// RunReport (C18/C21 lineage). The first run assigns `sweep-0`; the nested
|
||||
// report is serialized via `serde_json`, so the manifest's fields lead with
|
||||
// `broker` (the commit value is the volatile git HEAD, pinned by params below).
|
||||
assert!(first.starts_with("{\"family_id\":\"sweep-0\",\"report\":{\"manifest\":{"), "got: {stdout}");
|
||||
assert!(
|
||||
first.contains("\"params\":[[\"signals.trend.fast.length\",2.0],[\"signals.trend.slow.length\",4.0],[\"signals.momentum.fast.length\",2.0],[\"signals.momentum.slow.length\",4.0],[\"signals.momentum.signal.length\",3.0],[\"signals.blend.weights[0]\",1.0],[\"signals.blend.weights[1]\",1.0],[\"exposure.scale\",0.5]]"),
|
||||
"got: {stdout}"
|
||||
@@ -224,27 +226,71 @@ fn sweep_with_trailing_token_is_strict_and_exits_two() {
|
||||
assert_eq!(out.status.code(), Some(2), "exit status: {:?}", out.status);
|
||||
}
|
||||
|
||||
/// Property: `aura mc` runs the built-in Monte-Carlo family end-to-end through the
|
||||
/// binary — it prints one member line per seed (carrying the assigned `family_id`)
|
||||
/// plus one `mc_aggregate` line, exits 0, and persists the draws as a discoverable
|
||||
/// `MonteCarlo` family (C12 axis 4 / C18/C21). Driven through the built binary so
|
||||
/// it pins the observable CLI contract, not just the in-crate render helper.
|
||||
#[test]
|
||||
fn runs_list_and_rank_across_invocations() {
|
||||
fn mc_runs_persists_a_monte_carlo_family_and_lists_it() {
|
||||
let cwd = temp_cwd("mc-family");
|
||||
let out = Command::new(BIN).arg("mc").current_dir(&cwd).output().expect("spawn aura mc");
|
||||
assert!(out.status.success(), "mc exit: {:?}", out.status);
|
||||
|
||||
let stdout = String::from_utf8(out.stdout).expect("utf-8 stdout");
|
||||
let lines: Vec<&str> = stdout.lines().collect();
|
||||
// three built-in seeds -> three member lines + one aggregate line.
|
||||
assert_eq!(lines.len(), 4, "mc stdout: {stdout:?}");
|
||||
for line in &lines[..3] {
|
||||
assert!(line.contains("\"family_id\":\"mc-0\""), "member missing family_id: {line}");
|
||||
assert!(line.contains("\"seed\":"), "member missing seed: {line}");
|
||||
}
|
||||
assert!(lines[3].contains("\"mc_aggregate\":"), "missing aggregate line: {}", lines[3]);
|
||||
|
||||
// the run persisted the draws as a discoverable MonteCarlo family.
|
||||
let fams = Command::new(BIN).args(["runs", "families"]).current_dir(&cwd).output().expect("spawn families");
|
||||
assert!(fams.status.success(), "families exit: {:?}", fams.status);
|
||||
let fams_out = String::from_utf8(fams.stdout).expect("utf-8");
|
||||
assert_eq!(fams_out.lines().count(), 1, "families: {fams_out:?}");
|
||||
assert!(fams_out.contains("\"family_id\":\"mc-0\""), "families: {fams_out:?}");
|
||||
assert!(fams_out.contains("\"kind\":\"MonteCarlo\""), "families: {fams_out:?}");
|
||||
assert!(fams_out.contains("\"members\":3"), "families: {fams_out:?}");
|
||||
|
||||
let _ = std::fs::remove_dir_all(&cwd);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runs_families_list_and_per_family_rank_across_invocations() {
|
||||
let cwd = temp_cwd("runs-flow");
|
||||
// two sweeps accumulate 8 records over time
|
||||
// two sweeps accumulate two families (sweep-0, sweep-1) over time, each a
|
||||
// related set of 4 records (C18/C21 lineage), in the sibling family store.
|
||||
for _ in 0..2 {
|
||||
let out = Command::new(BIN).arg("sweep").current_dir(&cwd).output().expect("spawn sweep");
|
||||
assert!(out.status.success(), "sweep exit: {:?}", out.status);
|
||||
}
|
||||
|
||||
// `runs list` shows all 8 stored records
|
||||
let list = Command::new(BIN).args(["runs", "list"]).current_dir(&cwd).output().expect("spawn list");
|
||||
assert!(list.status.success(), "list exit: {:?}", list.status);
|
||||
let list_out = String::from_utf8(list.stdout).expect("utf-8");
|
||||
assert_eq!(list_out.lines().count(), 8, "list: {list_out:?}");
|
||||
assert!(list_out.lines().all(|l| l.starts_with("{\"manifest\":{")), "list shape: {list_out:?}");
|
||||
// `runs families` shows both stored families in first-seen order, each with
|
||||
// its kind + member count.
|
||||
let fams = Command::new(BIN).args(["runs", "families"]).current_dir(&cwd).output().expect("spawn families");
|
||||
assert!(fams.status.success(), "families exit: {:?}", fams.status);
|
||||
let fams_out = String::from_utf8(fams.stdout).expect("utf-8");
|
||||
let fam_lines: Vec<&str> = fams_out.lines().collect();
|
||||
assert_eq!(fam_lines.len(), 2, "families: {fams_out:?}");
|
||||
assert!(fam_lines[0].contains("\"family_id\":\"sweep-0\""), "fam0: {fams_out:?}");
|
||||
assert!(fam_lines[1].contains("\"family_id\":\"sweep-1\""), "fam1: {fams_out:?}");
|
||||
assert!(fam_lines.iter().all(|l| l.contains("\"members\":4")), "members: {fams_out:?}");
|
||||
|
||||
// `runs rank total_pips`: 8 lines, highest total_pips first
|
||||
let rank = Command::new(BIN).args(["runs", "rank", "total_pips"]).current_dir(&cwd).output().expect("spawn rank");
|
||||
// `runs family sweep-0 rank total_pips`: the family's 4 members, highest
|
||||
// total_pips first.
|
||||
let rank = Command::new(BIN)
|
||||
.args(["runs", "family", "sweep-0", "rank", "total_pips"])
|
||||
.current_dir(&cwd)
|
||||
.output()
|
||||
.expect("spawn rank");
|
||||
assert!(rank.status.success(), "rank exit: {:?}", rank.status);
|
||||
let rank_out = String::from_utf8(rank.stdout).expect("utf-8");
|
||||
assert_eq!(rank_out.lines().count(), 8, "rank: {rank_out:?}");
|
||||
assert_eq!(rank_out.lines().count(), 4, "rank: {rank_out:?}");
|
||||
assert!(rank_out.lines().all(|l| l.starts_with("{\"manifest\":{")), "rank shape: {rank_out:?}");
|
||||
let pips: Vec<f64> = rank_out
|
||||
.lines()
|
||||
.map(|l| {
|
||||
@@ -257,10 +303,24 @@ fn runs_list_and_rank_across_invocations() {
|
||||
.collect();
|
||||
assert!(pips.windows(2).all(|w| w[0] >= w[1]), "rank not descending: {pips:?}");
|
||||
|
||||
// unknown metric is a usage error
|
||||
let bogus = Command::new(BIN).args(["runs", "rank", "bogus"]).current_dir(&cwd).output().expect("spawn bogus");
|
||||
// an unknown metric is a usage error
|
||||
let bogus = Command::new(BIN)
|
||||
.args(["runs", "family", "sweep-0", "rank", "bogus"])
|
||||
.current_dir(&cwd)
|
||||
.output()
|
||||
.expect("spawn bogus");
|
||||
assert_eq!(bogus.status.code(), Some(2), "bogus exit: {:?}", bogus.status);
|
||||
|
||||
// an unknown family id is an empty family: zero lines, exit 0 (consistent
|
||||
// with `runs list` over an empty store).
|
||||
let unknown = Command::new(BIN)
|
||||
.args(["runs", "family", "nope-9"])
|
||||
.current_dir(&cwd)
|
||||
.output()
|
||||
.expect("spawn unknown");
|
||||
assert!(unknown.status.success(), "unknown family exit: {:?}", unknown.status);
|
||||
assert_eq!(unknown.stdout.len(), 0, "unknown family stdout: {:?}", unknown.stdout);
|
||||
|
||||
let _ = std::fs::remove_dir_all(&cwd);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user