From 094d63bc47661f84c61bf0d340805800198fc7dd Mon Sep 17 00:00:00 2001 From: Brummel Date: Sun, 21 Jun 2026 16:31:52 +0200 Subject: [PATCH] fix(family-stdout): unify family-wrapper key order with the store The sweep / walk-forward / mc family-member stdout lines routed the embedded RunReport through serde_json::json! -> a serde_json::Value, which re-alphabetizes the manifest keys (broker-first) and, for mc, even reordered the top-level keys (seed after report). That diverged from the commit-first declaration order that RunReport::to_json and the stored families.jsonl already use. Splice the pre-serialized report into a hand-built line via family_member_line / mc_member_line so each family-member stdout line is byte-identical to its families.jsonl record (commit-first manifest, seed between family_id and report). RED-first: the two new key-order tests fail against the old json! helpers and pass after the format! rewrite. closes #99 --- crates/aura-cli/src/main.rs | 74 ++++++++++++++++++++++++++++++++++--- 1 file changed, 68 insertions(+), 6 deletions(-) diff --git a/crates/aura-cli/src/main.rs b/crates/aura-cli/src/main.rs index 9457aad..053f3bd 100644 --- a/crates/aura-cli/src/main.rs +++ b/crates/aura-cli/src/main.rs @@ -916,6 +916,31 @@ fn parse_walkforward_args(rest: &[&str]) -> Result<(String, bool, DataChoice), S Ok((name, persist, real.finish(&usage)?)) } +/// Render a family-member stdout line: the assigned `family_id` plus the embedded +/// `RunReport`. The report is emitted in its own declaration key order (manifest +/// leads with `commit`, C18) so the line is byte-identical to the stored +/// `families.jsonl`. `serde_json::json!` would route the report through a +/// `serde_json::Value` and re-alphabetize the manifest keys (broker-first), +/// diverging from the store — hence the report is spliced in pre-serialized (#99). +fn family_member_line(id: &str, report: &RunReport) -> String { + format!( + r#"{{"family_id":{},"report":{}}}"#, + serde_json::to_string(id).expect("a string id always serializes"), + report.to_json() + ) +} + +/// Monte-Carlo variant of [`family_member_line`]: the per-draw line also carries the +/// realization `seed` (between `family_id` and `report`), matching `run_mc`'s shape. +fn mc_member_line(id: &str, seed: u64, report: &RunReport) -> String { + format!( + r#"{{"family_id":{},"seed":{},"report":{}}}"#, + serde_json::to_string(id).expect("a string id always serializes"), + seed, + report.to_json() + ) +} + /// `aura sweep [--strategy ] [--name |--trace ]`: run the /// selected built-in sweep, persist it as a *family* (related records sharing one /// `family_id`, C18/C21) via `append_family`, and print each point's record line @@ -935,7 +960,7 @@ fn run_sweep(strategy: Strategy, name: &str, persist: bool, data: DataSource) { } }; for pt in &family.points { - println!("{}", serde_json::json!({ "family_id": id, "report": pt.report })); + println!("{}", family_member_line(&id, &pt.report)); } } @@ -960,7 +985,7 @@ fn run_walkforward(name: &str, persist: bool, data: DataSource) { } }; for w in &result.windows { - println!("{}", serde_json::json!({ "family_id": id, "report": w.run.oos_report })); + println!("{}", family_member_line(&id, &w.run.oos_report)); } println!("{}", walkforward_summary_json(&result)); } @@ -1175,10 +1200,7 @@ fn run_mc(name: &str, persist: bool) { } }; for draw in &family.draws { - println!( - "{}", - serde_json::json!({ "family_id": id, "seed": draw.seed, "report": draw.report }) - ); + println!("{}", mc_member_line(&id, draw.seed, &draw.report)); } println!("{}", mc_aggregate_json(&family.aggregate)); } @@ -1432,6 +1454,46 @@ fn main() { mod tests { use super::*; + /// #99: a sweep/walk-forward family-member stdout line embeds the `RunReport` in + /// its own declaration key order (manifest leads with `commit`), byte-matching the + /// stored `families.jsonl` — never `serde_json::Value`'s alphabetical order (which + /// would lead the manifest with `broker`). + #[test] + fn family_member_line_keeps_report_in_store_key_order() { + let report = RunReport { + manifest: sim_optimal_manifest(vec![], (Timestamp(0), Timestamp(0)), 0, 1.0), + metrics: summarize(&[], &[]), + }; + let line = family_member_line("demo-1", &report); + assert!( + line.starts_with(r#"{"family_id":"demo-1","report":{"manifest":{"commit":"#), + "got: {line}" + ); + assert!( + !line.contains(r#""manifest":{"broker":"#), + "manifest re-alphabetized (broker-first), should be commit-first: {line}" + ); + } + + /// #99: the Monte-Carlo per-draw line carries the `seed` between `family_id` and + /// `report`, and the embedded report stays in store (commit-first) key order. + #[test] + fn mc_member_line_keeps_report_in_store_key_order_with_seed() { + let report = RunReport { + manifest: sim_optimal_manifest(vec![], (Timestamp(0), Timestamp(0)), 7, 1.0), + metrics: summarize(&[], &[]), + }; + let line = mc_member_line("mc-1", 7, &report); + assert!( + line.starts_with(r#"{"family_id":"mc-1","seed":7,"report":{"manifest":{"commit":"#), + "got: {line}" + ); + assert!( + !line.contains(r#""manifest":{"broker":"#), + "manifest re-alphabetized (broker-first), should be commit-first: {line}" + ); + } + // The vetted GER40 real-data window: the whole of September 2024 (UTC, // inclusive), the same calendar month the gated ingest `ger40_breakout_real` // test drives. Expressed in Unix-ms (`run_sample_real`'s window currency):