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");
+119
View File
@@ -1,6 +1,7 @@
//! exec — the one executor verb (#319): campaign file, campaign id,
//! blueprint single run. Flag discipline is per-leg.
use std::collections::HashSet;
use std::path::{Path, PathBuf};
mod common;
@@ -681,6 +682,124 @@ fn exec_campaign_walkforward_with_trades_emits_no_zero_trade_note() {
assert!(!out.contains("recorded zero trades"), "a traded run emits no zero-trade note: {out}");
}
// ---------------------------------------------------------------------------
// Task 9: `aura-runner` family-builder retirement — the run-semantics half of
// the retired `blueprint_sweep_family` unit test, ported onto the surviving
// campaign path.
// ---------------------------------------------------------------------------
/// [`campaign_doc_json_for`]'s shape with exactly ONE declared axis
/// (`fast.length`) at caller-chosen values — `slow.length`/`bias.scale` stay
/// at their bound defaults (4 and 0.5, `examples/r_sma.json`), isolating the
/// swept dimension for a clean member-by-member comparison.
fn campaign_doc_json_one_axis(
instrument: &str,
bp_id: &str,
proc_id: &str,
window: (i64, i64),
fast_values: &str,
) -> String {
format!(
r#"{{
"format_version": 1,
"kind": "campaign",
"name": "member-parity",
"data": {{ "instruments": ["{instrument}"], "windows": [ {{ "from_ms": {from}, "to_ms": {to} }} ] }},
"strategies": [ {{ "ref": {{ "content_id": "{bp}" }},
"axes": {{ "fast.length": {{ "kind": "I64", "values": [{fast_values}] }} }} }} ],
"process": {{ "ref": {{ "content_id": "{proc}" }} }},
"seed": 7,
"presentation": {{ "persist_taps": [], "emit": ["family_table"] }}
}}"#,
from = window.0,
to = window.1,
bp = bp_id,
proc = proc_id,
)
}
/// Property, ported from the retired `blueprint_sweep_family` unit test
/// `blueprint_sweep_member_equals_single_run_and_shares_topology_hash` (#319,
/// the family builders retired): a campaign family member computes the
/// IDENTICAL trading result a STANDALONE one-cell campaign at that same point
/// computes — the sweep terminal reuses the same member-run path regardless
/// of how many sibling points ride along — and every member of one family
/// carries the SAME `topology_hash` (the loaded blueprint's, not a per-point
/// value). `exec`'s blueprint leg has no `--real` (invariant 7: "a real-data
/// single run is a one-cell campaign document"), so the "single run"
/// comparator here IS the one-cell campaign the retirement itself designates
/// as that replacement, run over the identical instrument+window as the
/// multi-member family.
#[test]
fn exec_campaign_member_equals_a_standalone_single_cell_and_shares_topology_hash() {
let (dir, _fixture) = fresh_project_with_data();
let runs_dir = dir.join("runs");
std::fs::remove_dir_all(&runs_dir).ok();
let _cleanup = ScratchGuard(vec![
ScratchPath::Dir(runs_dir.clone()),
ScratchPath::File(dir.join("parity.process.json")),
ScratchPath::File(dir.join("parity_family.campaign.json")),
ScratchPath::File(dir.join("parity_single.campaign.json")),
]);
let bp_id = seed_blueprint(&dir, "exec-parity-seed");
let proc_id = register_process_doc(&dir, "parity.process.json", SWEEP_ONLY_PROCESS_DOC);
let window = (1709251200000, 1719791999999);
// (A) a 2-member family: fast.length in {2, 8}; slow.length/bias.scale
// stay at their bound defaults.
let doc_family = campaign_doc_json_one_axis("SYMA", &bp_id, &proc_id, window, "2, 8");
write_doc(&dir, "parity_family.campaign.json", &doc_family);
let (out_family, code_family) = run_code_in(&dir, &["exec", "parity_family.campaign.json"]);
assert_eq!(code_family, Some(0), "family run: {out_family}");
let family_members: Vec<serde_json::Value> = out_family
.lines()
.filter(|l| l.starts_with(r#"{"family_id":"#))
.map(|l| serde_json::from_str(l).expect("member line parses"))
.collect();
assert_eq!(family_members.len(), 2, "a 2-value axis yields 2 members: {out_family}");
let topo_values: HashSet<&str> = family_members
.iter()
.map(|m| m["report"]["manifest"]["topology_hash"].as_str().expect("topology_hash present"))
.collect();
assert_eq!(
topo_values.len(),
1,
"every member of one family shares ONE topology_hash: {family_members:?}"
);
let member_8 = family_members
.iter()
.find(|m| {
m["report"]["manifest"]["params"]
.as_array()
.expect("params array")
.iter()
.any(|e| e[0] == "fast.length" && e[1] == serde_json::json!({"I64": 8}))
})
.expect("the fast.length=8 member is present");
// (B) a standalone one-cell campaign pinned at the SAME point.
let doc_single = campaign_doc_json_one_axis("SYMA", &bp_id, &proc_id, window, "8");
write_doc(&dir, "parity_single.campaign.json", &doc_single);
let (out_single, code_single) = run_code_in(&dir, &["exec", "parity_single.campaign.json"]);
assert_eq!(code_single, Some(0), "single-cell run: {out_single}");
let single_member: serde_json::Value = out_single
.lines()
.find(|l| l.starts_with(r#"{"family_id":"#))
.map(|l| serde_json::from_str(l).expect("member line parses"))
.expect("the one-cell campaign's single member line");
assert_eq!(
member_8["report"]["metrics"], single_member["report"]["metrics"],
"a family member computes the identical result a standalone single-cell run at \
the same point computes"
);
assert_eq!(
member_8["report"]["manifest"]["topology_hash"],
single_member["report"]["manifest"]["topology_hash"],
"the standalone single-cell run shares the same topology_hash"
);
}
// ---------------------------------------------------------------------------
// Task 8: `cli_run.rs` disposition — ports onto `exec` (retire+port half only;
// the port-to-campaign-document half is a separate follow-up dispatch).
+30 -720
View File
@@ -1,60 +1,23 @@
//! Family assembly and orchestration (#295).
//!
//! The blueprint sweep / walk-forward / Monte-Carlo family builders — pure
//! member-run recipes driven off a [`DataSource`] (the shared synthetic/real
//! data provider every family builder threads, plus the `--select` objective
//! [`Selection`] walk-forward resolves its winner under) — together with the
//! winner-selection (`select_winner`) and axis-grid validation
//! (`validate_axis_grid`) machinery they share. No `aura` binary is needed to
//! drive a family: the shell (`aura-cli`) wraps these builders with
//! persistence + stdout rendering (`run_blueprint_sweep`,
//! `run_blueprint_walkforward`, `run_blueprint_mc`), which stay in the shell,
//! along with the `--select`/`--real` argv grammar (`parse_select`,
//! `select_rule_of`) and the `topology_hash`/`content_id` naming primitive
//! (inlined here instead, mirroring `member::run_signal_r`'s own copy — the
//! CLI shell still needs its own for call sites outside any family builder).
//! The blueprint sweep / walk-forward / Monte-Carlo family builders that used
//! to live here are retired (#319 — the research-verb quintet's own
//! machinery; the campaign document path, `aura-campaign::exec`, is the
//! surviving family-execution surface, with its own independent axis/window/
//! winner-selection machinery). What remains is the shared [`DataSource`]
//! data provider — the synthetic/real price-source abstraction
//! `aura_runner::reproduce` drives its own bit-identical re-derivation
//! over — and its synthetic stream primitives (`showcase_prices`,
//! `walkforward_prices`, `walkforward_window_source`, `synthetic_walk_sources`).
use std::collections::BTreeMap;
use std::sync::{Arc, Mutex};
use std::sync::Arc;
use aura_campaign::{catch_member_panic, member_fault_prose, MemberFault};
use aura_composites::StopRule;
use aura_core::{Cell, ParamSpec, Scalar, Timestamp};
use aura_engine::{
blueprint_from_json, walk_forward, window_of, BindError, Composite, FamilySelection,
RollMode, RunManifest, SyntheticSpec, VecSource, WindowBounds, WindowRoller,
};
use aura_registry::{
optimize_deflated, optimize_plateau, PlateauMode, DEFLATION_BLOCK_LEN, DEFLATION_N_RESAMPLES,
};
use aura_backtest::{
monte_carlo, McFamily, RunMetrics, RunReport, SweepFamily, SweepPoint, WalkForwardResult,
WindowRun, WF_REAL_IS_NS, WF_REAL_OOS_NS, WF_REAL_STEP_NS,
};
use aura_core::{Scalar, Timestamp};
use aura_engine::{window_of, SyntheticSpec, VecSource};
use aura_backtest::{WF_REAL_IS_NS, WF_REAL_OOS_NS, WF_REAL_STEP_NS};
use crate::binding::ResolvedBinding;
use crate::member::{
blueprint_axis_probe, blueprint_axis_probe_reopened, no_real_data, override_paths,
pip_or_refuse, probe_window, reopen_all, run_blueprint_member, wrapped_bound_overrides_of,
SYNTHETIC_PIP_SIZE,
};
use crate::member::{no_real_data, pip_or_refuse, probe_window, SYNTHETIC_PIP_SIZE};
use crate::project::Env;
use crate::translate::{R_SMA_STOP_K, R_SMA_STOP_LENGTH};
/// The demo default stop regime, shared by every family builder in this module.
const DEFAULT_STOP: StopRule = StopRule::Vol { length: R_SMA_STOP_LENGTH, k: R_SMA_STOP_K };
/// The in-sample winner-selection objective for walk-forward's per-window IS
/// refit (cycle 0077). `Argmax` is the bare-best pick deflated for trials
/// (#144, the default); `Plateau` argmaxes the neighbourhood-smoothed surface
/// instead (opt-in via `--select`). The CLI shell's `--select` grammar
/// (`parse_select`) and campaign-rule mapping (`select_rule_of`) build this
/// value; they stay in the shell since only this type crosses the boundary.
#[derive(Clone, Copy)]
pub enum Selection {
Argmax,
Plateau(PlateauMode),
}
/// A warm-up-adequate synthetic stream (~18 ticks rising, falling, then rising
/// again) used as `DataSource::Synthetic`'s full-window stream for the built-in
@@ -103,19 +66,18 @@ pub enum DataChoice {
Real { symbol: String, from_ms: Option<i64>, to_ms: Option<i64> },
}
/// The source provider threaded into the family builders: synthetic built-in
/// streams, or real M1 close bars from the data-server archive. Replaces the
/// hardcoded `VecSource` so a member's source, pip, window, and roller sizes come
/// from one place (Fork B/D/F).
/// The source provider `aura_runner::reproduce` threads through its
/// re-derivation: synthetic built-in streams, or real M1 close bars from the
/// data-server archive. Replaces the hardcoded `VecSource` so a member's
/// source, pip, window, and roller sizes come from one place (Fork B/D/F).
///
/// `Synthetic` denotes a *consumer-dependent* built-in stream, not one fixed
/// series: the full-window consumers (`full_window` / `run_sources`, used by
/// sweep / MC) draw the 18-bar `showcase_prices()`, while the windowed consumers
/// (`windowed_sources` / `wf_window_sizes`, used by walk-forward) draw the 60-bar
/// series: the full-window consumers (`full_window` / `run_sources`) draw the
/// 18-bar `showcase_prices()`, while the windowed consumers
/// (`windowed_sources` / `wf_window_sizes`) draw the 60-bar
/// `walkforward_prices()` so the `(24,12,12)`-bar roller fits its span. The two
/// faces never reach one consumer (a family is either full-window or windowed), so
/// the split is invisible per call site but real across the type — read both
/// family builders to see it whole.
/// faces never reach one consumer (full-window vs. windowed reproduction), so
/// the split is invisible per call site but real across the type.
pub enum DataSource {
Synthetic,
Real {
@@ -223,666 +185,14 @@ impl DataSource {
}
}
/// #260: the r-sma sugar/MC paths below run with either no cost model (empty
/// `cost` slice) or CLI-flag cost specs (scalar-only — `cost_specs_from_params`
/// only ever wraps `CostValue::Scalar`), so an instrument-keyed map can never
/// originate on these paths and the instrument context is genuinely inert.
/// Named once so every such call site states its intent by reference instead
/// of repeating the justifying comment.
const NO_INSTRUMENT_CONTEXT: &str = "";
/// The winner-selection objective for walk-forward's per-window IS refit — the
/// deflation-aware SQN variant (#144 default). Named once so `select_winner`
/// and `blueprint_walkforward_family` (the only two family-builder-side call
/// sites) cannot drift apart on the token; the CLI shell's campaign-sugar
/// bridge keeps its own copy of this token (main.rs `WINNER_SELECTION_METRIC`)
/// since it is not itself a family builder.
const WINNER_SELECTION_METRIC: &str = "sqn_normalized";
/// Fixed RNG seed for the trials-deflation reality-check bootstrap in walk-forward
/// winner selection. Recorded on each winner's manifest (so `overfit_probability`
/// is reproducible by re-run); a CLI flag for it is a deferred refinement. The
/// resample count and block length are the shared `aura_registry::DEFLATION_*`.
const DEFLATION_SEED: u64 = 0xDEF1_A7ED;
/// Renders a [`BindError`] as one-line prose in `member::override_paths`' sibling
/// register above (#247) — never the raw Rust `Debug` struct name
/// (`KindMismatch { .. }` / `MissingKnob("..")`), the two variants the sweep
/// terminal actually raises past `override_paths`' own pre-flight (which
/// already rejects an unresolvable axis name as prose before either call
/// site below ever reaches the terminal). `UnknownKnob` already carries a
/// fully-prosed message string (wrapped from `override_paths`' own
/// `Result<_, String>`) — unwrapped here rather than Debug-framed (#269), so
/// the walkforward path's rejection reaches stderr as bare prose too.
///
/// #328 render-seam fix: `BindError::MissingKnob`/`KindMismatch` carry the
/// WRAPPED knob name (the terminal's own bind-time frame — untouched by this
/// cycle, per the minuted deviation on acceptance-criterion 2). No
/// user-facing surface may print a wrapped axis name, so this renderer strips
/// the one leading node segment (`wrapped_to_raw_axis`, the same convention
/// `axes.rs` defines) before the name ever reaches prose — the fix lives at
/// the RENDER seam only, not in the error type or the resolution machinery.
pub fn render_bind_error(e: &BindError) -> String {
match e {
BindError::MissingKnob(name) => {
let name = crate::axes::wrapped_to_raw_axis(name);
format!(
"axis {name}: an open param with no axis and no bound default — \
bind it with `--axis {name}=<value>` — see `aura sweep <bp> --list-axes`"
)
}
BindError::KindMismatch { knob, expected, got } => {
let knob = crate::axes::wrapped_to_raw_axis(knob);
format!(
"axis {knob}: expected {expected:?}, supplied {got:?}\
see `aura sweep <bp> --list-axes`"
)
}
BindError::UnknownKnob(msg) => msg.clone(),
BindError::DuplicateBinding(name) => format!(
"axis {name}: bound twice — each param takes exactly one axis — \
see `aura sweep <bp> --list-axes`"
),
BindError::EmptyAxis(name) => format!(
"axis {name}: supplies no values — give at least one, e.g. `--axis {name}=2,4`"
),
BindError::EmptyRange(name) => format!(
"axis {name}: the named range is empty — give it at least one value"
),
// A blueprint defect, not an axis usage error: the point passed name
// resolution but its bootstrap failed. Prose frame with the compile
// detail explicitly labelled as internal — per-variant prose for
// CompileError belongs to the graph-build surface, not this boundary.
BindError::Compile(e) => format!(
"the resolved axis point failed to bootstrap — the blueprint is \
defective at this point, re-validate it with `aura graph build` \
(internal detail: {e:?})"
),
}
}
/// Resolve the in-sample winner under the chosen selection objective. `Argmax`
/// defers to the trials-deflation pick (#144). `Plateau` argmaxes the smoothed grid
/// surface — it needs the grid lattice, so a sweep with no lattice (a future random
/// walk-forward producer) is refused rather than silently argmaxed. The metric is
/// always known at the call sites, so a metric error is unreachable (`expect`); the
/// only fallible outcome is the plateau-without-lattice refusal, returned as
/// `Err(message)` for the caller to print and exit 2.
pub fn select_winner(
family: &SweepFamily, metric: &str, select: Selection, lattice: Option<&[usize]>,
) -> Result<(SweepPoint, FamilySelection), String> {
match select {
Selection::Argmax => Ok(optimize_deflated(
family, metric, DEFLATION_N_RESAMPLES, DEFLATION_BLOCK_LEN, DEFLATION_SEED,
).expect("walk-forward metrics are known")),
Selection::Plateau(mode) => match lattice {
Some(lens) => Ok(optimize_plateau(family, lens, metric, mode)
.expect("walk-forward metrics are known")),
None => Err(
"--select plateau requires a grid sweep; a random sweep has no parameter lattice"
.to_string(),
),
},
}
}
/// A constant, zero-compute `RunReport` for [`validate_axis_grid`]'s sweep-terminal
/// probe. `SweepBinder::sweep_with_lattice`'s own `resolve_axes`/arity/kind checks all
/// run BEFORE this closure is invoked per grid point, so its body never influences the
/// validation outcome — only its signature (`Fn(&[Cell]) -> RunReport`) needs to
/// satisfy the terminal, at O(1) cost per point instead of a full member run. Reused
/// (#278) as the discarded slot value for a member run [`catch_member_panic`]
/// contains: the caller always resolves the captured fault and exits before this
/// value is ever rendered or persisted, so its content is equally irrelevant there.
fn axis_grid_probe_report() -> RunReport {
RunReport {
manifest: RunManifest {
commit: String::new(),
params: Vec::new(),
defaults: Vec::new(),
window: (Timestamp(0), Timestamp(0)),
seed: 0,
broker: "wf-axis-preflight-placeholder".to_string(),
selection: None,
instrument: None,
topology_hash: None,
project: None,
},
metrics: RunMetrics { total_pips: 0.0, max_drawdown: 0.0, bias_sign_flips: 0, r: None },
}
}
/// The LOWEST enumeration index's captured member-panic message, found by
/// walking `family.points` — guaranteed by `assemble_sweep`'s own
/// `collect()` to be in grid-enumeration order regardless of which worker
/// finished first (C1) — for the first point whose params match a captured
/// fault. Thread-order-independent by construction (mirrors the lowest-
/// index-fault convention `aura-campaign::exec::run_members` establishes for
/// the identical #272 seam), without needing a separate numeric-index
/// side-channel: the returned family already carries the points in order.
fn lowest_point_fault(family: &SweepFamily, faults: Mutex<Vec<(Vec<Cell>, String)>>) -> Option<String> {
let captured = faults.into_inner().expect("fault capture lock");
family
.points
.iter()
.find_map(|pt| captured.iter().find(|(p, _)| *p == pt.params).map(|(_, m)| m.clone()))
}
/// The LOWEST seed's captured member-panic message (#278), found by walking
/// `family.draws` — `monte_carlo`'s own contract guarantees seed-**input**
/// order regardless of thread completion (C1) — for the first draw whose
/// seed matches a captured fault. Thread-order-independent by construction,
/// mirroring [`lowest_point_fault`]'s and `lowest_window_fault`'s identical
/// convention one level over (per-point sweep / per-window walk-forward).
fn lowest_seed_fault(family: &McFamily, faults: Mutex<Vec<(u64, String)>>) -> Option<String> {
let captured = faults.into_inner().expect("fault capture lock");
family
.draws
.iter()
.find_map(|d| captured.iter().find(|(s, _)| *s == d.seed).map(|(_, m)| m.clone()))
}
/// Render + exit(3) on a contained member panic (#278: the deliberate
/// failed-cells exit, C14, never the raw 101 an uncontained panic would
/// yield) — reuses `aura_campaign::member_fault_prose`'s established
/// "a member panicked: …" wording rather than re-wording it, and the
/// `aura: warning: ` class marker (#278 decision) the CLI's own `diag`
/// macro would emit, hand-written here since that macro is presentation-
/// crate-private (#295 boundary) and this fires from `aura-runner`.
fn exit_on_member_panic(msg: &str) -> ! {
eprintln!("aura: warning: {}", member_fault_prose(&MemberFault::Panic(msg.to_string())));
std::process::exit(3);
}
/// Validate the `--axis` grid against `doc`'s wrapped param space WITHOUT running any
/// member (#253). Reuses the SAME strict, erroring axis-name check
/// `blueprint_sweep_over` performs (`override_paths` — single-sourced, so the two
/// paths cannot drift to differently-worded rejections) to derive the #246 override
/// set, then drives the sweep terminal's own `resolve_axes`/arity/kind checks
/// (`SweepBinder::sweep_with_lattice`) with [`axis_grid_probe_report`] standing in for
/// the run closure — no data access, no sim engine tick. Axis resolution is
/// window-agnostic, so the caller derives or passes no window at all.
fn validate_axis_grid(
doc: &str, axes: &[(String, Vec<Scalar>)], raw_space: &[ParamSpec], probe_signal: &Composite,
env: &Env,
) -> Result<(), BindError> {
let overrides = override_paths(axes, raw_space, probe_signal).map_err(BindError::UnknownKnob)?;
let probe = blueprint_axis_probe_reopened(doc, env, &overrides);
let space = probe.param_space();
// #328: `axes` names are RAW (refused at the walkforward dispatch-boundary
// preflight ahead of this call, mirroring the plain sweep verb) — translate
// each onto the ONE wrapped `space` slot it suffix-matches
// (`wrapped_name_of`, the same convention `blueprint_sweep_family` feeds its
// own `SweepBinder`) before this terminal's exact-name resolution.
let mut iter = axes.iter();
let (first_name, first_vals) = iter.next().expect("a blueprint walk-forward declares >= 1 axis");
let mut binder = probe.axis(&crate::axes::wrapped_name_of(first_name, &space), first_vals.clone());
for (n, vals) in iter {
binder = binder.axis(&crate::axes::wrapped_name_of(n, &space), vals.clone());
}
binder.sweep_with_lattice(|_| axis_grid_probe_report()).map(|_| ())
}
/// Sweep a serialized signal `doc` over user-named param-space axes. Structurally it
/// keeps the shape of the retired `r_sma_sweep_family` demo builder (#159), with three
/// deviations. (1) The signal source is
/// `wrap_r(blueprint_from_json(doc))` — a loaded blueprint, not the Rust-built
/// r-sma graph. (2) The signal is RE-loaded from `doc` per member (a `Composite` is
/// `!Clone`, so the throwaway param-space probe and each grid point each reload). (3)
/// The axes are taken verbatim BY NAME (not the four suffix-resolved r-sma knobs):
/// each `(name, vals)` is fed straight to the `SweepBinder`, so an unknown name or a
/// kind mismatch surfaces as the sweep terminal's [`BindError`], rendered to a message
/// string — a named error, never a panic. An axis naming a bound param re-opens it
/// (#246: bound value = default); an axis matching neither space is refused by
/// `override_paths` before any run. Every member manifest carries the shared
/// `topology_hash` of the loaded signal; reduce-mode fold, identical to the retired
/// mirror's default (no-trace) arm.
pub fn blueprint_sweep_family(
doc: &str,
axes: &[(String, Vec<Scalar>)],
data: &DataSource,
env: &Env,
) -> Result<SweepFamily, String> {
// Identity + binding read the AUTHORED doc, raw (no override re-open):
// topology and the resolved role plan are properties of the document, not
// of any one sweep's axis choices.
let probe_signal = blueprint_from_json(doc, &|t| env.resolve(t))
.expect("doc parse-validated at the dispatch boundary; reload is infallible");
// topology_hash's own two-line body, inlined (mirrors member::run_signal_r):
// `content_id_of` over the canonical (#164) blueprint JSON — the CLI shell's
// `topology_hash` helper is the same primitive, kept single-sourced at
// `aura_research`.
let topo = aura_research::content_id_of(
&aura_engine::blueprint_to_json(&probe_signal).expect("a buildable signal serializes"),
);
// Strict binding resolution (name defaults — the verb path carries no
// campaign overrides): the family's open plan and wrap plan in one value.
let binding = crate::binding::resolve_binding(probe_signal.name(), probe_signal.input_roles(), &BTreeMap::new())?;
if matches!(data, DataSource::Synthetic) && !binding.close_only() {
return Err(crate::binding::synthetic_refusal(probe_signal.name(), &binding));
}
let pip = data.pip_size();
let window = data.full_window(env);
// The un-reopened wrapped OPEN space (#246): derives the override set (which
// named axes re-open a bound param) before the real, reopened probe is built —
// probe and per-member reloads must re-open identically so points resolve
// against one space. A name matching neither space is the error here.
let raw_space = blueprint_axis_probe(doc, env).param_space();
let overrides = override_paths(axes, &raw_space, &probe_signal)?;
let probe = blueprint_axis_probe_reopened(doc, env, &overrides);
let space = probe.param_space();
// The doc is parse-validated at the dispatch boundary (with file-path context),
// so every reload here is infallible: the builder has a single error contract —
// the `BindError` returned by the sweep terminal — and no hidden process exit.
// Member reloads re-open the SAME override set derived above, so every member
// resolves its axes against the identical (reopened) param space the probe used.
let reload = |d: &str| {
reopen_all(
blueprint_from_json(d, &|t| env.resolve(t))
.expect("doc parse-validated at the dispatch boundary; reload is infallible"),
&overrides,
)
};
// seed the named axes: `axes` names are RAW (#328 — refused at intake before
// this fn ever runs), so each is translated to the ONE wrapped `space` slot
// it suffix-matches (`raw_matches_wrapped`, the same convention
// `override_paths` just resolved the override set through) before feeding
// `Composite::axis`/`SweepBinder::axis`, which still key by the exact
// wrapped `param_space()` name. `resolve_axes` name- and kind-checks the
// translated axes at the sweep terminal, so an UnknownKnob / KindMismatch
// is returned, not panicked (translation cannot itself introduce one: every
// incoming name was already validated raw-or-bound at the intake / override
// preflight above).
let wrapped_name_of = |raw: &str| -> String {
space
.iter()
.find(|p| crate::axes::raw_matches_wrapped(raw, &p.name))
.map(|p| p.name.clone())
.unwrap_or_else(|| raw.to_string())
};
let mut iter = axes.iter();
let (first_name, first_vals) = iter.next().expect("a blueprint sweep declares >= 1 axis");
let mut binder = probe.axis(&wrapped_name_of(first_name), first_vals.clone());
for (n, vals) in iter {
binder = binder.axis(&wrapped_name_of(n), vals.clone());
}
// manifest.params records RAW names (#328): a name-only reshaping of `space`
// (order/kind untouched) fed to `run_blueprint_member`, which only zips it
// against `point` BY POSITION (`zip_params`) — never re-resolves by name —
// so renaming here is purely the manifest's own namespace, with no effect
// on member resolution.
let manifest_space: Vec<ParamSpec> = space
.iter()
.map(|p| ParamSpec { name: crate::axes::wrapped_to_raw_axis(&p.name).to_string(), kind: p.kind })
.collect();
// #278: `run_blueprint_member` runs bare here (no #272 fault boundary of its
// own), so a member-compile panic (e.g. an `Sma::new` length assert) would
// otherwise unwind straight through this sweep to an uncaught exit 101 —
// contained the same way the real/campaign path contains it, via
// `catch_member_panic` + a per-point capture, resolved to the lowest
// enumeration index's message after the sweep joins (thread-order-
// independent, C1).
let faults: Mutex<Vec<(Vec<Cell>, String)>> = Mutex::new(Vec::new());
let family = binder
.sweep(|point| {
// fresh per-member graph (Composite is !Clone, reload per member) run through
// the shared reduce-mode member path — the same fn reproduction re-runs.
match catch_member_panic(|| {
run_blueprint_member(reload(doc), point, &manifest_space, data.run_sources(env, &binding.columns()), window, 0, pip, &topo, env, DEFAULT_STOP, &binding, &[], NO_INSTRUMENT_CONTEXT)
}) {
Ok(report) => report,
Err(msg) => {
faults.lock().expect("fault capture lock").push((point.to_vec(), msg));
axis_grid_probe_report()
}
}
})
// render the sweep terminal's BindError to prose (#247), the fn's String error
// contract — never the raw Debug struct.
.map_err(|e| render_bind_error(&e))?;
if let Some(msg) = lowest_point_fault(&family, faults) {
exit_on_member_panic(&msg);
}
Ok(family)
}
/// Sweep the LOADED blueprint over the user `--axis` grid on an in-sample window
/// `[from,to]` — the windowed, lattice-carrying twin of
/// `blueprint_sweep_family`. `sweep_with_lattice` gives the grid lattice `--select
/// plateau` needs. An unknown/kind-mismatched axis surfaces as `BindError` at the
/// sweep terminal (no panic, no hidden exit) for the caller to render. An axis
/// naming a bound param re-opens it (#246: bound value = default, same
/// `override_paths`/`reopen_all` recipe as `blueprint_sweep_family` — this is
/// its walk-forward in-sample twin); an axis matching neither space is refused
/// (wrapped as `BindError::UnknownKnob`, the honest replacement for the retired
/// "fully bound; nothing to sweep" refusal) before any member runs.
///
/// The returned `Option<String>` (#278) is this fn's OWN resolved member-panic
/// capture: this fn's nested sweep runs inside `blueprint_walkforward_family`'s
/// per-window parallel closure, so a contained member panic here must NOT
/// `eprintln!`+`exit` on the spot (that would race across windows, the exact
/// `#177` duplicated-rejection class the dispatch-boundary axis pre-flight
/// above already avoids) — captured internally, resolved to the lowest-index
/// message via [`lowest_point_fault`] before returning, for the caller to push
/// onto its own per-window fault list only after every window has joined.
pub fn blueprint_sweep_over(
doc: &str, axes: &[(String, Vec<Scalar>)], from: Timestamp, to: Timestamp, data: &DataSource,
env: &Env, binding: &ResolvedBinding,
) -> Result<(SweepFamily, Vec<usize>, Option<String>), BindError> {
let reload = |d: &str| {
blueprint_from_json(d, &|t| env.resolve(t))
.expect("doc parse-validated at the dispatch boundary; reload is infallible")
};
let pip = data.pip_size();
let probe_signal = reload(doc);
// topology_hash's own two-line body, inlined (see `blueprint_sweep_family`).
let topo = aura_research::content_id_of(
&aura_engine::blueprint_to_json(&probe_signal).expect("a buildable signal serializes"),
);
// The un-reopened wrapped OPEN space (#246), against a raw probe + raw strategy
// load: derives the override set (which named axes re-open a bound param) before
// the reopened probe is built — probe and per-member reloads must re-open
// identically so points resolve against one space, exactly like
// `blueprint_sweep_family`.
let raw_space = blueprint_axis_probe(doc, env).param_space();
let overrides = override_paths(axes, &raw_space, &probe_signal).map_err(BindError::UnknownKnob)?;
let probe = blueprint_axis_probe_reopened(doc, env, &overrides);
let space = probe.param_space();
// #328: `axes` names are RAW (refused at the walkforward dispatch-boundary
// preflight ahead of the caller, mirroring the plain sweep verb) —
// translate each onto the ONE wrapped `space` slot it suffix-matches
// (`wrapped_name_of`, the same convention `blueprint_sweep_family` feeds
// its own `SweepBinder`) before this terminal's exact-name resolution.
let mut iter = axes.iter();
let (first_name, first_vals) = iter.next().expect("a blueprint walk-forward declares >= 1 axis");
let mut binder = probe.axis(&crate::axes::wrapped_name_of(first_name, &space), first_vals.clone());
for (n, vals) in iter {
binder = binder.axis(&crate::axes::wrapped_name_of(n, &space), vals.clone());
}
let faults: Mutex<Vec<(Vec<Cell>, String)>> = Mutex::new(Vec::new());
let (family, lattice) = binder.sweep_with_lattice(|point| {
let sources = data.windowed_sources(from, to, env, &binding.columns());
let window = window_of(&sources).expect("non-empty in-sample window");
match catch_member_panic(|| {
run_blueprint_member(reopen_all(reload(doc), &overrides), point, &space, sources, window, 0, pip, &topo, env, DEFAULT_STOP, binding, &[], NO_INSTRUMENT_CONTEXT)
}) {
Ok(report) => report,
Err(msg) => {
faults.lock().expect("fault capture lock").push((point.to_vec(), msg));
axis_grid_probe_report()
}
}
})?;
let fault = lowest_point_fault(&family, faults);
Ok((family, lattice, fault))
}
/// Run the winner params over an out-of-sample window `[from,to]` on the loaded
/// blueprint. The reduce-mode member
/// (`run_blueprint_member`) retains R-metrics, not a raw pip curve, so the stitching
/// segment is empty (an empty segment leaves the stitched curve unbroken). `overrides`
/// (#246) is the SAME family-wide set `blueprint_walkforward_family` derived once and
/// resolved `space`/`params` against — the OOS reload must re-open it too, or a
/// bound-param axis's winner point (kind-checked against the REOPENED space) fails
/// `bootstrap_with_cells`'s arity check against this still-closed reload.
#[allow(clippy::too_many_arguments)]
pub fn run_oos_blueprint(
doc: &str, params: &[Cell], space: &[ParamSpec], from: Timestamp, to: Timestamp,
topo: &str, data: &DataSource, env: &Env, binding: &ResolvedBinding,
overrides: &[String],
) -> (Vec<(Timestamp, f64)>, RunReport) {
let reload = reopen_all(
blueprint_from_json(doc, &|t| env.resolve(t))
.expect("doc parse-validated at the dispatch boundary; reload is infallible"),
overrides,
);
let pip = data.pip_size();
let sources = data.windowed_sources(from, to, env, &binding.columns());
let window = window_of(&sources).expect("non-empty out-of-sample window");
let report = run_blueprint_member(reload, params, space, sources, window, 0, pip, topo, env, DEFAULT_STOP, binding, &[], NO_INSTRUMENT_CONTEXT);
(Vec::new(), report)
}
/// A discarded placeholder [`WindowRun`] for a window whose IS refit or OOS
/// run was contained after a member panic (#278): `chosen_params` matches
/// `space`'s arity with inert zero cells (the `assemble_walk_forward` arity
/// invariant), `oos_equity` is empty (an empty segment leaves the stitched
/// curve unbroken, same convention as a genuinely equity-less window), and
/// the report reuses [`axis_grid_probe_report`]'s zero-cost placeholder.
/// Never rendered: the caller always resolves the captured window fault and
/// exits before touching a faulted window's run.
fn placeholder_window_run(space: &[ParamSpec]) -> WindowRun {
WindowRun {
chosen_params: space.iter().map(|_| Cell::from_i64(0)).collect(),
oos_equity: Vec::new(),
oos_report: axis_grid_probe_report(),
}
}
/// The LOWEST roll-order window's captured member-panic message (#278),
/// found by walking `result.windows` — guaranteed by `assemble_walk_forward`
/// to be in roll order regardless of thread completion (C1) — for the first
/// window whose bounds match a captured fault. Thread-order-independent by
/// construction, mirroring [`lowest_point_fault`]'s identical convention one
/// level down (per-point within one window's IS sweep) and
/// `aura-campaign::exec`'s own lowest-index walk-forward fault attribution.
fn lowest_window_fault(result: &WalkForwardResult, faults: Mutex<Vec<(WindowBounds, String)>>) -> Option<String> {
let captured = faults.into_inner().expect("fault capture lock");
result
.windows
.iter()
.find_map(|w| captured.iter().find(|(b, _)| *b == w.bounds).map(|(_, m)| m.clone()))
}
/// The loaded-blueprint IS-refit walk-forward: per IS window, re-optimize the
/// blueprint over the user `--axis` grid, select by `sqn_normalized`, run the
/// winner OOS, reusing the generic `walk_forward` driver + `select_winner`;
/// only the per-window sweep/OOS source the loaded blueprint. In-closure errors
/// (a bad `--axis`) `exit(2)` with the sweep terminal's message.
pub fn blueprint_walkforward_family(
doc: &str, axes: &[(String, Vec<Scalar>)], data: &DataSource, select: Selection,
env: &Env,
) -> WalkForwardResult {
let span = data.wf_full_span(env);
let (is_len, oos_len, step) = data.wf_window_sizes();
let roller = match WindowRoller::new(span, is_len, oos_len, step, RollMode::Rolling) {
Ok(r) => r,
Err(e) => {
eprintln!("aura: walk-forward window too short for one IS+OOS span: {e:?}");
std::process::exit(2);
}
};
let probe_signal = blueprint_from_json(doc, &|t| env.resolve(t))
.expect("doc parse-validated at the dispatch boundary; reload is infallible");
// topology_hash's own two-line body, inlined (see `blueprint_sweep_family`).
let topo = aura_research::content_id_of(
&aura_engine::blueprint_to_json(&probe_signal).expect("a buildable signal serializes"),
);
// The un-reopened wrapped OPEN space (#246), against a raw probe + raw strategy
// load: derives the override set ONCE for the whole family — every per-window
// sweep AND the OOS reload re-open the SAME set, mirroring
// `blueprint_sweep_family`/`blueprint_sweep_over`. The SILENT variant
// (`wrapped_bound_overrides_of`, not the validating `override_paths`): an axis
// matching neither space is simply not an override here — the strict check + its
// established error message stay single-sourced in `blueprint_sweep_over`'s own
// pre-flight call below, so this derivation cannot double-validate with a
// differently-worded rejection.
let axis_names: Vec<String> = axes.iter().map(|(n, _)| n.clone()).collect();
let raw_space = blueprint_axis_probe(doc, env).param_space();
let overrides = wrapped_bound_overrides_of(&axis_names, &raw_space, &probe_signal);
let space = blueprint_axis_probe_reopened(doc, env, &overrides).param_space();
// Strict binding resolution, once per family; refusal is the established
// `aura: ` + exit-1 register (the roller's usage refusals stay exit 2).
let binding = crate::binding::resolve_binding(probe_signal.name(), probe_signal.input_roles(), &BTreeMap::new())
.unwrap_or_else(|m| {
eprintln!("aura: {m}");
std::process::exit(1);
});
if matches!(data, DataSource::Synthetic) && !binding.close_only() {
eprintln!("aura: {}", crate::binding::synthetic_refusal(probe_signal.name(), &binding));
std::process::exit(1);
}
// Validate the `--axis` grid ONCE at the dispatch boundary, mirroring `aura sweep`
// (which resolves its axes a single time before any member runs). `walk_forward` fans
// the per-window closure out across the windows in parallel, so a `BindError` raised
// *inside* the closure would `eprintln!`+`exit(2)` from several windows before any one
// exit lands — a racy, duplicated rejection (#177). Axis resolution is window-agnostic
// (#253): `validate_axis_grid` resolves the SAME grid the sweep terminal would, without
// running a single member — no IS window (or a second roller) is needed here at all.
if let Err(e) = validate_axis_grid(doc, axes, &raw_space, &probe_signal, env) {
eprintln!("aura: {}", render_bind_error(&e));
std::process::exit(2);
}
// #278: `blueprint_sweep_over`'s inner sweep and `run_oos_blueprint`'s single
// member run both drive `run_blueprint_member` bare, so either could
// otherwise unwind an uncaught member-compile panic straight through this
// window closure. Both are contained (`blueprint_sweep_over` resolves its
// own capture and returns it; `run_oos_blueprint` via `catch_member_panic`
// directly) and captured PER WINDOW here rather than printed on the spot —
// this closure runs across windows in parallel (`walk_forward`), so an
// inline `eprintln!`+`exit` would race the same way the axis pre-flight
// above was written to avoid (#177). The lowest roll-order window's fault
// is resolved once, after every window has joined.
let window_faults: Mutex<Vec<(WindowBounds, String)>> = Mutex::new(Vec::new());
let result = walk_forward(roller, space.clone(), |w: WindowBounds| {
let (is_family, lattice, is_fault) = blueprint_sweep_over(doc, axes, w.is.0, w.is.1, data, env, &binding)
.expect("axes validated in the dispatch-boundary pre-flight");
if let Some(msg) = is_fault {
window_faults.lock().expect("fault capture lock").push((w, msg));
return placeholder_window_run(&space);
}
let (best, selection) = match select_winner(&is_family, WINNER_SELECTION_METRIC, select, Some(&lattice)) {
Ok(v) => v,
Err(msg) => { eprintln!("aura: {msg}"); std::process::exit(2); }
};
match catch_member_panic(|| {
run_oos_blueprint(doc, &best.params, &space, w.oos.0, w.oos.1, &topo, data, env, &binding, &overrides)
}) {
Ok((oos_equity, mut oos_report)) => {
oos_report.manifest.selection = Some(selection);
WindowRun { chosen_params: best.params, oos_equity, oos_report }
}
Err(msg) => {
window_faults.lock().expect("fault capture lock").push((w, msg));
placeholder_window_run(&space)
}
}
});
if let Some(msg) = lowest_window_fault(&result, window_faults) {
exit_on_member_panic(&msg);
}
result
}
/// A fresh seeded synthetic price walk for one Monte-Carlo draw — `blueprint_mc_family`'s
/// pattern (a distinct realization per seed). A FIXED `SyntheticSpec` shared by the
/// `aura mc <blueprint.json>` persist path AND the reproduce MonteCarlo branch, so the
/// seed->walk reconstruction is bit-exact (C1). Length 60 comfortably warms the loaded
/// r-sma graph (SMA slow=4 + the len-3 vol stop) so draws produce differing trades.
/// A fresh seeded synthetic price walk for one Monte-Carlo draw (a distinct
/// realization per seed). A FIXED `SyntheticSpec` shared by the reproduce
/// MonteCarlo branch (`aura_runner::reproduce`, its only remaining
/// production caller — #319 retired the family builder that used to mint
/// such a family), so the seed->walk reconstruction is bit-exact (C1).
/// Length 60 comfortably warms the loaded r-sma graph (SMA slow=4 + the
/// len-3 vol stop) so draws produce differing trades.
pub fn synthetic_walk_sources(seed: u64) -> Vec<Box<dyn aura_engine::Source>> {
let spec = SyntheticSpec { start: 1.0, len: 60, step: 1 };
vec![Box::new(spec.source(seed))]
}
/// Build a Monte-Carlo family from a loaded CLOSED signal blueprint: run the fixed
/// blueprint across `n_seeds` seeds, each seed drawing a distinct synthetic walk. The
/// blueprint must be CLOSED (empty wrapped `param_space`) — MC binds no axis, so a free
/// knob has no binder; an OPEN blueprint yields a named `Err` (exit-free like the sibling
/// [`blueprint_sweep_family`]: the IO wrapper `run_blueprint_mc` renders it to stderr +
/// exit 2) before any run, pre-empting the `compile_with_params` arity panic. Each draw
/// runs the shared reduce-mode member path (`run_blueprint_member`, the same fn reproduce
/// re-runs), so reproduction is bit-identical (C1); every member carries the shared
/// `topology_hash`.
pub fn blueprint_mc_family(
doc: &str, n_seeds: u64, data: &DataSource, env: &Env,
) -> Result<McFamily, String> {
let reload = |d: &str| {
blueprint_from_json(d, &|t| env.resolve(t))
.expect("doc parse-validated at the dispatch boundary; reload is infallible")
};
let probe_signal = reload(doc);
// topology_hash's own two-line body, inlined (see `blueprint_sweep_family`).
let topo = aura_research::content_id_of(
&aura_engine::blueprint_to_json(&probe_signal).expect("a buildable signal serializes"),
);
// Strict binding resolution (name defaults — mc's synthetic family binds
// no campaign overrides); the exit-free Err contract of this builder.
let binding = crate::binding::resolve_binding(probe_signal.name(), probe_signal.input_roles(), &BTreeMap::new())?;
if !binding.close_only() {
// MC draws ALWAYS run the seeded synthetic close walk (real-data mc
// routes through the campaign sugar and never reaches this builder).
return Err(crate::binding::synthetic_refusal(probe_signal.name(), &binding));
}
let pip = data.pip_size();
// probe the wrapped param_space (the same probe the sweep resolves against);
// MC needs it empty. `blueprint_axis_probe` is the single source of that wrap.
let space = blueprint_axis_probe(doc, env).param_space();
if !space.is_empty() {
// Exit-free like blueprint_sweep_family: the builder's single error contract is this
// returned message (no hidden process exit), so the rejection is unit-testable; the IO
// wrapper run_blueprint_mc renders it to stderr + exit 2 at the boundary.
return Err(format!(
"mc requires a closed blueprint (no free parameters); {} free knob(s) — \
bind them or use `aura sweep --axis`",
space.len()
));
}
// Closed blueprint -> an empty base point (as `aura run <blueprint.json>`); the MC
// draws vary the SEED, not a tuning param (C12 axis 4). Delegate the disjoint C1 draws
// to the shared `monte_carlo` helper — it runs them in parallel across sims (invariant 1),
// deterministic in seed-input order. Each draw
// re-runs the shared reduce-mode member path over its own seeded synthetic walk.
let seeds: Vec<u64> = (1..=n_seeds).collect();
let base_point: Vec<Scalar> = Vec::new();
// #278: `run_blueprint_member` ran bare here, the one family builder without
// the #272 fault boundary its sweep/walk-forward siblings gained in 51096a3
// — a member-compile panic (e.g. an `Sma::new` length assert) would otherwise
// unwind straight through `monte_carlo`'s `run_indexed` to an uncaught exit
// 101. Contained the same way: `catch_member_panic` + a per-seed capture,
// resolved to the LOWEST seed's message after the join (thread-order-
// independent, C1) via `lowest_seed_fault`.
let faults: Mutex<Vec<(u64, String)>> = Mutex::new(Vec::new());
let family = monte_carlo(&base_point, &seeds, |seed, _base| {
let sources = synthetic_walk_sources(seed);
let window = window_of(&sources).expect("non-empty synthetic walk");
match catch_member_panic(|| {
run_blueprint_member(reload(doc), &[], &space, sources, window, seed, pip, &topo, env, DEFAULT_STOP, &binding, &[], NO_INSTRUMENT_CONTEXT)
}) {
Ok(report) => report,
Err(msg) => {
faults.lock().expect("fault capture lock").push((seed, msg));
axis_grid_probe_report()
}
}
});
// The captured fault must win BEFORE the vacuous-mc guard below: a faulted
// draw's placeholder report is metrics-identical across every faulted seed,
// so a run with >= 2 faulted seeds (or one faulted + one real draw sharing
// its placeholder's zero metrics) could otherwise trip the vacuous refusal
// instead of surfacing the real member fault.
if let Some(msg) = lowest_seed_fault(&family, faults) {
exit_on_member_panic(&msg);
}
// Silent-vacuous MC guard (refuse-don't-guess, C10): with >= 2 seeds, if every draw's
// metrics are bit-identical to the first, no seed reached a distinguishable realization —
// the strategy never warmed over the fixed synthetic walk (e.g. a lookback as deep as the
// walk is long), so the "distribution" is a single point masquerading as a family: a wrong
// result with no error. Compare `metrics`, not the whole `RunReport` — the manifest's
// `seed` differs per draw by construction, so a whole-report compare could never detect the
// collapse; the metrics are the realization the seed is meant to move. A single-draw MC
// (n == 1) is trivially "all identical" and is NOT this cross-seed condition, so it passes.
if family.draws.len() >= 2
&& family
.draws
.iter()
.all(|d| d.report.metrics == family.draws[0].report.metrics)
{
return Err(
"mc is vacuous: every seed produced an identical result — the strategy never warmed \
over the synthetic walk, so no seed reached a distinguishable realization; use a \
shallower-lookback blueprint or a longer walk"
.to_string(),
);
}
Ok(family)
}
@@ -185,7 +185,6 @@ fn the_shell_defines_no_domain_modules_and_no_lib_target() {
"render.rs",
"research_docs.rs",
"scaffold.rs",
"verb_sugar.rs",
],
"C28 (#295): the shell holds argv/dispatch, argv->document translation, \
and presentation only — a new module needs a library home"