feat(0094): content-addressed reproduction — aura reproduce <family-id> (iter 1)

Restore C18 reproducibility for generated (blueprint-sweep) runs (#158, acc 2):
a sweep's topology is stored once, content-addressed by the topology_hash the
manifest already carries, and `aura reproduce <family-id>` re-derives every
persisted member bit-identically (C1) from the stored blueprint.

- aura-registry: a dumb content-addressed bytes store — Registry::put_blueprint
  / get_blueprint / blueprints_dir over runs/blueprints/<hash>.json. No sha2 dep
  (caller owns the hash; reproduction's bit-identical compare is the integrity
  check); absent id -> Ok(None) (treat-as-empty, like load).
- aura-cli: run_blueprint_member extracted from blueprint_sweep_family's member
  closure (behaviour-preserving; keystone stays green) so the sweep AND reproduce
  re-run the identical reduce-mode path — bit-identity by construction. The store
  write hooks the sweep persist seam (one blueprint per family, shared topo).
  reproduce_family_in loads each member's blueprint by hash, reconstructs the
  point from the recorded params (point_from_params, inverse of zip_params, over
  the WRAPPED signal's param_space so the prefixed knob names match), re-runs, and
  compares metrics. `["reproduce", id]` dispatch arm; refuse-don't-guess on an
  unknown id / missing stored blueprint (exit 2), DIVERGED -> exit 1.

Dependency decision (per-case review, ratified): enabled serde_json
`float_roundtrip`. Stored member metrics round-trip through families.jsonl as
f64 JSON; the default parser can be 1 ULP off, which would make disk-loaded !=
recomputed and "bit-identical" (C1) physically impossible. The feature guarantees
exact f64 round-trip. Blast-radius clean (full workspace suite green); it serves
determinism, the invariant reproduction rests on. Canary:
f64_blueprint_param_survives_store_round_trip_bit_identically.

Two plan bugs caught + fixed by the implement loop and verified: reproduce's
param_space must come from the wrapped signal (prefixed names, the #167 lockstep),
and the family id is 0-indexed (live-0 / smacross-0, not -1).

Verified: full workspace suite green (51 suites); clippy -D warnings clean; and
the binary end-to-end — `aura sweep` (3 members, one shared stored blueprint) then
`aura reproduce live-0` (3/3 bit-identical incl the open-at-end member), unknown
id -> exit 2. acc 1 (graph introspect --content-id) + acc 3 (Tier-1 id stability)
land in iteration 2 (plan 0094b).

refs #158
This commit is contained in:
2026-07-01 02:55:52 +02:00
parent a6d3774d04
commit 008692c043
4 changed files with 521 additions and 33 deletions
+7 -1
View File
@@ -27,4 +27,10 @@ publish = false
[workspace.dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
# float_roundtrip is load-bearing for bit-identical reproduction (`aura reproduce`,
# #158): an f64 blueprint param (e.g. a Bias `scale`) is round-tripped through the
# content-addressed store as JSON, and the DEFAULT serde_json parser can be 1 ULP off,
# which would re-derive a member as DIVERGED. This feature guarantees the exact f64 is
# parsed back. Exercised (with a 1-ULP canary value) by aura-cli's
# `f64_blueprint_param_survives_store_round_trip_bit_identically`.
serde_json = { version = "1", features = ["float_roundtrip"] }
+264 -32
View File
@@ -2469,6 +2469,128 @@ fn runs_family(id: &str, rank: Option<&str>) {
}
}
/// The outcome of reproducing one persisted family: per member, whether its re-run
/// metrics are bit-identical to the stored metrics (C1).
struct ReproduceReport {
outcomes: Vec<(String, bool)>,
}
/// Reconstruct a member's bootstrap point from its recorded named params — the inverse
/// of `zip_params(space, point)`. Walks the reloaded signal's `param_space` in order
/// (deterministic for the same blueprint) and reads each knob's value from the manifest.
fn point_from_params(space: &[ParamSpec], params: &[(String, Scalar)]) -> Vec<Cell> {
space
.iter()
.map(|ps| {
params
.iter()
.find(|(n, _)| n == &ps.name)
.map(|(_, s)| s.cell())
.unwrap_or_else(|| {
// A manifest missing a param the reloaded space expects is
// corrupted-on-disk data — exit cleanly (`aura:` + exit 2) like
// every other persisted-data failure on the reproduce path, not
// a panic.
eprintln!("aura: manifest is missing param {}", ps.name);
std::process::exit(2);
})
})
.collect()
}
/// 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(reg: &Registry, id: &str, data: &DataSource) -> ReproduceReport {
let members = reg.load_family_members().unwrap_or_else(|e| {
eprintln!("aura: {e}");
std::process::exit(2);
});
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(2);
};
let pip = data.pip_size();
let window = data.full_window();
let mut outcomes = Vec::new();
for member in &family.members {
let stored = &member.report;
let hash = stored.manifest.topology_hash.clone().unwrap_or_else(|| {
eprintln!("aura: family member has no topology_hash; not a generated run");
std::process::exit(2);
});
let doc = reg
.get_blueprint(&hash)
.unwrap_or_else(|e| {
eprintln!("aura: {e}");
std::process::exit(2);
})
.unwrap_or_else(|| {
eprintln!("aura: blueprint {hash} missing from store");
std::process::exit(2);
});
// Reload the stored blueprint per use: a Composite is !Clone, and both the
// param-space probe (below) and the re-run each consume one. The doc was
// canonical-serialized at store time, so every reload is infallible.
let reload = || {
blueprint_from_json(&doc, &|t| std_vocabulary(t)).unwrap_or_else(|e| {
eprintln!("aura: stored blueprint {hash} does not parse: {e:?}");
std::process::exit(2);
})
};
// The param_space of the WRAPPED signal — its knobs carry the `stage1_r`
// wrapper's `stage1_signal.` node-path prefix, exactly the names the manifest
// recorded at write time. Mirrors `blueprint_sweep_family`'s probe so the
// reproduce-side space name-matches the stored params (raw `signal.param_space()`
// would drop the prefix and `point_from_params` could not find the knobs).
let (tx_eq, _) = mpsc::channel();
let (tx_ex, _) = mpsc::channel();
let (tx_r, _) = mpsc::channel();
let (tx_req, _) = mpsc::channel();
let space =
wrap_stage1r(reload(), tx_eq, tx_ex, tx_r, tx_req, false, true, None).param_space();
let point = point_from_params(&space, &stored.manifest.params);
let label = stored
.manifest
.params
.iter()
.map(|(n, v)| format!("{n}={}", render_value(v)))
.collect::<Vec<_>>()
.join(", ");
let rerun = run_blueprint_member(
reload(),
&point,
&space,
data.run_sources(),
window,
stored.manifest.seed,
pip,
&hash,
);
outcomes.push((label, rerun.metrics == stored.metrics));
}
ReproduceReport { outcomes }
}
/// `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).
fn reproduce_family(id: &str) {
let rep = reproduce_family_in(&default_registry(), id, &DataSource::Synthetic);
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 {
std::process::exit(1);
}
}
// --- MACD proof-of-concept (a richer, nested indicator + strategy) -----------
/// The MACD signal as a named composite: price → fast/slow `Ema` → the MACD line
@@ -2970,6 +3092,48 @@ fn run_signal_stage1r(signal: Composite, params: &[Scalar], data: RunData, seed:
RunReport { manifest, metrics }
}
/// Run one bootstrapped member of a loaded-signal sweep: the reduce-mode path
/// (`SeriesReducer` folds eq/ex, `GatedRecorder` retains the gated R rows — the
/// O(cycles)→O(trades) fold), shared by the live sweep AND reproduction so a
/// reproduced member re-derives bit-identically (C1). `signal` is a freshly
/// reloaded blueprint (`Composite` is `!Clone`); `point` is the member's bound
/// cells; `space` gives the by-name manifest params; `topo` the shared signal hash.
#[allow(clippy::too_many_arguments)]
fn run_blueprint_member(
signal: Composite,
point: &[Cell],
space: &[ParamSpec],
sources: Vec<Box<dyn aura_engine::Source>>,
window: (Timestamp, Timestamp),
seed: u64,
pip: f64,
topo: &str,
) -> RunReport {
let (tx_eq, rx_eq) = mpsc::channel();
let (tx_ex, rx_ex) = mpsc::channel();
let (tx_r, rx_r) = mpsc::channel();
let (tx_req, _rx_req) = mpsc::channel();
let mut h = wrap_stage1r(signal, tx_eq, tx_ex, tx_r, tx_req, false, true, None)
.bootstrap_with_cells(point)
.expect("member bootstraps (point kind-checked against param_space)");
h.run(sources);
let named = zip_params(space, point); // by-name params for the manifest record
let mut manifest = sim_optimal_manifest(named, window, seed, pip);
manifest.broker = stage1_r_broker_label(pip);
manifest.topology_hash = Some(topo.to_string());
let r_rows: Vec<(Timestamp, Vec<Scalar>)> = rx_r.try_iter().collect();
let (total_pips, max_drawdown) = rx_eq
.try_iter()
.next()
.map(|(_, row)| (row[0].as_f64(), row[1].as_f64()))
.unwrap_or((0.0, 0.0));
let bias_sign_flips =
rx_ex.try_iter().next().map(|(_, row)| row[2].as_i64() as u64).unwrap_or(0);
let mut m = RunMetrics { total_pips, max_drawdown, bias_sign_flips, r: None };
m.r = Some(summarize_r(&r_rows, &[]));
RunReport { manifest, metrics: m }
}
/// Sweep a serialized signal `doc` over user-named param-space axes — the structural
/// twin of [`stage1_r_sweep_family`], with three deviations. (1) The signal source is
/// `wrap_stage1r(blueprint_from_json(doc))` — a loaded blueprint, not the Rust-built
@@ -3015,33 +3179,9 @@ fn blueprint_sweep_family(
binder = binder.axis(n, vals.clone());
}
binder.sweep(|point| {
// fresh per-member graph (deviation 2: reload + fresh channels) bootstrapped
// with the point's cells.
let (tx_eq, rx_eq) = mpsc::channel();
let (tx_ex, rx_ex) = mpsc::channel();
let (tx_r, rx_r) = mpsc::channel();
let (tx_req, _rx_req) = mpsc::channel();
let mut h = wrap_stage1r(reload(doc), tx_eq, tx_ex, tx_r, tx_req, false, true, None)
.bootstrap_with_cells(point)
.expect("member bootstraps (point kind-checked against param_space by resolve_axes)");
h.run(data.run_sources());
let named = zip_params(&space, point); // by-name params for the manifest record
let mut manifest = sim_optimal_manifest(named, window, 0, pip);
manifest.broker = stage1_r_broker_label(pip);
manifest.topology_hash = Some(topo.clone()); // deviation 3: the loaded signal's hash
// reduce-mode fold, EXACTLY as stage1_r_sweep_family's no-trace arm: SeriesReducer
// folds eq/ex to one summary row, GatedRecorder retains the gated R rows.
let r_rows: Vec<(Timestamp, Vec<Scalar>)> = rx_r.try_iter().collect();
let (total_pips, max_drawdown) = rx_eq
.try_iter()
.next()
.map(|(_, row)| (row[0].as_f64(), row[1].as_f64()))
.unwrap_or((0.0, 0.0));
let bias_sign_flips =
rx_ex.try_iter().next().map(|(_, row)| row[2].as_i64() as u64).unwrap_or(0);
let mut m = RunMetrics { total_pips, max_drawdown, bias_sign_flips, r: None };
m.r = Some(summarize_r(&r_rows, &[]));
RunReport { manifest, metrics: m }
// fresh per-member graph (Composite is !Clone, reload per member) run through
// the shared reduce-mode member path — the same fn reproduction re-runs.
run_blueprint_member(reload(doc), point, &space, data.run_sources(), window, 0, pip, &topo)
})
}
@@ -3066,10 +3206,27 @@ fn run_blueprint_sweep(doc: &str, axes: &[(String, Vec<Scalar>)], name: &str, pe
eprintln!("aura: {e:?}");
std::process::exit(2);
});
// Record the family unconditionally (C18/C21 lineage), exactly like `run_sweep`, and
// keep the assigned id so every printed member line carries it — the stdout is then
// linkable to the stored `FamilyKind::Sweep` family.
let id = match default_registry().append_family(name, FamilyKind::Sweep, &sweep_member_reports(&family)) {
let reg = default_registry();
// Store the canonical blueprint ONCE, keyed by the family's shared topology_hash —
// exactly the bytes whose SHA256 the members carry (#164 byte-canonical, round-trip
// idempotent). One stored topology per family (C18/C11/C12).
let topo = family.points[0]
.report
.manifest
.topology_hash
.clone()
.expect("a blueprint sweep stamps every member's topology_hash");
let canonical = blueprint_to_json(
&blueprint_from_json(doc, &|t| std_vocabulary(t))
.expect("doc parse-validated at the dispatch boundary"),
)
.expect("a loaded blueprint re-serializes");
reg.put_blueprint(&topo, &canonical).unwrap_or_else(|e| {
eprintln!("aura: {e}");
std::process::exit(2);
});
// Record the family unconditionally (C18/C21 lineage), exactly like `run_sweep`.
let id = match reg.append_family(name, FamilyKind::Sweep, &sweep_member_reports(&family)) {
Ok(id) => id,
Err(e) => {
eprintln!("aura: {e}");
@@ -3710,7 +3867,7 @@ const COST_FLAGS_NOTE: &str = "\ncost flags (stage1-r only; all >= 0, charged in
--carry-per-cycle = carry in price units per ENGINE cycle (not per day, not an overnight swap)";
const USAGE: &str =
"usage: aura run [--harness <sma|macd|stage1-r>] [--real <SYMBOL> [--from <ms>] [--to <ms>]] [--trace <name>] [--cost-per-trade <f64>] [--slip-vol-mult <f64>] [--carry-per-cycle <f64>] | aura chart <name> [--tap <t>] [--panels] | aura graph | aura sweep [--strategy <sma|momentum|stage1-r|stage1-breakout|stage1-meanrev>] [--real <SYMBOL> [--from <ms>] [--to <ms>]] [--name <n>|--trace <n>] | aura mc [--name <n>|--trace <n>] | aura mc --strategy stage1-r [--real <SYMBOL> [--from <ms>] [--to <ms>]] [--fast <csv>] [--slow <csv>] [--stop-length <csv>] [--stop-k <csv>] [--block-len <n>] [--resamples <n>] [--seed <n>] | aura walkforward [--strategy <sma|momentum|stage1-r|stage1-breakout|stage1-meanrev>] [--real <SYMBOL> [--from <ms>] [--to <ms>]] [--name <n>|--trace <n>] [--fast <csv>] [--slow <csv>] [--stop-length <csv>] [--stop-k <csv>] [--select <argmax|plateau:mean|plateau:worst>] | aura generalize [--strategy stage1-r] --real <SYM1,SYM2,...> --fast <n> --slow <n> --stop-length <n> --stop-k <f> [--from <ms>] [--to <ms>] [--metric <r-metric>] [--name <n>] | aura runs families | aura runs family <id> [rank <metric>]";
"usage: aura run [--harness <sma|macd|stage1-r>] [--real <SYMBOL> [--from <ms>] [--to <ms>]] [--trace <name>] [--cost-per-trade <f64>] [--slip-vol-mult <f64>] [--carry-per-cycle <f64>] | aura chart <name> [--tap <t>] [--panels] | aura graph | aura sweep [--strategy <sma|momentum|stage1-r|stage1-breakout|stage1-meanrev>] [--real <SYMBOL> [--from <ms>] [--to <ms>]] [--name <n>|--trace <n>] | aura mc [--name <n>|--trace <n>] | aura mc --strategy stage1-r [--real <SYMBOL> [--from <ms>] [--to <ms>]] [--fast <csv>] [--slow <csv>] [--stop-length <csv>] [--stop-k <csv>] [--block-len <n>] [--resamples <n>] [--seed <n>] | aura walkforward [--strategy <sma|momentum|stage1-r|stage1-breakout|stage1-meanrev>] [--real <SYMBOL> [--from <ms>] [--to <ms>]] [--name <n>|--trace <n>] [--fast <csv>] [--slow <csv>] [--stop-length <csv>] [--stop-k <csv>] [--select <argmax|plateau:mean|plateau:worst>] | aura generalize [--strategy stage1-r] --real <SYM1,SYM2,...> --fast <n> --slow <n> --stop-length <n> --stop-k <f> [--from <ms>] [--to <ms>] [--metric <r-metric>] [--name <n>] | aura runs families | aura runs family <id> [rank <metric>] | aura reproduce <family-id>";
fn main() {
// Restore the default SIGPIPE disposition. Rust's runtime sets SIGPIPE to SIG_IGN
@@ -3852,6 +4009,7 @@ fn main() {
["runs", "families"] => runs_families(),
["runs", "family", id] => runs_family(id, None),
["runs", "family", id, "rank", metric] => runs_family(id, Some(metric)),
["reproduce", id] => reproduce_family(id),
_ => {
eprintln!("aura: {USAGE}");
std::process::exit(2);
@@ -5025,6 +5183,80 @@ mod tests {
assert_ne!(k4, k6, "distinct grid points key distinctly");
}
#[test]
fn reproduce_family_re_derives_every_member_bit_identically() {
// a unique temp runs store so the on-disk family + blueprint store do not collide.
let dir = std::env::temp_dir().join(format!("aura-repro-{}", std::process::id()));
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 open = stage1_signal(None, None);
let doc = blueprint_to_json(&open).expect("serializes");
let data = DataSource::Synthetic;
// 2x grid over slow.length {4,6} at fast=2 — slow=4 is the open-at-end member.
let axes = vec![
("stage1_signal.fast.length".to_string(), vec![Scalar::i64(2)]),
("stage1_signal.slow.length".to_string(), vec![Scalar::i64(4), Scalar::i64(6)]),
];
let family = blueprint_sweep_family(&doc, &axes, &data).expect("axes resolve");
// persist exactly as run_blueprint_sweep does: store the blueprint, append the family.
let topo = family.points[0].report.manifest.topology_hash.clone().expect("topo");
let canonical =
blueprint_to_json(&blueprint_from_json(&doc, &|t| std_vocabulary(t)).unwrap()).unwrap();
reg.put_blueprint(&topo, &canonical).expect("store blueprint");
let id = reg
.append_family("repro", FamilyKind::Sweep, &sweep_member_reports(&family))
.expect("append");
// reproduce: every member re-derives bit-identically (incl the open-at-end member).
let rep = reproduce_family_in(&reg, &id, &data);
assert_eq!(rep.outcomes.len(), 2, "two members reproduced");
assert!(
rep.outcomes.iter().all(|(_, ok)| *ok),
"every member re-derives bit-identically: {:?}",
rep.outcomes
);
let _ = std::fs::remove_dir_all(&dir);
}
/// Property: an f64 blueprint param survives the content-addressed store's
/// serialize -> parse -> re-serialize round-trip **bit-identically**, so `aura
/// reproduce` re-derives an f64-bearing member without DIVERGED. This is exactly
/// what workspace `serde_json/float_roundtrip` buys: the constant below is a
/// full-precision f64 (`0.12387080150408619`) that the DEFAULT serde_json parser
/// mis-parses by 1 ULP — with `float_roundtrip` on it parses back exactly, so the
/// canonical bytes are stable and the re-run reproduces. The i64-axis reproduce
/// tests exercise a `scale=0.5` blueprint (exactly representable), so this is the
/// only test that actually depends on the feature.
#[test]
fn f64_blueprint_param_survives_store_round_trip_bit_identically() {
// 1-ULP canary for serde_json float_roundtrip: parses back off-by-one without it.
const HARD_SCALE: f64 = 0.12387080150408619;
// A one-node signal whose only knob is the non-short-decimal f64 scale.
let mut g = GraphBuilder::new("scale_probe");
let bias = g.add(Bias::builder().named("bias").bind("scale", Scalar::f64(HARD_SCALE)));
let signal = g.source_role("signal", ScalarKind::F64);
g.feed(signal, vec![bias.input("signal")]);
g.expose(bias.output("bias"), "bias");
let sig = g.build().expect("one-node bias signal wiring resolves");
// the store keeps canonical bytes verbatim (dumb bytes-by-key), so the f64
// fidelity lives entirely in this serialize -> parse -> re-serialize hop.
let doc = blueprint_to_json(&sig).expect("serializes");
assert!(
doc.contains("0.12387080150408619"),
"canonical JSON carries the full-precision f64: {doc}"
);
let reloaded = blueprint_from_json(&doc, &|t| std_vocabulary(t)).expect("loads");
let doc2 = blueprint_to_json(&reloaded).expect("re-serializes");
// Without float_roundtrip the reparsed scale drifts 1 ULP and ryu re-serializes
// it to a different string, so these canonical bytes would differ.
assert_eq!(doc, doc2, "f64 blueprint param survives the store round-trip bit-identically");
}
/// `parse_walkforward_args` defaults to synthetic / name "walkforward" / no
/// persist, admits `--real <SYMBOL>` (window-less here) yielding `DataChoice::Real`,
/// and rejects two name flags or a `--real` missing its symbol.
+202
View File
@@ -3287,6 +3287,68 @@ fn aura_sweep_loads_a_blueprint_and_persists_a_sweep_family() {
let _ = std::fs::remove_dir_all(&cwd);
}
/// Property (#158/#164, C18/C11/C12): a blueprint sweep content-addresses its topology
/// — the canonical blueprint is persisted ONCE under `runs/blueprints/<topology_hash>.json`,
/// keyed by the very SHA256 every member's manifest carries, holding exactly the bytes
/// `blueprint_to_json(blueprint_from_json(doc))` yields. That stored topology is what makes
/// a generated sweep family reproducible from disk (the `aura reproduce` re-derivation).
#[test]
fn aura_sweep_persists_the_canonical_blueprint_keyed_by_topology_hash() {
use aura_engine::{blueprint_from_json, blueprint_to_json};
use aura_std::std_vocabulary;
use sha2::{Digest, Sha256};
let cwd = temp_cwd("blueprint-sweep-store");
let fixture = format!("{}/tests/fixtures/stage1_signal_open.json", env!("CARGO_MANIFEST_DIR"));
let out = Command::new(BIN)
.args([
"sweep", &fixture,
"--axis", "stage1_signal.fast.length=2,4",
"--axis", "stage1_signal.slow.length=8,16",
"--trace", "f",
])
.current_dir(&cwd)
.output()
.expect("spawn aura sweep blueprint");
assert!(out.status.success(), "exit: {:?} stderr={}", out.status, String::from_utf8_lossy(&out.stderr));
// exactly one blueprint stored for the whole family (one topology per family).
let store = cwd.join("runs/blueprints");
let mut entries: Vec<_> = std::fs::read_dir(&store)
.unwrap_or_else(|e| panic!("the sweep must create the blueprint store {store:?}: {e}"))
.map(|e| e.expect("dir entry").path())
.collect();
entries.sort();
assert_eq!(entries.len(), 1, "one stored topology per family: {entries:?}");
// keyed by a 64-hex topology hash, holding the canonical re-serialization.
let stored_path = &entries[0];
let stem = stored_path.file_stem().expect("stem").to_str().expect("utf-8");
assert_eq!(stem.len(), 64, "content id is a 64-hex SHA256: {stem}");
assert!(stem.bytes().all(|b| b.is_ascii_hexdigit()), "content id is hex: {stem}");
let doc = std::fs::read_to_string(&fixture).expect("read fixture");
let expected = blueprint_to_json(&blueprint_from_json(&doc, &|t| std_vocabulary(t)).expect("loads"))
.expect("re-serializes");
let stored = std::fs::read_to_string(stored_path).expect("read stored blueprint");
assert_eq!(stored, expected, "the store holds the canonical blueprint bytes");
// content-addressing, the feature's load-bearing property: the key IS the SHA256 of
// the stored bytes — `get_blueprint(hash)` addresses exactly these bytes by identity,
// not by coincidence of the store and the members computing the hash separately.
let content_id: String =
Sha256::digest(stored.as_bytes()).iter().map(|b| format!("{b:02x}")).collect();
assert_eq!(stem, content_id, "the store key is the SHA256 of its content");
// and that key is exactly what every family member's manifest carries — the hash the
// reproduce path reads back to fetch the topology. All 4 members share the one topology.
let members = std::fs::read_to_string(cwd.join("runs/families.jsonl")).expect("read family store");
let carried = members.matches(&format!("\"topology_hash\":\"{stem}\"")).count();
assert_eq!(carried, 4, "every member records the store key as its topology_hash: {members}");
let _ = std::fs::remove_dir_all(&cwd);
}
/// An `--axis` name that does not resolve against the loaded blueprint's param_space
/// fails clean (a named BindError, non-zero exit), not a panic. The fully-bound
/// fixture has an empty param_space, so any axis name is unknown.
@@ -3304,3 +3366,143 @@ fn aura_sweep_rejects_an_unknown_axis() {
"names the unresolved axis: {stderr}"
);
}
/// Property (#158, C18 "re-derives full results on demand"): `aura reproduce <family-id>`
/// re-derives every member of a persisted sweep family from the content-addressed
/// blueprint store and reports each bit-identical, exiting 0. Drives the verb through the
/// binary — the `reproduce_family` stdout contract (a per-member `bit-identical` line plus
/// a `reproduced N/N` summary) that the `reproduce_family_in` unit seam bypasses.
#[test]
fn aura_reproduce_re_derives_a_persisted_sweep_family() {
let cwd = temp_cwd("blueprint-reproduce");
let fixture = format!("{}/tests/fixtures/stage1_signal_open.json", env!("CARGO_MANIFEST_DIR"));
let sweep = Command::new(BIN)
.args([
"sweep", &fixture,
"--axis", "stage1_signal.fast.length=2,4",
"--axis", "stage1_signal.slow.length=8,16",
"--trace", "f",
])
.current_dir(&cwd)
.output()
.expect("spawn aura sweep blueprint");
assert!(sweep.status.success(), "sweep exit: {:?} stderr={}", sweep.status, String::from_utf8_lossy(&sweep.stderr));
// `--trace f` names the family `f-0`; reproduce it from disk.
let out = Command::new(BIN)
.args(["reproduce", "f-0"])
.current_dir(&cwd)
.output()
.expect("spawn aura reproduce");
assert!(out.status.success(), "reproduce exit: {:?} stderr={}", out.status, String::from_utf8_lossy(&out.stderr));
let stdout = String::from_utf8(out.stdout).expect("utf-8 stdout");
// one verdict line per member (all bit-identical) + the summary; the label renders
// param values portably (`length=2`, not the `I64(2)` Debug form).
assert_eq!(
stdout.lines().filter(|l| l.contains("reproduced: bit-identical")).count(),
4,
"every member re-derives bit-identically: {stdout}"
);
assert!(stdout.contains("stage1_signal.fast.length=2"), "label renders values portably: {stdout}");
assert!(!stdout.contains("I64("), "label must not leak the Scalar Debug form: {stdout}");
assert!(stdout.contains("reproduced 4/4 members bit-identically"), "summary line: {stdout}");
let _ = std::fs::remove_dir_all(&cwd);
}
/// Property (C1/C18 integrity net): when a persisted member's stored metrics no longer
/// match its re-derivation, `aura reproduce` reports that member `DIVERGED` and exits 1
/// — the non-happy branch of the verb. Forced by tampering one member's on-disk
/// `total_pips` after the sweep (the store's bit-identical compare IS the integrity check).
#[test]
fn aura_reproduce_reports_a_diverged_member_and_exits_one() {
let cwd = temp_cwd("blueprint-reproduce-diverge");
let fixture = format!("{}/tests/fixtures/stage1_signal_open.json", env!("CARGO_MANIFEST_DIR"));
let sweep = Command::new(BIN)
.args([
"sweep", &fixture,
"--axis", "stage1_signal.fast.length=2,4",
"--axis", "stage1_signal.slow.length=8,16",
"--trace", "f",
])
.current_dir(&cwd)
.output()
.expect("spawn aura sweep blueprint");
assert!(sweep.status.success(), "sweep exit: {:?} stderr={}", sweep.status, String::from_utf8_lossy(&sweep.stderr));
// corrupt the first member's stored `total_pips` so its re-run metrics differ.
let store = cwd.join("runs/families.jsonl");
let s = std::fs::read_to_string(&store).expect("read family store");
let key = "\"total_pips\":";
let at = s.find(key).expect("a member records total_pips") + key.len();
let end = at + s[at..].find(',').expect("total_pips value terminates");
let corrupted = format!("{}999.0{}", &s[..at], &s[end..]);
std::fs::write(&store, corrupted).expect("write corrupted store");
let out = Command::new(BIN)
.args(["reproduce", "f-0"])
.current_dir(&cwd)
.output()
.expect("spawn aura reproduce");
assert_eq!(out.status.code(), Some(1), "a diverged member exits 1: stderr={}", String::from_utf8_lossy(&out.stderr));
let stdout = String::from_utf8(out.stdout).expect("utf-8 stdout");
assert!(stdout.contains("reproduced: DIVERGED"), "the tampered member is reported DIVERGED: {stdout}");
assert!(stdout.contains("reproduced 3/4 members bit-identically"), "summary counts the diverged member: {stdout}");
let _ = std::fs::remove_dir_all(&cwd);
}
/// E2E (acc 2): persist a blueprint sweep, then `aura reproduce <id>` re-derives every
/// member bit-identically from the content-addressed store and exits 0.
#[test]
fn aura_reproduce_re_derives_a_persisted_sweep_bit_identically() {
let cwd = temp_cwd("reproduce_roundtrip");
let fixture = format!("{}/tests/fixtures/stage1_signal_open.json", env!("CARGO_MANIFEST_DIR"));
let sweep = std::process::Command::new(BIN)
.args([
"sweep",
&fixture,
"--axis",
"stage1_signal.fast.length=2",
"--axis",
"stage1_signal.slow.length=4,6",
"--name",
"smacross",
])
.current_dir(&cwd)
.output()
.expect("run aura sweep");
assert!(sweep.status.success(), "sweep ok: {}", String::from_utf8_lossy(&sweep.stderr));
let repro = std::process::Command::new(BIN)
.args(["reproduce", "smacross-0"])
.current_dir(&cwd)
.output()
.expect("run aura reproduce");
let stdout = String::from_utf8_lossy(&repro.stdout);
assert!(repro.status.success(), "reproduce exits 0: {}", String::from_utf8_lossy(&repro.stderr));
assert!(
stdout.contains("reproduced 2/2 members bit-identically"),
"every member re-derives: {stdout}"
);
let _ = std::fs::remove_dir_all(&cwd);
}
/// E2E (negative): `aura reproduce <unknown>` is a hard error (exit non-zero + named
/// cause on stderr), distinct from `runs family <id>`'s treat-as-empty exit 0.
#[test]
fn aura_reproduce_rejects_an_unknown_family() {
let cwd = temp_cwd("reproduce_unknown");
let out = std::process::Command::new(BIN)
.args(["reproduce", "no-such-family-1"])
.current_dir(&cwd)
.output()
.expect("run aura reproduce");
assert!(!out.status.success(), "unknown family exits non-zero");
assert!(
String::from_utf8_lossy(&out.stderr).contains("no such family 'no-such-family-1'"),
"names the cause: {}",
String::from_utf8_lossy(&out.stderr)
);
let _ = std::fs::remove_dir_all(&cwd);
}
+48
View File
@@ -94,6 +94,43 @@ impl Registry {
}
Ok(reports)
}
/// The content-addressed blueprint store dir — a sibling of the runs store,
/// `<runs.jsonl>.with_file_name("blueprints")`. A topology is stored once,
/// keyed by its content id (`topology_hash`), so a whole sweep family's
/// members share one stored blueprint (C18 tiny manifest; C11/C12 dedup).
fn blueprints_dir(&self) -> PathBuf {
self.path.with_file_name("blueprints")
}
/// The single content-id→path mapping `put_blueprint` and `get_blueprint` both
/// route through, so the store can never write one path and read another (a
/// drifted key would silently break round-trip — `get` returns `None` and
/// reproduction cannot re-derive the member, rather than erroring).
fn blueprint_path(&self, hash: &str) -> PathBuf {
self.blueprints_dir().join(format!("{hash}.json"))
}
/// Write-once content-addressed put: `blueprints/<hash>.json` = `canonical_json`.
/// Idempotent — the same content id always addresses identical canonical bytes,
/// so a repeated write re-writes identical content. The registry does NOT
/// verify `sha256(bytes) == hash` (no `sha2` dep here): the caller owns the
/// hash, and reproduction's bit-identical metric compare is the integrity check.
pub fn put_blueprint(&self, hash: &str, canonical_json: &str) -> Result<(), RegistryError> {
fs::create_dir_all(self.blueprints_dir())?;
fs::write(self.blueprint_path(hash), canonical_json)?;
Ok(())
}
/// Read a stored blueprint by content id; `Ok(None)` if absent — the same
/// treat-as-empty discipline `load` applies to a missing runs store.
pub fn get_blueprint(&self, hash: &str) -> Result<Option<String>, RegistryError> {
match fs::read_to_string(self.blueprint_path(hash)) {
Ok(s) => Ok(Some(s)),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(e) => Err(RegistryError::Io(e)),
}
}
}
/// Which metric a best-first comparison keys on. Resolves a metric *name* (a
@@ -662,6 +699,17 @@ mod tests {
assert_eq!(reg.load().expect("load missing"), Vec::<RunReport>::new());
}
#[test]
fn blueprint_store_round_trips_by_content_id() {
let reg = Registry::open(temp_family_dir("blueprint_store_round_trip"));
let canonical = r#"{"format_version":1,"input_roles":[],"nodes":[],"edges":[]}"#;
reg.put_blueprint("deadbeef", canonical).expect("put");
// exact bytes come back, keyed by content id
assert_eq!(reg.get_blueprint("deadbeef").expect("get"), Some(canonical.to_string()));
// an absent id is Ok(None), not an error (treat-as-empty discipline)
assert_eq!(reg.get_blueprint("never-written").expect("get"), None);
}
#[test]
fn corrupt_line_is_a_parse_error_with_line_number() {
let path = temp_path("corrupt");