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
This commit is contained in:
2026-06-21 16:31:52 +02:00
parent 99fd32b1f9
commit 094d63bc47
+68 -6
View File
@@ -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 <sma|momentum>] [--name <n>|--trace <n>]`: 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):