feat(aura-runner, aura-cli): reproduce returns its report; mismatch guard replaces silent DIVERGED
reproduce_family returns Result<ReproduceReport, RunnerError> — divergence is a result the embedding caller reads (all_identical()), never an error; the CLI renders the per-member lines and summary byte-identically and owns the exit (1 on divergence, no stderr line). The library's print block and the empty-message refusal are gone; exit_on_runner_error always prints (no empty producer remains). reproduce_family_in gains the identity/pip guard, firing before any store fetch: an instrument mismatch (incl. synthetic-for-real) and a broker/pip mismatch — forward-built label compare against the stored broker string (RunManifest carries no pip field; the mint's label constructors became shared pub(crate) helpers, so guard and stamp cannot drift). The class is context-borne: the explicit-source seam refuses 2 (caller-named source), the derived simple path refuses 1 (data drift), same prose. A window guard is deliberately absent — no reproduce path takes a caller window. In-process tests pin the promise: divergence returns Ok, drift refuses class 1, the host survives. Fork minutes: issues/299#issuecomment-4875, -4877, -4879. closes #299
This commit is contained in:
+161
-12
@@ -1069,12 +1069,11 @@ pub(crate) fn exit_on_campaign_result(r: Result<usize, String>) {
|
||||
}
|
||||
}
|
||||
|
||||
/// The dispatch_reproduce pattern, shared: print the runner refusal and
|
||||
/// exit with its class (#297). Empty message = no stderr line.
|
||||
/// The shared runner-refusal arm: print the message and exit with its
|
||||
/// class (#297). Every producer carries prose since #299 retired the
|
||||
/// empty-message divergence refusal (divergence is report data now).
|
||||
fn exit_on_runner_error(e: aura_runner::RunnerError) -> ! {
|
||||
if !e.message.is_empty() {
|
||||
eprintln!("aura: {}", e.message);
|
||||
}
|
||||
eprintln!("aura: {}", e.message);
|
||||
std::process::exit(e.exit_code);
|
||||
}
|
||||
|
||||
@@ -1467,13 +1466,24 @@ fn dispatch_runs(a: RunsCmd, env: &aura_runner::project::Env) {
|
||||
}
|
||||
|
||||
fn dispatch_reproduce(a: ReproduceCmd, env: &aura_runner::project::Env) {
|
||||
// `reproduce_family` (#295) returns a `RunnerError` instead of exiting
|
||||
// the process itself; the shared helper prints its message (empty for
|
||||
// the "not every member reproduced" refusal, since no stderr line
|
||||
// accompanies it — only the stdout summary `reproduce_family` already
|
||||
// emitted) and exits with its code.
|
||||
if let Err(e) = aura_runner::reproduce::reproduce_family(&a.id, env) {
|
||||
exit_on_runner_error(e);
|
||||
// `reproduce_family` (#299) returns the report as a value — the library
|
||||
// never prints and never exits. This arm renders it (byte-identical to
|
||||
// the pre-#299 shell) and owns the exit: `exit(1)` when not every member
|
||||
// reproduced, with NO accompanying stderr line (unchanged observable).
|
||||
match aura_runner::reproduce::reproduce_family(&a.id, env) {
|
||||
Ok(report) => {
|
||||
let total = report.outcomes.len();
|
||||
let ok = report.outcomes.iter().filter(|(_, b)| *b).count();
|
||||
for (label, identical) in &report.outcomes {
|
||||
let verdict = if *identical { "bit-identical" } else { "DIVERGED" };
|
||||
println!("{} member {label} reproduced: {verdict}", a.id);
|
||||
}
|
||||
println!("reproduced {ok}/{total} members bit-identically");
|
||||
if ok != total {
|
||||
std::process::exit(1); // no stderr line — unchanged observable
|
||||
}
|
||||
}
|
||||
Err(e) => exit_on_runner_error(e),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1933,6 +1943,24 @@ mod tests {
|
||||
.expect("loads")
|
||||
}
|
||||
|
||||
/// An `Env` whose registry resolves to `dir` (#299): the
|
||||
/// project-facing surface `reproduce_family` reads via `env.registry()`,
|
||||
/// built directly (no `Aura.toml` on disk) by pointing `paths.runs` at the
|
||||
/// same absolute directory a test's own `Registry::open` uses — so seeding
|
||||
/// through the raw `Registry` handle and reading back through
|
||||
/// `reproduce_family`'s derived path see the identical `runs.jsonl`.
|
||||
fn env_over(dir: &std::path::Path) -> aura_runner::project::Env {
|
||||
aura_runner::project::Env::with_project(aura_runner::project::ProjectEnv {
|
||||
root: std::path::PathBuf::from("."),
|
||||
toml: aura_runner::project::AuraToml {
|
||||
paths: aura_runner::project::AuraPaths { data: None, runs: Some(dir.to_path_buf()) },
|
||||
nodes: Default::default(),
|
||||
},
|
||||
commit: None,
|
||||
native: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn cmp_member(key: &str, ts: &[i64], vals: &[f64]) -> FamilyMember {
|
||||
cmp_member_win(key, ts, vals, (0, 0))
|
||||
}
|
||||
@@ -2302,6 +2330,127 @@ mod tests {
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
/// #299: `reproduce_family` (the derived-source seam behind
|
||||
/// plain `aura reproduce <id>`) survives a family whose STORED metrics were
|
||||
/// hand-falsified after minting — it returns `Ok(report)` with
|
||||
/// `all_identical() == false`, never a process exit or panic. The process
|
||||
/// obviously survives: this test returns normally and inspects the value
|
||||
/// `reproduce_family` handed back, not a captured exit code.
|
||||
#[test]
|
||||
fn reproduce_family_survives_a_falsified_stored_metric() {
|
||||
let dir = std::path::Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/../../target/tmp"))
|
||||
.join("aura-repro-falsified");
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(&dir).expect("temp dir");
|
||||
let reg = Registry::open(dir.join("runs.jsonl"));
|
||||
|
||||
let env = aura_runner::project::Env::std();
|
||||
let closed = load_closed_r_sma();
|
||||
let doc = blueprint_to_json(&closed).expect("serializes");
|
||||
let reload = || blueprint_from_json(&doc, &|t| std_vocabulary(t)).expect("loads");
|
||||
let topo = test_topology_hash(&reload());
|
||||
let space = blueprint_axis_probe(&doc, &env).param_space();
|
||||
let data = DataSource::Synthetic;
|
||||
let pip = data.pip_size();
|
||||
let window = data.full_window(&env).expect("synthetic full_window never refuses");
|
||||
let binding = aura_runner::binding::resolve_binding("falsified", reload().input_roles(), &BTreeMap::new())
|
||||
.expect("the price role resolves");
|
||||
|
||||
let mut report = run_blueprint_member(
|
||||
reload(),
|
||||
&[],
|
||||
&space,
|
||||
data.run_sources(&env, &binding.columns()).expect("synthetic run_sources never refuses"),
|
||||
window,
|
||||
0,
|
||||
pip,
|
||||
&topo,
|
||||
&env,
|
||||
StopRule::Vol { length: R_SMA_STOP_LENGTH, k: R_SMA_STOP_K },
|
||||
&binding,
|
||||
&[],
|
||||
"",
|
||||
)
|
||||
.expect("no cost model: nothing to refuse");
|
||||
// Hand-falsify the STORED metric after an honest mint: the record on
|
||||
// disk now diverges from what a re-run actually produces — exactly the
|
||||
// property `reproduce_family` must surface as data, not a process exit.
|
||||
report.metrics.total_pips += 1.0;
|
||||
|
||||
let canonical = blueprint_to_json(&reload()).unwrap();
|
||||
reg.put_blueprint(&topo, &canonical).expect("store blueprint");
|
||||
let id = reg.append_family("falsified", FamilyKind::Sweep, &[report]).expect("append");
|
||||
|
||||
let rep = aura_runner::reproduce::reproduce_family(&id, &env_over(&dir))
|
||||
.expect("a divergent member is a reported outcome, not a refusal");
|
||||
assert!(!rep.all_identical(), "the falsified member must report DIVERGED: {:?}", rep.outcomes);
|
||||
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
/// #299: the derived-source seam's identity/pip guard
|
||||
/// (Fork 6) refuses class 1, NOT class 2, on `reproduce_family` — a
|
||||
/// drifted stored broker label is data drift within the family (this
|
||||
/// path's `data` is reconstructed FROM the manifest, never caller-given),
|
||||
/// distinct from `reproduce_family_in`'s explicit-source class 2 (pinned
|
||||
/// in `aura_runner::reproduce`'s own test module).
|
||||
#[test]
|
||||
fn reproduce_family_reports_the_derived_path_mismatch_as_class_one() {
|
||||
let dir = std::path::Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/../../target/tmp"))
|
||||
.join("aura-repro-derived-class");
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(&dir).expect("temp dir");
|
||||
let reg = Registry::open(dir.join("runs.jsonl"));
|
||||
|
||||
let env = aura_runner::project::Env::std();
|
||||
let closed = load_closed_r_sma();
|
||||
let doc = blueprint_to_json(&closed).expect("serializes");
|
||||
let reload = || blueprint_from_json(&doc, &|t| std_vocabulary(t)).expect("loads");
|
||||
let topo = test_topology_hash(&reload());
|
||||
let space = blueprint_axis_probe(&doc, &env).param_space();
|
||||
let data = DataSource::Synthetic;
|
||||
let pip = data.pip_size();
|
||||
let window = data.full_window(&env).expect("synthetic full_window never refuses");
|
||||
let binding = aura_runner::binding::resolve_binding("driftclass", reload().input_roles(), &BTreeMap::new())
|
||||
.expect("the price role resolves");
|
||||
|
||||
let mut report = run_blueprint_member(
|
||||
reload(),
|
||||
&[],
|
||||
&space,
|
||||
data.run_sources(&env, &binding.columns()).expect("synthetic run_sources never refuses"),
|
||||
window,
|
||||
0,
|
||||
pip,
|
||||
&topo,
|
||||
&env,
|
||||
StopRule::Vol { length: R_SMA_STOP_LENGTH, k: R_SMA_STOP_K },
|
||||
&binding,
|
||||
&[],
|
||||
"",
|
||||
)
|
||||
.expect("no cost model: nothing to refuse");
|
||||
// Drift the STORED broker label away from what this pip re-derives
|
||||
// (mirrors `aura_runner::reproduce`'s own pip-mismatch guard fixture,
|
||||
// but read back through the derived `reproduce_family` seam here).
|
||||
report.manifest.broker = "sim-optimal(pip_size=0.0002)".to_string();
|
||||
|
||||
let canonical = blueprint_to_json(&reload()).unwrap();
|
||||
reg.put_blueprint(&topo, &canonical).expect("store blueprint");
|
||||
let id = reg.append_family("driftclass", FamilyKind::Sweep, &[report]).expect("append");
|
||||
|
||||
let err = aura_runner::reproduce::reproduce_family(&id, &env_over(&dir))
|
||||
.expect_err("a drifted stored broker label must refuse, not silently reproduce");
|
||||
assert_eq!(err.exit_code, 1, "derived-path mismatch is class 1, not class 2: {}", err.message);
|
||||
assert!(
|
||||
err.message.contains("reproduce broker/pip mismatch"),
|
||||
"the refusal names the mismatch: {}",
|
||||
err.message
|
||||
);
|
||||
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reproduce_re_derives_the_member_stop_regime_not_the_default() {
|
||||
// Property: reproduce re-runs each member under the SAME stop regime the member
|
||||
|
||||
@@ -77,7 +77,7 @@ pub fn sim_optimal_manifest(
|
||||
defaults: Vec::new(),
|
||||
window,
|
||||
seed,
|
||||
broker: format!("sim-optimal(pip_size={pip_size})"),
|
||||
broker: sim_optimal_broker_label(pip_size),
|
||||
selection: None,
|
||||
instrument: None,
|
||||
topology_hash: None,
|
||||
@@ -101,10 +101,18 @@ pub fn raw_bound_defaults(signal: &Composite) -> Vec<(String, Scalar)> {
|
||||
signal.bound_param_space().into_iter().map(|b| (b.name, b.value)).collect()
|
||||
}
|
||||
|
||||
/// The plain sim-optimal broker label (no RiskExecutor branch): shared by
|
||||
/// `sim_optimal_manifest` and (#299) `reproduce_family_in`'s forward-built
|
||||
/// guard comparison, so the two cannot drift.
|
||||
pub(crate) fn sim_optimal_broker_label(pip_size: f64) -> String {
|
||||
format!("sim-optimal(pip_size={pip_size})")
|
||||
}
|
||||
|
||||
/// The honest broker label for the dual-tap r-sma harness: it runs a RiskExecutor
|
||||
/// branch alongside the SimBroker, so the plain "sim-optimal" label would under-report
|
||||
/// it (#132). Shared by the single run and the sweep so the two cannot drift.
|
||||
fn r_sma_broker_label(pip_size: f64) -> String {
|
||||
/// it (#132). Shared by the single run and the sweep (#299: and `reproduce_family_in`'s
|
||||
/// forward-built guard comparison) so they cannot drift.
|
||||
pub(crate) fn r_sma_broker_label(pip_size: f64) -> String {
|
||||
format!("sim-optimal+risk-executor(pip_size={pip_size})")
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
//! Bit-identical reproduction (`aura reproduce`) — #295.
|
||||
//!
|
||||
//! A library function here reports a refusal as a returned
|
||||
//! [`crate::RunnerError`] rather than ending the process. The shell dispatch
|
||||
//! arm (`aura-cli`'s `dispatch_reproduce`) is the single place that prints
|
||||
//! the error's message to stderr and calls `std::process::exit` on its code,
|
||||
//! keeping the observable stderr/exit bytes unchanged (C18).
|
||||
//! Both outcomes here are values, not process control flow: success returns a
|
||||
//! [`ReproduceReport`] the caller inspects (`ReproduceReport::all_identical`);
|
||||
//! a refusal returns a [`crate::RunnerError`] — neither ends the process. The
|
||||
//! shell dispatch arm (`aura-cli`'s `dispatch_reproduce`) is the single place
|
||||
//! that renders the report and prints a refusal's message to stderr, calling
|
||||
//! `std::process::exit` on its code, keeping the observable stderr/exit bytes
|
||||
//! unchanged (C18); a library embedder (a World program) reads the returned
|
||||
//! report directly instead.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::mpsc;
|
||||
@@ -23,10 +26,18 @@ use crate::RunnerError;
|
||||
|
||||
/// The outcome of reproducing one persisted family: per member, whether its re-run
|
||||
/// metrics are bit-identical to the stored metrics (C1).
|
||||
#[derive(Debug)]
|
||||
pub struct ReproduceReport {
|
||||
pub outcomes: Vec<(String, bool)>,
|
||||
}
|
||||
|
||||
impl ReproduceReport {
|
||||
/// True iff every member reproduced bit-identically (C1).
|
||||
pub fn all_identical(&self) -> bool {
|
||||
self.outcomes.iter().all(|(_, identical)| *identical)
|
||||
}
|
||||
}
|
||||
|
||||
/// Look up a persisted family by id, or refuse (exit code 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
|
||||
@@ -68,11 +79,35 @@ pub fn load_family(reg: &Registry, id: &str) -> Result<Family, RunnerError> {
|
||||
|
||||
/// 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).
|
||||
///
|
||||
/// Refusal contract: an identity/pip mismatch (below) refuses class 2 — this is the
|
||||
/// explicit-source seam, so a caller-given `data` that contradicts the member
|
||||
/// manifests is argv-named content (C14). `reproduce_family`, the simple/derived
|
||||
/// path, hits the same guard at class 1 instead (Fork 6, #299: on that path `data`
|
||||
/// is derived FROM the family's own manifest, so a mismatch there is data drift, not
|
||||
/// a caller error). A WINDOW guard is deliberately absent: no reproduce path takes a
|
||||
/// caller window (WF and real-sweep re-runs load stored.manifest.window; MonteCarlo
|
||||
/// derives its walk from manifest.seed; synthetic uses the full window).
|
||||
pub fn reproduce_family_in(
|
||||
reg: &Registry,
|
||||
id: &str,
|
||||
data: &DataSource,
|
||||
env: &Env,
|
||||
) -> Result<ReproduceReport, RunnerError> {
|
||||
reproduce_family_with_class(reg, id, data, env, 2)
|
||||
}
|
||||
|
||||
/// The shared reproduce loop (#299 Fork 6): `mismatch_class` is the exit code the
|
||||
/// identity/pip guard refuses with — context-borne, not a property of the guard
|
||||
/// itself. `reproduce_family_in` (explicit-source seam) passes 2; `reproduce_family`
|
||||
/// (derived-source seam) passes 1. Guard messages are identical either way; only the
|
||||
/// class varies.
|
||||
fn reproduce_family_with_class(
|
||||
reg: &Registry,
|
||||
id: &str,
|
||||
data: &DataSource,
|
||||
env: &Env,
|
||||
mismatch_class: i32,
|
||||
) -> Result<ReproduceReport, RunnerError> {
|
||||
let family = load_family(reg, id)?;
|
||||
let pip = data.pip_size();
|
||||
@@ -83,6 +118,52 @@ pub fn reproduce_family_in(
|
||||
exit_code: 1,
|
||||
message: "family member has no topology_hash; not a generated run".to_string(),
|
||||
})?;
|
||||
// Identity/geometry guard (#299): a source whose identity or pip
|
||||
// contradicts the member manifests makes divergence mechanical. The
|
||||
// class is context-borne (Fork 6, see this fn's doc): caller error on
|
||||
// the explicit-source seam, data drift on the derived seam.
|
||||
if let Some(recorded) = stored.manifest.instrument.as_deref() {
|
||||
match data {
|
||||
DataSource::Synthetic => {
|
||||
return Err(RunnerError {
|
||||
exit_code: mismatch_class,
|
||||
message: format!(
|
||||
"reproduce source mismatch: family member was recorded over instrument '{recorded}', the source is synthetic"
|
||||
),
|
||||
});
|
||||
}
|
||||
DataSource::Real { symbol, .. } if symbol != recorded => {
|
||||
return Err(RunnerError {
|
||||
exit_code: mismatch_class,
|
||||
message: format!(
|
||||
"reproduce source mismatch: family member was recorded over instrument '{recorded}', the source names '{symbol}'"
|
||||
),
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
// pip has no manifest field of its own: forward-build the label the
|
||||
// FAMILY MINT stamps for this pip (the r_sma/risk-executor variant —
|
||||
// `run_blueprint_member` unconditionally stamps that label) and
|
||||
// compare against the recorded one. The plain-arm branch below
|
||||
// tolerates a stored record shaped like a single-run stamp (legacy or
|
||||
// otherwise), not a variant any live re-run of THIS family would
|
||||
// itself produce.
|
||||
let expected = if stored.manifest.broker.contains("+risk-executor") {
|
||||
crate::member::r_sma_broker_label(pip)
|
||||
} else {
|
||||
crate::member::sim_optimal_broker_label(pip)
|
||||
};
|
||||
if expected != stored.manifest.broker {
|
||||
return Err(RunnerError {
|
||||
exit_code: mismatch_class,
|
||||
message: format!(
|
||||
"reproduce broker/pip mismatch: manifest recorded '{}', the source's pip renders '{expected}'",
|
||||
stored.manifest.broker
|
||||
),
|
||||
});
|
||||
}
|
||||
let doc = reg
|
||||
.get_blueprint(&hash)
|
||||
.map_err(|e| RunnerError { exit_code: 1, message: format!("{e}") })?
|
||||
@@ -253,13 +334,17 @@ pub fn reproduce_family_in(
|
||||
/// 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.
|
||||
///
|
||||
/// Prints its own per-member + summary lines to stdout (unchanged from the
|
||||
/// pre-#295 shell function — this is the report's normal output, not an error
|
||||
/// path); only the final "not every member reproduced" outcome is a refusal,
|
||||
/// returned with an EMPTY message (there is no accompanying stderr line today,
|
||||
/// so the shell dispatch arm must not synthesize one either — see
|
||||
/// `dispatch_reproduce`).
|
||||
pub fn reproduce_family(id: &str, env: &Env) -> Result<(), RunnerError> {
|
||||
/// Returns the report as a value (#299): divergence is data the caller inspects
|
||||
/// (`ReproduceReport::all_identical`), not a process exit — this library never
|
||||
/// prints and never exits. The CLI's `dispatch_reproduce` renders the report and
|
||||
/// owns the exit; a library embedder (a World program) reads `report.outcomes`
|
||||
/// directly.
|
||||
///
|
||||
/// This is the derived-source seam (Fork 6, #299): `data` is reconstructed FROM
|
||||
/// the family's own manifest, not caller-given, so the identity/pip guard inside
|
||||
/// the shared loop refuses class 1 here (data drift within the stored family),
|
||||
/// not class 2 (`reproduce_family_in`'s explicit-source class — see its doc).
|
||||
pub fn reproduce_family(id: &str, env: &Env) -> Result<ReproduceReport, RunnerError> {
|
||||
let reg = env.registry();
|
||||
let family = load_family(®, id)?;
|
||||
// Reconstruct the DataSource the family was minted over: `None` instrument
|
||||
@@ -272,18 +357,7 @@ pub fn reproduce_family(id: &str, env: &Env) -> Result<(), RunnerError> {
|
||||
DataSource::from_choice(DataChoice::Real { symbol, from_ms: None, to_ms: None }, env)?
|
||||
}
|
||||
};
|
||||
let rep = reproduce_family_in(®, id, &data, env)?;
|
||||
let total = rep.outcomes.len();
|
||||
let ok = rep.outcomes.iter().filter(|(_, b)| *b).count();
|
||||
for (label, identical) in &rep.outcomes {
|
||||
let verdict = if *identical { "bit-identical" } else { "DIVERGED" };
|
||||
println!("{id} member {label} reproduced: {verdict}");
|
||||
}
|
||||
println!("reproduced {ok}/{total} members bit-identically");
|
||||
if ok != total {
|
||||
return Err(RunnerError { exit_code: 1, message: String::new() });
|
||||
}
|
||||
Ok(())
|
||||
reproduce_family_with_class(®, id, &data, env, 1)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -293,7 +367,9 @@ mod tests {
|
||||
use aura_engine::RunManifest;
|
||||
use aura_registry::{FamilyKind, Registry};
|
||||
|
||||
use super::load_family;
|
||||
use super::{load_family, reproduce_family_in, ReproduceReport};
|
||||
use crate::family::DataSource;
|
||||
use crate::project::Env;
|
||||
|
||||
/// A registry over a fresh per-test directory (the #258 tag-keyed pattern:
|
||||
/// fixed name under the build-tree tmp anchor, pre-create wipe).
|
||||
@@ -324,6 +400,116 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// A persisted member carrying a `topology_hash` (#299 guard tests): the
|
||||
/// identity/pip guards fire BEFORE the blueprint fetch, so a seeded record
|
||||
/// needs a `topology_hash` to clear the earlier "not a generated run"
|
||||
/// check, but the hash itself never resolves against the store.
|
||||
fn stamped_report(instrument: Option<&str>, broker: &str) -> RunReport {
|
||||
RunReport {
|
||||
manifest: RunManifest {
|
||||
topology_hash: Some("deadbeef".to_string()),
|
||||
instrument: instrument.map(str::to_string),
|
||||
broker: broker.to_string(),
|
||||
..minimal_report().manifest
|
||||
},
|
||||
..minimal_report()
|
||||
}
|
||||
}
|
||||
|
||||
/// The instrument guard (#299): a family member recorded over a real
|
||||
/// instrument refuses class 2 — before any blueprint fetch — when the
|
||||
/// caller-given source is `Synthetic`, naming the recorded instrument in
|
||||
/// the message.
|
||||
#[test]
|
||||
fn reproduce_family_in_refuses_synthetic_source_for_a_real_instrument_member() {
|
||||
let reg = temp_registry("guard-synthetic-for-real");
|
||||
reg.append_family(
|
||||
"f",
|
||||
FamilyKind::Sweep,
|
||||
&[stamped_report(Some("GER40"), "sim-optimal(pip_size=0.0001)")],
|
||||
)
|
||||
.expect("append family");
|
||||
|
||||
let err = reproduce_family_in(®, "f", &DataSource::Synthetic, &Env::std())
|
||||
.expect_err("a synthetic source must not silently reproduce a real-instrument member");
|
||||
assert_eq!(err.exit_code, 2);
|
||||
assert!(err.message.contains("GER40"), "message: {}", err.message);
|
||||
assert!(err.message.contains("synthetic"), "message: {}", err.message);
|
||||
}
|
||||
|
||||
/// The instrument guard's Real-vs-Real twin: a caller-given `Real` source
|
||||
/// naming a DIFFERENT symbol than the member's recorded instrument refuses
|
||||
/// class 2, naming both sides.
|
||||
#[test]
|
||||
fn reproduce_family_in_refuses_a_real_source_naming_the_wrong_symbol() {
|
||||
let reg = temp_registry("guard-wrong-symbol");
|
||||
reg.append_family(
|
||||
"f",
|
||||
FamilyKind::Sweep,
|
||||
&[stamped_report(Some("GER40"), "sim-optimal(pip_size=0.0001)")],
|
||||
)
|
||||
.expect("append family");
|
||||
|
||||
// A hand-built `DataSource::Real` fixture: `DataServer::new` merely
|
||||
// scans its base path (a no-op on a missing directory), so the guard
|
||||
// — which never touches the server — is exercised without any real
|
||||
// archive on disk.
|
||||
let server = std::sync::Arc::new(data_server::DataServer::new("/nonexistent-aura-test-path"));
|
||||
let data = DataSource::Real {
|
||||
server,
|
||||
symbol: "US500".to_string(),
|
||||
from_ms: None,
|
||||
to_ms: None,
|
||||
pip: 1.0,
|
||||
};
|
||||
|
||||
let err = reproduce_family_in(®, "f", &data, &Env::std())
|
||||
.expect_err("a real source naming the wrong symbol must not silently reproduce");
|
||||
assert_eq!(err.exit_code, 2);
|
||||
assert!(err.message.contains("GER40"), "message: {}", err.message);
|
||||
assert!(err.message.contains("US500"), "message: {}", err.message);
|
||||
}
|
||||
|
||||
/// The broker/pip guard (#299): the recorded manifest carries no pip
|
||||
/// field of its own — the pip survives only inside the broker label — so
|
||||
/// the guard forward-builds the label the family mint stamps for this pip
|
||||
/// (tolerating a plain-shape stored record generically, not asserting a
|
||||
/// live re-run would itself produce it — #299) and refuses
|
||||
/// class 2 on a label mismatch, naming both labels.
|
||||
#[test]
|
||||
fn reproduce_family_in_refuses_a_pip_mismatch_via_the_broker_label() {
|
||||
let reg = temp_registry("guard-pip-mismatch");
|
||||
// Recorded under a pip the actual synthetic run (0.0001, `SYNTHETIC_PIP_SIZE`)
|
||||
// does not match: the label the re-run would forward-build diverges from
|
||||
// the stored one, so the guard must fire before any re-run.
|
||||
reg.append_family(
|
||||
"f",
|
||||
FamilyKind::Sweep,
|
||||
&[stamped_report(None, "sim-optimal(pip_size=0.0002)")],
|
||||
)
|
||||
.expect("append family");
|
||||
|
||||
let err = reproduce_family_in(®, "f", &DataSource::Synthetic, &Env::std())
|
||||
.expect_err("a pip mismatch forward-built via the broker label must refuse");
|
||||
assert_eq!(err.exit_code, 2);
|
||||
assert!(err.message.contains("sim-optimal(pip_size=0.0002)"), "message: {}", err.message);
|
||||
assert!(err.message.contains("sim-optimal(pip_size=0.0001)"), "message: {}", err.message);
|
||||
}
|
||||
|
||||
/// `all_identical` is true iff every outcome reproduced bit-identically.
|
||||
#[test]
|
||||
fn all_identical_reflects_every_outcome() {
|
||||
let all_ok = ReproduceReport {
|
||||
outcomes: vec![("a".to_string(), true), ("b".to_string(), true)],
|
||||
};
|
||||
assert!(all_ok.all_identical());
|
||||
|
||||
let one_diverged = ReproduceReport {
|
||||
outcomes: vec![("a".to_string(), true), ("b".to_string(), false)],
|
||||
};
|
||||
assert!(!one_diverged.all_identical());
|
||||
}
|
||||
|
||||
/// One id vocabulary across enumeration and reproduction (C18, #298): the
|
||||
/// family-identity string a consumer lifts off the registry's own member
|
||||
/// enumeration (`FamilyRunRecord.family`) resolves through the reproduce
|
||||
|
||||
@@ -857,7 +857,12 @@ error; only its environment refusals (a tap-trace store write failure)
|
||||
exit 1. The no-data family (no local data, no data in the requested window,
|
||||
no recorded geometry) belongs to the real-data verbs — `aura run`,
|
||||
`reproduce`, the campaign legs — where it likewise exits 1 (#297; the full
|
||||
partition lives in [C14](design/contracts/c14-headless-two-faces.md)).
|
||||
partition lives in [C14](design/contracts/c14-headless-two-faces.md)). `aura
|
||||
reproduce`'s identity/pip guard (a stored member's recorded instrument or
|
||||
broker/pip label contradicting the source it re-derives against) is
|
||||
likewise class 1 on the plain verb — its source is derived from the
|
||||
family's own manifest, so a mismatch there is data drift, not a caller
|
||||
error (Fork 6, #299).
|
||||
|
||||
The class boundary is FORM vs VALUE, not "environment/data/IO vs everything
|
||||
else": an `--override` value that is well-formed but out of the node's own
|
||||
|
||||
Reference in New Issue
Block a user