refactor(aura-runner, aura-cli): retire the family builders and the CLI topology_hash duplicate

Slice 7 of the #319 retirement. aura-runner's blueprint_sweep_family /
blueprint_walkforward_family / blueprint_mc_family lost their only
production callers with the quintet and are gone (family.rs 889 -> 199
lines) together with their zero-caller helpers; reproduce's live inputs
(DataSource, showcase/walkforward sources) stay. Coverage was ported
first: member-computation identity + family-shared topology_hash now pin
through a campaign-document twin in tests/exec.rs, reproduce's multi-member
and MC-seed-label branches re-mint their fixtures directly. The CLI-side
topology_hash helper goes with them — its keep-rationale was disproven
(the record line's hash is computed in the runner's member machinery;
minuted on the issue) — and c28_layering's file list drops the stale
verb_sugar entry. The synthetic per-seed MC family remains an intentional
retirement casualty, recorded in the test dispositions.

refs #319
This commit is contained in:
2026-07-25 22:14:24 +02:00
parent db8f947441
commit 06d7e0f30a
5 changed files with 311 additions and 1120 deletions
+5 -2
View File
@@ -663,8 +663,11 @@ pub fn introspect_cmd(cmd: crate::GraphIntrospectCmd, env: &aura_runner::project
// The content id (#158) is the SHA256 of the same `blueprint_to_json`
// bytes `graph build` emits; the identity id (#171) the SHA256 of the
// debug-name-blind `blueprint_identity_json` form — both via the one
// shared `crate::content_id` primitive `topology_hash` also uses, so all
// surfaces agree by construction.
// shared `crate::content_id` primitive, the same hash the run
// manifest's `topology_hash` uses (computed inline in
// `aura_runner::member`'s `run_signal_r`/`run_blueprint_member` — #319
// Task 9 retired the CLI-side `topology_hash` wrapper this comment
// used to name), so all surfaces agree by construction.
let composite = match cmd.content_id.as_ref() {
Some(Some(file)) => {
let text = match std::fs::read_to_string(file) {
+157 -397
View File
@@ -15,26 +15,33 @@ use aura_core::{Scalar, Timestamp};
#[cfg(test)]
use aura_core::ScalarKind;
#[cfg(test)]
use aura_core::Cell;
#[cfg(test)]
use aura_composites::StopRule;
use aura_engine::{
blueprint_from_json, blueprint_to_json, f64_field, join_on_ts,
Composite, JoinedRow,
blueprint_from_json, f64_field, join_on_ts,
JoinedRow,
SelectionMode,
};
// `blueprint_to_json`/`Composite` lost their last production caller with
// `topology_hash` (#319 Task 9 — the CLI-side wrapper retired, its survival
// rationale disproven); reached only from the test module now, mirroring
// `Bias` below.
#[cfg(test)]
use aura_engine::{blueprint_to_json, Composite};
#[cfg(test)]
use aura_engine::{window_of, ColumnarTrace, Harness, VecSource};
use aura_registry::{group_families, rank_by, FamilyMember, NameKind, RunTraces};
// `FamilyKind`/`mc_member_reports`/`sweep_member_reports`/`PlateauMode` are only
// reached from the test module's family-builder reproduce-fixture tests (#319
// Task 8 — the quintet's own production callers of these are retired); the
// imports are test-only, mirroring `Bias` below.
// `FamilyKind` is only reached from the test module's reproduce-fixture tests
// (#319 Task 8 — the quintet's own production callers of these are retired);
// the import is test-only, mirroring `Bias` below.
#[cfg(test)]
use aura_registry::{mc_member_reports, sweep_member_reports, FamilyKind, PlateauMode};
use aura_registry::FamilyKind;
#[cfg(test)]
use aura_registry::Registry;
use aura_backtest::RunReport;
#[cfg(test)]
use aura_backtest::{SweepFamily, WF_REAL_IS_NS, WF_REAL_OOS_NS, WF_REAL_STEP_NS};
use aura_backtest::{WF_REAL_IS_NS, WF_REAL_OOS_NS, WF_REAL_STEP_NS};
use aura_measurement::information_coefficient;
// The C28 assembly position (#295): the member-run recipe, the axis/translate
// helpers, and the harness-assembly constants all live in `aura-runner` now;
@@ -46,18 +53,16 @@ use aura_runner::member::{
use aura_runner::{TapPlan, TapSubscription};
#[cfg(test)]
use aura_runner::member::{run_blueprint_member, wrap_r, SYNTHETIC_PIP_SIZE};
// The family builders (blueprint sweep / walk-forward / MC) and the shared
// `DataSource` data provider / `--select` objective `Selection` live in
// `aura_runner::family` (#295 Task 8); with the quintet's own production
// callers retired (#319 Task 8), they are only reached from the test
// module's family-builder tests now (Task 9 owns their eventual removal) —
// the imports are test-only, mirroring `Bias` below.
// The shared `DataSource` data provider lives in `aura_runner::family`
// (#295 Task 8) — its production caller is `aura_runner::reproduce`;
// `showcase_prices`/`synthetic_walk_sources` are its synthetic stream
// primitives, reached here only from the test module's reproduce-fixture
// tests (#319 Task 9 — the family builders that used to call them in this
// crate are retired). The imports are test-only, mirroring `Bias` below.
#[cfg(test)]
use aura_runner::family::{
blueprint_mc_family, blueprint_sweep_family, blueprint_walkforward_family, DataSource, Selection,
};
use aura_runner::family::{synthetic_walk_sources, DataSource};
#[cfg(test)]
use aura_runner::family::{render_bind_error, select_winner, showcase_prices};
use aura_runner::family::showcase_prices;
// `reproduce_family_in` (#295) lives in `aura_runner::reproduce`, returning
// `Result<_, aura_runner::RunnerError>`; the test module's own call sites
// unwrap it (the production dispatch arm is `dispatch_reproduce`).
@@ -94,19 +99,18 @@ use aura_runner::member::CostLeg;
use aura_backtest::{summarize, summarize_r};
#[cfg(test)]
use aura_engine::GraphBuilder;
#[cfg(test)]
use aura_std::Sub;
// `std_vocabulary` is now only reached through `aura_runner::project::Env::resolve` in production
// code; the test module still builds reference blueprints against it directly, so
// the import is test-only.
#[cfg(test)]
use aura_vocabulary::std_vocabulary;
// `Bias`/`Sma` are only reached from the test module; the imports are test-only.
// `Bias` is only reached from the test module; the import is test-only.
// `Sub`/`Sma` lost their last test users with #319 Task 9's family-builder
// unit-test retirement (they built the deleted tests' probe graphs) — no
// longer imported here.
#[cfg(test)]
use aura_strategy::Bias;
#[cfg(test)]
use aura_std::Sma;
#[cfg(test)]
use std::sync::mpsc;
#[cfg(test)]
use std::collections::HashSet;
@@ -574,31 +578,20 @@ fn runs_family(id: &str, rank: Option<&str>, env: &aura_runner::project::Env) {
// --- r-sma harness (the SMA-cross signal scored in R) --------------------
/// SHA256 (hex) of a canonical (#164) blueprint JSON string — the content id (#158).
/// The single hashing primitive, shared by [`topology_hash`] (from a live `Composite`)
/// and the op-script `graph introspect --content-id` path (`crate::content_id`), so the
/// two surfaces agree by construction over the same canonical bytes. Research-side
/// The single hashing primitive: fed a live `Composite`'s canonical bytes
/// (`blueprint_to_json`) this is the run manifest's `topology_hash` — the
/// same computation `aura_runner::member`'s `run_signal_r`/
/// `run_blueprint_member` inline on the record-line path (#319 Task 9: the
/// CLI-side `topology_hash` wrapper that used to duplicate this call is
/// retired, its survival rationale disproven — its only callers were the
/// family-builder-range unit tests, now gone with the builders) — and fed the
/// op-script `graph introspect --content-id` path's bytes (`crate::content_id`)
/// it is the SAME hash, so the two surfaces agree by construction. Research-side
/// (aura-cli), off the frozen engine (invariant 8).
fn content_id(canonical_json: &str) -> String {
aura_research::content_id_of(canonical_json)
}
/// SHA256 (hex) of the canonical (#164, no-trailing-newline) serialization of a
/// signal blueprint — the run's `topology_hash` (#158).
///
/// #319 Task 8 finding: this primitive's only production caller was
/// `validate_and_register_axes` (retired with the quintet); `exec`'s own
/// record line gets its `manifest.topology_hash` from `aura_runner::member`'s
/// own internal computation, never through this fn. It is kept alive
/// (production-visible, not test-gated) per the retirement plan's explicit
/// instruction, and still exercised directly by the family-builder-range
/// unit tests below (Task 9 territory) — hence `#[allow(dead_code)]` rather
/// than deletion. Orchestrator: confirm whether `exec` should route through
/// this fn after all, or whether the KEEP was based on a stale assumption.
#[allow(dead_code)]
fn topology_hash(signal: &Composite) -> String {
content_id(&blueprint_to_json(signal).expect("a buildable signal serializes"))
}
/// One measurement run's IC scalar + its permutation-null significance, for stdout.
#[derive(serde::Serialize)]
struct IcReport {
@@ -1099,10 +1092,10 @@ fn exec_blueprint_leg(path: &str, overrides: &[String], tap: &[String], env: &au
};
if has_bias {
// #246 override set (Task 3): validated against the un-reopened
// wrapped OPEN space via `override_paths` (the strict, erroring
// variant `blueprint_sweep_family` also resolves its `--axis` grid
// through), so an unmatched path refuses with that same shared
// prose (`member.rs:768-771`) rather than a second wording.
// wrapped OPEN space via `override_paths` (the single strict,
// erroring axis-resolution path this shell drives), so an
// unmatched path refuses with that same shared prose
// (`member.rs:768-771`) rather than a second wording.
let raw_space = blueprint_axis_probe(&doc, env).param_space();
let axes: Vec<(String, Vec<Scalar>)> = ov.iter().map(|(p, v)| (p.clone(), vec![*v])).collect();
let paths = override_paths(&axes, &raw_space, &signal).unwrap_or_else(|e| {
@@ -1541,35 +1534,6 @@ fn main() {
mod tests {
use super::*;
/// Audit follow-up to #247/#269: `render_bind_error` is exhaustive — the
/// axis-usage variants the old catch-all Debug-framed render as prose
/// naming the axis, with no Rust identifier on the user's stderr.
#[test]
fn render_bind_error_prose_covers_the_axis_usage_variants() {
let dup = render_bind_error(&aura_engine::BindError::DuplicateBinding("a.b".into()));
assert!(dup.contains("a.b") && dup.contains("bound twice"), "{dup}");
assert!(!dup.contains("DuplicateBinding"), "Debug leak: {dup}");
let empty = render_bind_error(&aura_engine::BindError::EmptyAxis("a.b".into()));
assert!(empty.contains("a.b") && empty.contains("no values"), "{empty}");
assert!(!empty.contains("EmptyAxis"), "Debug leak: {empty}");
let range = render_bind_error(&aura_engine::BindError::EmptyRange("a.b".into()));
assert!(range.contains("a.b") && !range.contains("EmptyRange"), "Debug leak: {range}");
}
/// Audit follow-up to #247/#269: a `Compile` fault at the sweep boundary —
/// a blueprint defect, not an axis usage error — renders as a prose frame
/// that names the situation and labels the embedded compile detail as
/// internal, instead of leaking the bare Debug struct as the whole message.
#[test]
fn render_bind_error_frames_a_compile_fault_as_prose_with_labeled_detail() {
let msg = render_bind_error(&aura_engine::BindError::Compile(
aura_engine::CompileError::BadInteriorIndex,
));
assert!(msg.contains("failed to bootstrap"), "{msg}");
assert!(msg.contains("internal detail"), "{msg}");
assert!(!msg.starts_with("Compile"), "the frame must lead with prose: {msg}");
}
/// An empty (or absent) archive is informational absence, not a fault
/// (#264), but under the all-JSON namespace reshape (#273) stdout must
/// stay pure NDJSON: `data_list_report` returns an empty `Vec` rather
@@ -1718,17 +1682,6 @@ mod tests {
.expect("loads")
}
#[test]
fn select_winner_refuses_plateau_without_a_lattice() {
// A plateau request with no lattice (a random sweep would yield None) is
// refused, never silently argmaxed. The refuse short-circuits before the
// family is read, so an empty family is fine here.
let fam = SweepFamily { space: vec![], points: vec![] };
let err = select_winner(&fam, "total_pips", Selection::Plateau(PlateauMode::Mean), None)
.unwrap_err();
assert!(err.contains("requires a grid sweep"), "refuse message: {err}");
}
fn cmp_member(key: &str, ts: &[i64], vals: &[f64]) -> FamilyMember {
cmp_member_win(key, ts, vals, (0, 0))
}
@@ -1981,45 +1934,6 @@ mod tests {
assert_eq!(d.full_window(&env), window_of(&s).unwrap());
}
/// A multi-column blueprint over synthetic data (a single close walk)
/// refuses honestly through the builders' exit-free Err contract, naming
/// the beyond-close columns and the --real remedy — never a panic from a
/// source-count mismatch. High/low-consuming, closed (mc requires it),
/// with the mandatory `bias` output.
const OHLC_REFUSAL_BLUEPRINT: &str = r#"{
"format_version": 1,
"blueprint": {
"name": "hl_range",
"nodes": [ {"primitive":{"type":"Sub"}} ],
"edges": [],
"input_roles": [
{"name":"high","targets":[{"node":0,"slot":0}],"source":"F64"},
{"name":"low","targets":[{"node":0,"slot":1}],"source":"F64"}
],
"output": [{"node":0,"field":0,"name":"bias"}]
}
}"#;
#[test]
fn synthetic_data_refuses_a_multi_column_blueprint() {
let env = aura_runner::project::Env::std();
let err = blueprint_mc_family(OHLC_REFUSAL_BLUEPRINT, 2, &DataSource::Synthetic, &env)
.expect_err("a high/low blueprint cannot run over the synthetic close walk");
assert_eq!(
err,
"strategy \"hl_range\" consumes columns beyond close (high, low) — synthetic \
data generates a close series only; run with --real <SYMBOL>"
);
let err = blueprint_sweep_family(
OHLC_REFUSAL_BLUEPRINT,
&[("x".to_string(), vec![Scalar::i64(1)])],
&DataSource::Synthetic,
&env,
)
.expect_err("the synthetic sweep path refuses the same shape");
assert!(err.contains("consumes columns beyond close"), "got: {err}");
}
#[test]
fn wf_real_roller_sizes_are_90_30_30_days_in_ns() {
// Independent expected value: a day reconstructed from its time units
@@ -2043,50 +1957,6 @@ mod tests {
// below is dropped for the same reason (`member_key` no longer exists); the
// doc comment is trimmed to match.
/// Property: a `blueprint_sweep_family` member built from a serialized signal is
/// the SAME trading result as the cycle-1 single run of that signal at the same
/// params — the loaded-blueprint sweep reuses the identical `wrap_r` run path
/// (the keystone). Every member of one family carries the SAME `topology_hash` (the
/// loaded signal's, the deviation from the Rust-built mirror).
#[test]
fn blueprint_sweep_member_equals_single_run_and_shares_topology_hash() {
// An OPEN signal (both SMA knobs free) so the sweep can bind them by name; the
// serialized doc round-trips to the topology the single run hashes.
let env = aura_runner::project::Env::std();
let open = load_open_r_sma();
let doc = blueprint_to_json(&open).expect("serializes");
let data = DataSource::Synthetic;
// fast pinned at 2, slow varied over {4, 6}: a 2x1 grid, slow the varying axis.
let axes = vec![
("sma_signal.fast.length".to_string(), vec![Scalar::i64(2)]),
("sma_signal.slow.length".to_string(), vec![Scalar::i64(4), Scalar::i64(6)]),
];
let family = blueprint_sweep_family(&doc, &axes, &data, &env).expect("named axes resolve");
assert_eq!(family.points.len(), 2, "2x1 grid -> 2 members");
// (b) every member carries the shared topology_hash of the loaded signal.
let topo = topology_hash(&open);
assert_eq!(topo.len(), 64, "topology_hash is a 64-hex SHA256");
for pt in &family.points {
assert_eq!(pt.report.manifest.topology_hash.as_deref(), Some(topo.as_str()));
}
// (a) the slow=4 member reproduces the cycle-1 single run at fast=2, slow=4 —
// same equity/exposure stream (total_pips/max_drawdown/bias_sign_flips) and the
// same topology_hash, proving the loaded blueprint runs through the identical path.
let single = run_signal_r(
load_open_r_sma(),
&[Scalar::i64(2), Scalar::i64(4)],
RunData::Synthetic,
0,
&env,
TapPlan::record_all(),
);
let member4 = &family.points[0].report; // slow=4 is the first odometer point
assert_eq!(member4.metrics, single.metrics, "loaded sweep member == single run");
assert_eq!(member4.manifest.topology_hash, single.manifest.topology_hash);
}
#[test]
fn blueprint_axis_probe_lists_prefixed_open_knobs() {
// The open fixture's two SMA lengths are the sweepable knobs; the probe
@@ -2104,97 +1974,15 @@ mod tests {
assert!(blueprint_axis_probe(closed, &env).param_space().is_empty());
}
#[test]
fn blueprint_walkforward_family_refits_each_window() {
// The closed blueprint's two bound SMA lengths are re-fit per IS window over
// a 2x2 grid via the #246 bound-override reopen path (bound = overridable default).
let env = aura_runner::project::Env::std();
let doc = include_str!("../examples/r_sma.json");
let axes = vec![
("sma_signal.fast.length".to_string(), vec![Scalar::i64(2), Scalar::i64(3)]),
("sma_signal.slow.length".to_string(), vec![Scalar::i64(4), Scalar::i64(6)]),
];
let result = blueprint_walkforward_family(doc, &axes, &DataSource::Synthetic, Selection::Argmax, &env);
// 24/12/12 over the 60-bar synthetic span -> 3 rolling windows.
assert_eq!(result.windows.len(), 3, "three rolling IS/OOS windows");
for w in &result.windows {
assert_eq!(w.run.chosen_params.len(), 2, "both axes re-fit each window");
assert!(w.run.oos_report.metrics.r.is_some(), "OOS record is R-metrics");
}
// reduce-mode retains no raw pip curve -> the stitched pip-equity is empty.
assert!(result.stitched_oos_equity.is_empty(), "no raw pip curve in reduce-mode");
}
#[test]
fn blueprint_mc_family_seeds_differ() {
// MC over a CLOSED signal (both SMA knobs bound): 3 seeds -> 3 draws, one shared
// topology_hash, and DIFFERING metrics — the seed reaches the DATA (a distinct
// synthetic walk per draw), not just the manifest label. The anti-degenerate guard:
// a regression to seed-as-label-only would make the three draws identical.
let env = aura_runner::project::Env::std();
let closed = load_closed_r_sma();
let doc = blueprint_to_json(&closed).expect("serializes");
let family = blueprint_mc_family(&doc, 3, &DataSource::Synthetic, &env).expect("closed blueprint");
assert_eq!(family.draws.len(), 3, "one draw per seed");
assert_eq!(family.draws.iter().map(|d| d.seed).collect::<Vec<_>>(), vec![1, 2, 3]);
let topo = family.draws[0].report.manifest.topology_hash.clone();
assert!(topo.is_some(), "members carry a topology_hash");
assert!(
family.draws.iter().all(|d| d.report.manifest.topology_hash == topo),
"all members share one topology_hash"
);
let m: Vec<_> = family.draws.iter().map(|d| &d.report.metrics).collect();
assert!(m[0] != m[1] || m[1] != m[2], "seeds must yield differing realizations");
}
#[test]
fn blueprint_mc_family_rejects_vacuous_deep_lookback() {
// Property: the mc family builder REFUSES a silent-vacuous Monte-Carlo — one where
// every per-seed draw collapses to a bit-identical realization — by RETURNING a named
// error rather than an `Ok` family that looks like a real (but indistinguishable)
// distribution. A CLOSED deep-lookback signal whose slow SMA length (60) equals the
// fixed 60-bar synthetic walk never warms, so every seed yields zero trades and thus
// identical metrics; that is a wrong result with no error (C10 refuse-don't-guess).
let env = aura_runner::project::Env::std();
// slow len (60) == walk len -> never warms
let mut g = GraphBuilder::new("deep_probe");
let fast = g.add(Sma::builder().named("fast").bind("length", Scalar::i64(2)));
let slow = g.add(Sma::builder().named("slow").bind("length", Scalar::i64(60)));
let spread = g.add(Sub::builder());
let exposure = g.add(Bias::builder().named("bias").bind("scale", Scalar::f64(0.5)));
let price = g.source_role("price", ScalarKind::F64);
g.feed(price, vec![fast.input("series"), slow.input("series")]);
g.connect(fast.output("value"), spread.input("lhs"));
g.connect(slow.output("value"), spread.input("rhs"));
g.connect(spread.output("value"), exposure.input("signal"));
g.expose(exposure.output("bias"), "bias");
let deep = g.build().expect("deep probe wiring resolves");
let doc = blueprint_to_json(&deep).expect("serializes");
let err = blueprint_mc_family(&doc, 3, &DataSource::Synthetic, &env)
.expect_err("a vacuous (all-identical) Monte-Carlo is rejected, not returned");
assert!(
err.contains("vacuous") || err.contains("identical"),
"names the vacuous/degenerate condition: {err}"
);
}
#[test]
fn blueprint_mc_family_rejects_an_open_blueprint() {
// Property: the mc family builder REFUSES an open blueprint (free knobs) by RETURNING
// a named error — never a hidden process exit — so the closed-blueprint precondition
// is unit-testable (the IO wrapper renders it to stderr + exit 2, mirroring the sibling
// blueprint_sweep_family). MC binds no axis, so a free knob would have no binder; the
// rejection pre-empts the downstream compile_with_params arity panic.
let env = aura_runner::project::Env::std();
// both SMA knobs free -> non-empty param_space
let open = load_open_r_sma();
let doc = blueprint_to_json(&open).expect("serializes");
let err = blueprint_mc_family(&doc, 4, &DataSource::Synthetic, &env)
.expect_err("an open blueprint is rejected, not run");
assert!(err.contains("closed blueprint"), "names the closed-blueprint requirement: {err}");
}
/// Property, ported (#319 — the family builders retired, this test's
/// original minted its 2-member family via the now-deleted
/// `blueprint_sweep_family`): `reproduce_family_in` re-derives EVERY
/// member of a persisted multi-member family bit-identically (index
/// alignment across members, not just the single-member case its
/// stop-regime/costed-member siblings pin) — including the open-at-end
/// member. The family is minted directly via `run_blueprint_member`
/// (the same reduce-mode member path any minting mechanism drives),
/// mirroring the sibling reproduce tests below.
#[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.
@@ -2208,20 +1996,44 @@ mod tests {
let open = load_open_r_sma();
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![
("sma_signal.fast.length".to_string(), vec![Scalar::i64(2)]),
("sma_signal.slow.length".to_string(), vec![Scalar::i64(4), Scalar::i64(6)]),
];
let family = blueprint_sweep_family(&doc, &axes, &data, &env).expect("axes resolve");
let reload = || blueprint_from_json(&doc, &|t| std_vocabulary(t)).expect("loads");
// topology_hash's own two-line body, inlined (mirrors
// `aura_runner::member::run_signal_r`'s and `run_blueprint_member`'s
// own copy — the #319 Task 9 finding: the CLI-side `topology_hash`
// wrapper is retired, its only production/test callers gone).
let topo = content_id(&blueprint_to_json(&reload()).expect("a buildable signal serializes"));
let binding = aura_runner::binding::resolve_binding("everymember", reload().input_roles(), &BTreeMap::new())
.expect("the price role resolves");
let window = data.full_window(&env);
let pip = data.pip_size();
// fast pinned at 2, slow varied over {4, 6}: two members, slow=6 the open-at-end one.
let space = blueprint_axis_probe(&doc, &env).param_space();
let reports: Vec<RunReport> = [4_i64, 6]
.iter()
.map(|&slow| {
run_blueprint_member(
reload(),
&[Cell::from_i64(2), Cell::from_i64(slow)],
&space,
data.run_sources(&env, &binding.columns()),
window,
0,
pip,
&topo,
&env,
StopRule::Vol { length: R_SMA_STOP_LENGTH, k: R_SMA_STOP_K },
&binding,
&[],
"",
)
})
.collect();
// 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();
// persist exactly as any family-minting mechanism does: store the blueprint, append the family.
let canonical = blueprint_to_json(&reload()).unwrap();
reg.put_blueprint(&topo, &canonical).expect("store blueprint");
let id = reg
.append_family("repro", FamilyKind::Sweep, &sweep_member_reports(&family))
.append_family("repro", FamilyKind::Sweep, &reports)
.expect("append");
// reproduce: every member re-derives bit-identically (incl the open-at-end member).
@@ -2258,7 +2070,11 @@ mod tests {
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 = topology_hash(&reload());
// topology_hash's own two-line body, inlined (mirrors
// `aura_runner::member::run_signal_r`'s and `run_blueprint_member`'s
// own copy — the #319 Task 9 finding: the CLI-side `topology_hash`
// wrapper is retired, its only production/test callers gone).
let topo = content_id(&blueprint_to_json(&reload()).expect("a buildable signal serializes"));
let space = blueprint_axis_probe(&doc, &env).param_space();
let pip = data.pip_size();
let window = data.full_window(&env);
@@ -2324,7 +2140,11 @@ mod tests {
let data = DataSource::Synthetic;
let doc = blueprint_to_json(&load_closed_r_sma()).expect("serializes");
let reload = || blueprint_from_json(&doc, &|t| std_vocabulary(t)).expect("loads");
let topo = topology_hash(&reload());
// topology_hash's own two-line body, inlined (mirrors
// `aura_runner::member::run_signal_r`'s and `run_blueprint_member`'s
// own copy — the #319 Task 9 finding: the CLI-side `topology_hash`
// wrapper is retired, its only production/test callers gone).
let topo = content_id(&blueprint_to_json(&reload()).expect("a buildable signal serializes"));
let space = blueprint_axis_probe(&doc, &env).param_space();
let pip = data.pip_size();
let window = data.full_window(&env);
@@ -2375,6 +2195,14 @@ mod tests {
let _ = std::fs::remove_dir_all(&dir);
}
/// Property, ported (#319 — the family builders retired, this test's
/// original minted its 3-draw MC family via the now-deleted
/// `blueprint_mc_family`): `reproduce_family_in` re-derives every MC
/// member bit-identically, each seed's synthetic walk reconstructed from
/// `manifest.seed` (the realization branch) — minted directly via
/// `run_blueprint_member` over `synthetic_walk_sources(seed)`, the same
/// per-seed source `aura_runner::reproduce`'s own synthetic-MC branch
/// reconstructs.
#[test]
fn reproduce_family_re_derives_every_mc_member_bit_identically() {
let dir = std::path::Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/../../target/tmp"))
@@ -2387,16 +2215,34 @@ mod tests {
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 data = DataSource::Synthetic;
let family = blueprint_mc_family(&doc, 3, &data, &env).expect("closed blueprint");
// topology_hash's own two-line body, inlined (mirrors
// `aura_runner::member::run_signal_r`'s and `run_blueprint_member`'s
// own copy — the #319 Task 9 finding: the CLI-side `topology_hash`
// wrapper is retired, its only production/test callers gone).
let topo = content_id(&blueprint_to_json(&reload()).expect("a buildable signal serializes"));
let binding = aura_runner::binding::resolve_binding("everymc", reload().input_roles(), &BTreeMap::new())
.expect("the price role resolves");
let space = blueprint_axis_probe(&doc, &env).param_space();
assert!(space.is_empty(), "MC binds no axis — the fixture must be closed");
// persist exactly as run_blueprint_mc does: store the blueprint, append the MC family.
let topo = family.draws[0].report.manifest.topology_hash.clone().expect("topo");
let canonical =
blueprint_to_json(&blueprint_from_json(&doc, &|t| std_vocabulary(t)).unwrap()).unwrap();
let reports: Vec<RunReport> = (1_u64..=3)
.map(|seed| {
let sources = synthetic_walk_sources(seed);
let window = window_of(&sources).expect("non-empty synthetic walk");
run_blueprint_member(
reload(), &[], &space, sources, window, seed, data.pip_size(), &topo, &env,
StopRule::Vol { length: R_SMA_STOP_LENGTH, k: R_SMA_STOP_K }, &binding, &[], "",
)
})
.collect();
// persist exactly as any MC-minting mechanism does: store the blueprint, append the family.
let canonical = blueprint_to_json(&reload()).unwrap();
reg.put_blueprint(&topo, &canonical).expect("store blueprint");
let id = reg
.append_family("mcrepro", FamilyKind::MonteCarlo, &mc_member_reports(&family))
.append_family("mcrepro", FamilyKind::MonteCarlo, &reports)
.expect("append");
// reproduce: every MC member re-derives bit-identically (its seed-driven walk is
@@ -2411,73 +2257,13 @@ mod tests {
let _ = std::fs::remove_dir_all(&dir);
}
/// #246/#328: a fully-bound (closed) blueprint IS sweepable — a RAW axis
/// naming a bound param re-opens it (the bound value is the default), so
/// the retired "fully bound; nothing to sweep" refusal must not resurface.
/// Inverse guard: an axis naming NEITHER an open nor a bound param gets
/// the one clear boundary message (not a terse `UnknownKnob` debug leak).
#[test]
fn blueprint_sweep_family_overrides_a_bound_param_and_names_unknown_axes() {
let env = aura_runner::project::Env::std();
// same fixture the retired test swept: both SMA knobs bound.
let closed = load_closed_r_sma();
let doc = blueprint_to_json(&closed).expect("serializes");
// (a) override axis (RAW name, #328): two members, no refusal
let axes = vec![(
"fast.length".to_string(),
vec![Scalar::i64(2), Scalar::i64(4)],
)];
let fam = blueprint_sweep_family(&doc, &axes, &DataSource::Synthetic, &env)
.expect("a bound param is a default — the axis overrides it");
assert_eq!(fam.points.len(), 2);
// (b) unknown axis: the boundary message, no UnknownKnob leak
let bad = vec![("nope".to_string(), vec![Scalar::i64(1)])];
let err = blueprint_sweep_family(&doc, &bad, &DataSource::Synthetic, &env)
.expect_err("an axis matching neither space is refused");
assert!(err.contains("names no param"), "boundary message, got: {err}");
assert!(err.contains("--list-axes"), "must point at --list-axes: {err}");
assert!(!err.contains("UnknownKnob"), "must not leak the debug render: {err}");
}
/// #249/#328: an axis-reopened bound param flows through `params` ("what
/// varied") and must NOT also appear in `defaults` ("what was held") — the
/// two are disjoint by construction. Sweeping `fast.length` (RAW, #328) on
/// the fully bound r_sma example leaves `slow.length`/`bias.scale`
/// untouched (they stay in `defaults`), while `fast.length` moves to
/// `params` and drops out of `defaults` entirely. Both `manifest.params`
/// and `manifest.defaults` keys are RAW (#328, batch 2 flips defaults too).
#[test]
fn sweep_override_excludes_the_reopened_default_from_the_manifest() {
let env = aura_runner::project::Env::std();
let closed = load_closed_r_sma();
let doc = blueprint_to_json(&closed).expect("serializes");
let axes = vec![("fast.length".to_string(), vec![Scalar::i64(2)])];
let fam = blueprint_sweep_family(&doc, &axes, &DataSource::Synthetic, &env)
.expect("a bound param is a default — the axis overrides it");
assert_eq!(fam.points.len(), 1);
let manifest = &fam.points[0].report.manifest;
let param_names: Vec<&str> = manifest.params.iter().map(|(n, _)| n.as_str()).collect();
assert!(param_names.contains(&"fast.length"), "swept axis is a raw param: {param_names:?}");
let default_names: Vec<&str> = manifest.defaults.iter().map(|(n, _)| n.as_str()).collect();
assert!(
!default_names.contains(&"fast.length"),
"the reopened default must not also appear in defaults: {default_names:?}"
);
assert_eq!(
default_names,
["slow.length", "bias.scale"],
"the untouched bound params stay in defaults, RAW: {default_names:?}"
);
}
/// Property: an MC family's `aura reproduce` lines carry the member's own `seed=<N>`
/// label. MC members hold no tuning params (the params-join is empty), so the line would
/// otherwise print a BLANK member label; the seed is each draw's realization identity and
/// must show. Sweep / walk-forward labels still echo their params (covered elsewhere).
/// Property, ported (#319 — the family builders retired, same minting
/// swap as the sibling MC-reproduce test above): an MC family's `aura
/// reproduce` lines carry the member's own `seed=<N>` label. MC members
/// hold no tuning params (the params-join is empty), so the line would
/// otherwise print a BLANK member label; the seed is each draw's
/// realization identity and must show. Sweep / walk-forward labels
/// still echo their params (covered elsewhere).
#[test]
fn reproduce_mc_member_labels_carry_the_seed() {
let dir = std::path::Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/../../target/tmp"))
@@ -2490,15 +2276,32 @@ mod tests {
// MC binds no axis -> closed blueprint
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 data = DataSource::Synthetic;
let family = blueprint_mc_family(&doc, 3, &data, &env).expect("closed blueprint");
// topology_hash's own two-line body, inlined (mirrors
// `aura_runner::member::run_signal_r`'s and `run_blueprint_member`'s
// own copy — the #319 Task 9 finding: the CLI-side `topology_hash`
// wrapper is retired, its only production/test callers gone).
let topo = content_id(&blueprint_to_json(&reload()).expect("a buildable signal serializes"));
let binding = aura_runner::binding::resolve_binding("mcseedlabel", reload().input_roles(), &BTreeMap::new())
.expect("the price role resolves");
let space = blueprint_axis_probe(&doc, &env).param_space();
let topo = family.draws[0].report.manifest.topology_hash.clone().expect("topo");
let canonical =
blueprint_to_json(&blueprint_from_json(&doc, &|t| std_vocabulary(t)).unwrap()).unwrap();
let reports: Vec<RunReport> = (1_u64..=3)
.map(|seed| {
let sources = synthetic_walk_sources(seed);
let window = window_of(&sources).expect("non-empty synthetic walk");
run_blueprint_member(
reload(), &[], &space, sources, window, seed, data.pip_size(), &topo, &env,
StopRule::Vol { length: R_SMA_STOP_LENGTH, k: R_SMA_STOP_K }, &binding, &[], "",
)
})
.collect();
let canonical = blueprint_to_json(&reload()).unwrap();
reg.put_blueprint(&topo, &canonical).expect("store blueprint");
let id = reg
.append_family("mcseed", FamilyKind::MonteCarlo, &mc_member_reports(&family))
.append_family("mcseed", FamilyKind::MonteCarlo, &reports)
.expect("append");
let rep = reproduce_family_in(&reg, &id, &data, &env).expect("reproduce_family_in succeeds over a well-formed persisted family");
@@ -2552,43 +2355,10 @@ mod tests {
assert_eq!(doc, doc2, "f64 blueprint param survives the store round-trip bit-identically");
}
/// `topology_hash` is deterministic per signal and distinguishes topologies —
/// the #158 reproducibility-anchor property.
#[test]
fn topology_hash_is_stable_and_distinguishes() {
let closed = load_closed_r_sma();
let open = load_open_r_sma();
let h = topology_hash(&closed);
assert_eq!(h, topology_hash(&closed), "same signal -> same hash");
assert_ne!(h, topology_hash(&open), "distinct blueprints -> distinct hash");
// Same shape as the closed example, differing only in the BOUND fast-SMA
// length (3 vs 2): the hash must still discriminate on bound param values,
// not merely on topology shape — distinct sweep/mc members must key to
// distinct store entries.
let mut g = GraphBuilder::new("sma_signal");
let fast = g.add(Sma::builder().named("fast").bind("length", Scalar::i64(3)));
let slow = g.add(Sma::builder().named("slow").bind("length", Scalar::i64(4)));
let spread = g.add(Sub::builder());
let exposure = g.add(Bias::builder().named("bias").bind("scale", Scalar::f64(0.5)));
let price = g.source_role("price", ScalarKind::F64);
g.feed(price, vec![fast.input("series"), slow.input("series")]);
g.connect(fast.output("value"), spread.input("lhs"));
g.connect(slow.output("value"), spread.input("rhs"));
g.connect(spread.output("value"), exposure.input("signal"));
g.expose(exposure.output("bias"), "bias");
let bound_variant = g.build().expect("bound-value probe wiring resolves");
assert_ne!(
h,
topology_hash(&bound_variant),
"same shape, different bound value -> different hash"
);
}
/// #158 acc 1 (content-id stability across the store round-trip): a blueprint's content
/// id survives serialize -> reload -> re-serialize (#164 idempotence) — the property
/// content-addressed reproduction rests on (the stored bytes re-hash to the members'
/// topology_hash). `content_id` is the shared primitive `topology_hash` uses.
/// manifest `topology_hash`, `content_id`'s own composite-facing computation).
#[test]
fn content_id_is_stable_across_the_store_round_trip() {
let closed = load_closed_r_sma();
@@ -2624,16 +2394,6 @@ mod tests {
);
}
/// #158 acc 1 (cross-surface agreement): `topology_hash` (from a live `Composite`, the
/// run/sweep path) and the op-script `graph introspect --content-id` surface are the
/// SAME hash of the SAME canonical bytes — both go through the one `content_id`
/// primitive. Pins that the two command paths cannot silently drift apart.
#[test]
fn topology_hash_is_the_content_id_of_the_canonical_form() {
let sig = load_closed_r_sma();
assert_eq!(topology_hash(&sig), content_id(&blueprint_to_json(&sig).expect("serializes")));
}
/// The op-script twin of the closed r-sma example (`load_closed_r_sma`): same
/// topology — SMA(2)/SMA(4) over one `price` role, spread, Bias with `scale`
/// BOUND to 0.5 (boundness is identity-bearing) — differing only in debug names
@@ -2663,7 +2423,7 @@ mod tests {
.expect("op-script twin builds");
assert_ne!(
content_id(&json),
topology_hash(&rust_built),
content_id(&blueprint_to_json(&rust_built).expect("a buildable signal serializes")),
"authoring paths keep distinct content ids"
);
let loaded = blueprint_from_json(&json, &|t| std_vocabulary(t)).expect("twin reloads");