fix(cli): reproduce re-derives real-data families from the member manifest (#229)

reproduce_family no longer hardcodes DataSource::Synthetic: it reads the
family's instrument + window from the persisted manifest and rebuilds the
same local-archive source the dissolved verbs run over; a missing archive
refuses with the verbs' established named-data refusal (exit 1) instead of
silently re-running over wrong data. Sweep families now reproduce N/N
bit-identically; WalkForward families yield a verdict instead of the
empty-OOS-window panic. The synthetic-family path is byte-unchanged.

Accepted minimal duplication: reproduce_family performs its own family
lookup to read the instrument before delegating to reproduce_family_in,
whose signature and unit-test seam stay byte-identical (constraint:
minimal fix).

MonteCarlo reproduce still runs the synthetic seed walk regardless of a
real manifest — pre-existing, semantically distinct (bit-identity over
bootstrap resamples), filed forward.

Verified: both #229 RED tests green over the GER40 archive, full workspace
suite 1107/0, clippy -D warnings clean.

closes #229
This commit is contained in:
2026-07-10 15:32:39 +02:00
parent 0408fc41a2
commit 3016bd6b11
+48 -16
View File
@@ -24,7 +24,7 @@ use aura_engine::{
};
use aura_registry::{
check_r_metric, group_families, mc_member_reports, optimize_deflated,
optimize_plateau, rank_by, sweep_member_reports, walkforward_member_reports, FamilyKind,
optimize_plateau, rank_by, sweep_member_reports, walkforward_member_reports, Family, FamilyKind,
FamilyMember, Generalization, NameKind, PlateauMode, Registry, RunTraces,
DEFLATION_BLOCK_LEN, DEFLATION_N_RESAMPLES,
};
@@ -1031,6 +1031,22 @@ fn point_from_params(space: &[ParamSpec], params: &[(String, Scalar)]) -> Vec<Ce
.collect()
}
/// Look up a persisted family by id, or exit 1 (unknown id / registry load failure) —
/// the single place `reproduce_family` and `reproduce_family_in` resolve a family, so
/// the two exit-1 error phrasings can't drift out of sync between the call sites.
fn load_family_or_exit(reg: &Registry, id: &str) -> Family {
let members = reg.load_family_members().unwrap_or_else(|e| {
eprintln!("aura: {e}");
std::process::exit(1);
});
group_families(members).into_iter().find(|f| f.id == id).unwrap_or_else(|| {
// reproduce is an action, not a lookup: an unknown id is a hard error (distinct
// from `runs family <id>`'s treat-as-empty exit 0).
eprintln!("aura: no such family '{id}'");
std::process::exit(1);
})
}
/// Re-derive every member of a persisted sweep family from the content-addressed store
/// and compare to the stored result, against an explicit registry (testable seam).
fn reproduce_family_in(
@@ -1039,18 +1055,8 @@ fn reproduce_family_in(
data: &DataSource,
env: &project::Env,
) -> ReproduceReport {
let members = reg.load_family_members().unwrap_or_else(|e| {
eprintln!("aura: {e}");
std::process::exit(1);
});
let Some(family) = group_families(members).into_iter().find(|f| f.id == id) else {
// reproduce is an action, not a lookup: an unknown id is a hard error (distinct
// from `runs family <id>`'s treat-as-empty exit 0).
eprintln!("aura: no such family '{id}'");
std::process::exit(1);
};
let family = load_family_or_exit(reg, id);
let pip = data.pip_size();
let window = data.full_window(env);
let mut outcomes = Vec::new();
for member in &family.members {
let stored = &member.report;
@@ -1123,7 +1129,19 @@ fn reproduce_family_in(
let w = window_of(&s).expect("non-empty OOS window");
(s, w)
}
_ => (data.run_sources(env), window),
// Sweep / plain-run members: a Real source reopens the exact per-member
// window the manifest recorded, the same `windowed_sources` loader
// WalkForward uses above (and `CliMemberRunner` uses at mint time, #229)
// — never a fresh full-archive probe, which need not match the window the
// family was minted over. Synthetic keeps the pre-#229 full-window path
// (byte-identical: `data.full_window` is a pure, no-IO computation).
_ => match data {
DataSource::Real { .. } => {
let (from, to) = stored.manifest.window;
(data.windowed_sources(from, to, env), (from, to))
}
DataSource::Synthetic => (data.run_sources(env), data.full_window(env)),
},
};
let rerun = run_blueprint_member(
reload(),
@@ -1144,10 +1162,24 @@ fn reproduce_family_in(
/// `aura reproduce <family-id>`: re-derive a persisted sweep family from the
/// content-addressed blueprint store and verify each member reproduces bit-identically
/// (C18 "re-derives full results on demand"). Synthetic data this cycle (deterministic);
/// recorded-dataset reproduction rides the DataServer seam (#124).
/// (C18 "re-derives full results on demand"). The data source is reconstructed from
/// the family's own manifest (#229) — a hardcoded `DataSource::Synthetic` here would
/// re-derive a real-data family over the wrong stream; see `reproduce_family_in`'s
/// per-member window loader for how the reconstructed source is actually used.
fn reproduce_family(id: &str, env: &project::Env) {
let rep = reproduce_family_in(&env.registry(), id, &DataSource::Synthetic, env);
let reg = env.registry();
let family = load_family_or_exit(&reg, id);
// Reconstruct the DataSource the family was minted over: `None` instrument
// (every synthetic-family member, pre-#229 lines) stays the built-in synthetic
// stream; a real instrument reopens the same local archive `--real` runs use,
// via the same named-data refusal (exit 1) on a missing sidecar/archive.
let data = match family.members.first().and_then(|m| m.report.manifest.instrument.clone()) {
None => DataSource::Synthetic,
Some(symbol) => {
DataSource::from_choice(DataChoice::Real { symbol, from_ms: None, to_ms: None }, env)
}
};
let rep = reproduce_family_in(&reg, id, &data, env);
let total = rep.outcomes.len();
let ok = rep.outcomes.iter().filter(|(_, b)| *b).count();
for (label, identical) in &rep.outcomes {