refactor(cli): wrap_r declares its root roles from the resolved binding (#231 task 3)

Behaviour-preserving threading: wrap_r's hard-wired
source_role("price") weld is gone — root roles now come from a
ResolvedBinding, one per entry in canonical column order, each fed into
the signal's same-named port; the guaranteed close entry always feeds
broker + executor (whose 'price' is a node-schema port name, not a
role). Every existing blueprint resolves to the identical single-price
plan, so the entire suite is the gate: green unchanged.

Threaded through all callers: run_signal_r, run_blueprint_member (+ its
production and test callers), blueprint_axis_probe (lenient probe
binding), reproduce_family_in, the sweep/oos/walkforward/mc family
builders, CliMemberRunner::run_member, persist_campaign_traces.

Vocabulary role names use their static form at the port seam; only an
override-renamed role leaks its name once per graph build (post-review
polish capping the per-member leak).

Verified: full workspace suite green unchanged, clippy -D warnings
clean.

refs #231
This commit is contained in:
2026-07-11 02:27:05 +02:00
parent 595c98b264
commit 99a7f6603c
3 changed files with 139 additions and 40 deletions
+17
View File
@@ -31,6 +31,23 @@ const RESERVED_CLOSE_ROLE: &str = "close";
pub(crate) const COLUMN_VOCABULARY: &str = pub(crate) const COLUMN_VOCABULARY: &str =
"open, high, low, close (alias: price), spread, volume"; "open, high, low, close (alias: price), spread, volume";
/// The `&'static str` form of a vocabulary role name, when it is one — the
/// common case at the wrap seam, so declaring a port for a vocabulary-named
/// role needs no per-build leak; only an override-renamed (non-vocabulary)
/// role falls back to leaking its name.
pub(crate) fn static_role_name(name: &str) -> Option<&'static str> {
match name {
"open" => Some("open"),
"high" => Some("high"),
"low" => Some("low"),
"close" => Some("close"),
"price" => Some("price"),
"spread" => Some("spread"),
"volume" => Some("volume"),
_ => None,
}
}
/// The closed role-name -> archive-column map (`price` aliases `close`). /// The closed role-name -> archive-column map (`price` aliases `close`).
pub(crate) fn column_for_role(name: &str) -> Option<M1Field> { pub(crate) fn column_for_role(name: &str) -> Option<M1Field> {
match name { match name {
+20 -7
View File
@@ -14,6 +14,7 @@
//! submodule idiom: main.rs is the crate root, so its private items are //! submodule idiom: main.rs is the crate root, so its private items are
//! visible to child modules without promotion. //! visible to child modules without promotion.
use std::collections::BTreeMap;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::{mpsc, Arc}; use std::sync::{mpsc, Arc};
@@ -241,6 +242,15 @@ impl MemberRunner for CliMemberRunner<'_> {
let space = crate::blueprint_axis_probe(&cell.blueprint_json, self.env).param_space(); let space = crate::blueprint_axis_probe(&cell.blueprint_json, self.env).param_space();
let point = bind_axes(&space, &cell.strategy_id, params)?; let point = bind_axes(&space, &cell.strategy_id, params)?;
// The member's blueprint + its resolved input binding (name defaults;
// the campaign's data.bindings overrides thread through in the
// DataSection task). A refusal is a member fault, never a process exit.
let signal = blueprint_from_json(&cell.blueprint_json, &|t| self.env.resolve(t))
.expect("stored blueprint passed the referential gate; reload is infallible");
let binding =
crate::binding::resolve_binding(&cell.strategy_id, signal.input_roles(), &BTreeMap::new())
.map_err(MemberFault::Bind)?;
// Real windowed data — geometry BEFORE bar data (the shipped pre-data // Real windowed data — geometry BEFORE bar data (the shipped pre-data
// refusal order of `open_real_source`), both as member faults. // refusal order of `open_real_source`), both as member faults.
let geo = instrument_geometry(&self.server, &cell.instrument).ok_or_else(|| { let geo = instrument_geometry(&self.server, &cell.instrument).ok_or_else(|| {
@@ -279,12 +289,10 @@ impl MemberRunner for CliMemberRunner<'_> {
}); });
} }
// The shipped member recipe (reload — a Composite is !Clone — wrap, // The shipped member recipe (wrap, bind, run, summarize):
// bind, run, summarize): `run_blueprint_member` verbatim. Seed 0 // `run_blueprint_member` verbatim. Seed 0 (seed-free real-data runs),
// (seed-free real-data runs), topology_hash = the strategy's content // topology_hash = the strategy's content id, project provenance
// id, project provenance stamped inside the helper. // stamped inside the helper.
let signal = blueprint_from_json(&cell.blueprint_json, &|t| self.env.resolve(t))
.expect("stored blueprint passed the referential gate; reload is infallible");
let stop = stop_rule_for_regime(cell.regime); let stop = stop_rule_for_regime(cell.regime);
let mut report = crate::run_blueprint_member( let mut report = crate::run_blueprint_member(
signal, signal,
@@ -297,6 +305,7 @@ impl MemberRunner for CliMemberRunner<'_> {
&cell.strategy_id, &cell.strategy_id,
self.env, self.env,
stop, stop,
&binding,
); );
report.manifest.instrument = Some(cell.instrument.clone()); report.manifest.instrument = Some(cell.instrument.clone());
Ok(report) Ok(report)
@@ -827,6 +836,10 @@ pub(crate) fn persist_campaign_traces(
} }
let signal = blueprint_from_json(blueprint_json, &|t| env.resolve(t)) let signal = blueprint_from_json(blueprint_json, &|t| env.resolve(t))
.expect("stored blueprint passed the referential gate; reload is infallible"); .expect("stored blueprint passed the referential gate; reload is infallible");
// Same name-default binding the member ran under (campaign
// overrides thread through in the DataSection task).
let binding =
crate::binding::resolve_binding(&cell_rec.strategy, signal.input_roles(), &BTreeMap::new())?;
// The cell's OWN regime (#219/#212), not the hardcoded default: the // The cell's OWN regime (#219/#212), not the hardcoded default: the
// recorded member ran under `cell_rec.regime`, bound through the same // recorded member ran under `cell_rec.regime`, bound through the same
// `stop_rule_for_regime` helper `CliMemberRunner::run_member` uses, so // `stop_rule_for_regime` helper `CliMemberRunner::run_member` uses, so
@@ -837,7 +850,7 @@ pub(crate) fn persist_campaign_traces(
let (tx_ex, rx_ex) = mpsc::channel(); let (tx_ex, rx_ex) = mpsc::channel();
let (tx_r, rx_r) = mpsc::channel(); let (tx_r, rx_r) = mpsc::channel();
let (tx_req, rx_req) = mpsc::channel(); let (tx_req, rx_req) = mpsc::channel();
let mut h = crate::wrap_r(signal, tx_eq, tx_ex, tx_r, tx_req, stop, false, geo.pip_size) let mut h = crate::wrap_r(signal, tx_eq, tx_ex, tx_r, tx_req, stop, false, geo.pip_size, &binding)
.bootstrap_with_cells(&point) .bootstrap_with_cells(&point)
.expect("the member's point re-bootstraps (it already ran this realization)"); .expect("the member's point re-bootstraps (it already ran this realization)");
let sources: Vec<Box<dyn aura_engine::Source>> = vec![Box::new(source)]; let sources: Vec<Box<dyn aura_engine::Source>> = vec![Box::new(source)];
+102 -33
View File
@@ -68,7 +68,7 @@ use aura_std::{Bias, Ema, Sma};
use aura_std::{RollingMax, RollingMin, Sub}; use aura_std::{RollingMax, RollingMin, Sub};
use std::sync::mpsc; use std::sync::mpsc;
use std::sync::LazyLock; use std::sync::LazyLock;
use std::collections::HashSet; use std::collections::{BTreeMap, HashSet};
use clap::{Args, Parser, Subcommand}; use clap::{Args, Parser, Subcommand};
/// The pip size the built-in *synthetic* families (sweep/walkforward/mc over a /// The pip size the built-in *synthetic* families (sweep/walkforward/mc over a
@@ -1111,7 +1111,14 @@ fn reproduce_family_in(
let (tx_r, _) = mpsc::channel(); let (tx_r, _) = mpsc::channel();
let (tx_req, _) = mpsc::channel(); let (tx_req, _) = mpsc::channel();
let stop = stop_rule_from_params(&stored.manifest.params); let stop = stop_rule_from_params(&stored.manifest.params);
let space = wrap_r(reload(), tx_eq, tx_ex, tx_r, tx_req, stop, true, SYNTHETIC_PIP_SIZE).param_space(); // The member's binding, re-derived from the stored blueprint's own
// input roles (name defaults — family manifests carry no overrides).
let binding = binding::resolve_binding(&hash, reload().input_roles(), &BTreeMap::new())
.unwrap_or_else(|m| {
eprintln!("aura: {m}");
std::process::exit(1);
});
let space = wrap_r(reload(), tx_eq, tx_ex, tx_r, tx_req, stop, true, SYNTHETIC_PIP_SIZE, &binding).param_space();
let point = point_from_params(&space, &stored.manifest.params); let point = point_from_params(&space, &stored.manifest.params);
// A MonteCarlo member carries no tuning params (the params-join is empty), so its // A MonteCarlo member carries no tuning params (the params-join is empty), so its
// reproduce line would print a BLANK member label; the seed IS its realization // reproduce line would print a BLANK member label; the seed IS its realization
@@ -1172,6 +1179,7 @@ fn reproduce_family_in(
&hash, &hash,
env, env,
stop, stop,
&binding,
); );
outcomes.push((label, rerun.metrics == stored.metrics)); outcomes.push((label, rerun.metrics == stored.metrics));
} }
@@ -1265,11 +1273,14 @@ fn topology_hash(signal: &Composite) -> String {
content_id(&blueprint_to_json(signal).expect("a buildable signal serializes")) content_id(&blueprint_to_json(signal).expect("a buildable signal serializes"))
} }
/// Wrap a `signal` composite (a `price`→`bias` leg) in the R run /// Wrap a `signal` composite (a roles→`bias` leg) in the R run
/// scaffolding: pip broker, the per-tap recorders, the vol-stop RiskExecutor, the /// scaffolding: pip broker, the per-tap recorders, the vol-stop RiskExecutor, the
/// r_equity / cost legs. The signal is nested and its `price`/`bias` boundary is /// r_equity / cost legs. The signal is nested; its root roles come from the
/// wired across; a serialized signal loaded via `blueprint_from_json` runs through /// resolved `binding` (one per entry, canonical column order — the same order the
/// exactly the scaffolding the Rust-built signal does. /// callers open real columns in, so role i receives source i), and the binding's
/// guaranteed close entry always feeds the broker/executor pair. A serialized
/// signal loaded via `blueprint_from_json` runs through exactly the scaffolding
/// the Rust-built signal does.
#[allow(clippy::type_complexity, clippy::too_many_arguments)] #[allow(clippy::type_complexity, clippy::too_many_arguments)]
fn wrap_r( fn wrap_r(
signal: Composite, signal: Composite,
@@ -1280,9 +1291,10 @@ fn wrap_r(
stop: StopRule, stop: StopRule,
reduce: bool, reduce: bool,
pip_size: f64, pip_size: f64,
binding: &binding::ResolvedBinding,
) -> Composite { ) -> Composite {
let mut g = GraphBuilder::new("r_sma"); let mut g = GraphBuilder::new("r_sma");
// SMA-cross signal → Bias, nested as a serializable `price`→`bias` leg. // The strategy signal → Bias, nested as a serializable roles→`bias` leg.
let sig = g.add(BlueprintNode::Composite(signal)); let sig = g.add(BlueprintNode::Composite(signal));
// pip branch (verbatim from the retired `sample_blueprint_with_sinks`, #159). // pip branch (verbatim from the retired `sample_blueprint_with_sinks`, #159).
let broker = g.add(SimBroker::builder(pip_size)); let broker = g.add(SimBroker::builder(pip_size));
@@ -1313,13 +1325,32 @@ fn wrap_r(
} else { } else {
g.add(Recorder::builder(PM_RECORD_KINDS.to_vec(), Firing::Any, tx_r)) g.add(Recorder::builder(PM_RECORD_KINDS.to_vec(), Firing::Any, tx_r))
}; };
let price = g.source_role("price", ScalarKind::F64); // Root roles come from the resolved binding, one per entry in canonical
let price_targets = vec![ // column order (the C4 merge tie-break order the callers open columns in).
sig.input("price"), // The binding guarantees a close entry, which always feeds the broker and
broker.input("price"), // the executor — "price" below is their node-schema PORT name, not a role
exec.input("price"), // name. For a single-`price` signal this declares exactly the old weld:
]; // one role, targets [sig, broker, exec] (feed calls append targets).
g.feed(price, price_targets); let mut close_handle = None;
for entry in binding.entries() {
let role = g.source_role(&entry.role, binding::column_kind(entry.column));
if entry.feeds_signal {
// `NodeHandle::input` takes a `&'static str` port name (the fluent
// `GraphBuilder`'s authoring-time contract, builder.rs) but the
// binding's role names are dynamic (loaded from a blueprint at
// runtime). Vocabulary names use their static form; only an
// override-renamed role leaks its name (once per graph build,
// never per-tick).
let port: &'static str = binding::static_role_name(&entry.role)
.unwrap_or_else(|| Box::leak(entry.role.clone().into_boxed_str()));
g.feed(role, [sig.input(port)]);
}
if entry.column == aura_ingest::M1Field::Close && close_handle.is_none() {
close_handle = Some(role);
}
}
let close = close_handle.expect("ResolvedBinding guarantees a close entry");
g.feed(close, [broker.input("price"), exec.input("price")]);
g.connect(sig.output("bias"), broker.input("exposure")); g.connect(sig.output("bias"), broker.input("exposure"));
g.connect(sig.output("bias"), ex.input("col[0]")); g.connect(sig.output("bias"), ex.input("col[0]"));
g.connect(sig.output("bias"), exec.input("bias")); g.connect(sig.output("bias"), exec.input("bias"));
@@ -1378,6 +1409,13 @@ fn run_signal_r(
signal: Composite, params: &[Scalar], data: RunData, seed: u64, env: &project::Env, signal: Composite, params: &[Scalar], data: RunData, seed: u64, env: &project::Env,
) -> RunReport { ) -> RunReport {
let topo = topology_hash(&signal); // before signal is consumed let topo = topology_hash(&signal); // before signal is consumed
// The default binding (name defaults; `aura run` carries no campaign
// overrides). Refusals are the established `aura: ` + exit-1 register.
let binding = binding::resolve_binding(signal.name(), signal.input_roles(), &BTreeMap::new())
.unwrap_or_else(|m| {
eprintln!("aura: {m}");
std::process::exit(1);
});
let names: Vec<String> = signal let names: Vec<String> = signal
.param_space() .param_space()
.iter() .iter()
@@ -1390,7 +1428,7 @@ fn run_signal_r(
// receiver alive so the sink's sends do not fail, but do not drain it. // receiver alive so the sink's sends do not fail, but do not drain it.
let (tx_req, _rx_req) = mpsc::channel(); let (tx_req, _rx_req) = mpsc::channel();
let (sources, window, pip_size) = resolve_run_data(&data, env); let (sources, window, pip_size) = resolve_run_data(&data, env);
let wrapped = wrap_r(signal, tx_eq, tx_ex, tx_r, tx_req, StopRule::Vol { length: R_SMA_STOP_LENGTH, k: R_SMA_STOP_K }, false, pip_size); let wrapped = wrap_r(signal, tx_eq, tx_ex, tx_r, tx_req, StopRule::Vol { length: R_SMA_STOP_LENGTH, k: R_SMA_STOP_K }, false, pip_size, &binding);
let flat = wrapped let flat = wrapped
.compile_with_params(params) .compile_with_params(params)
.expect("signal binds + wraps to a valid harness"); .expect("signal binds + wraps to a valid harness");
@@ -1428,12 +1466,13 @@ fn run_blueprint_member(
topo: &str, topo: &str,
env: &project::Env, env: &project::Env,
stop: StopRule, stop: StopRule,
binding: &binding::ResolvedBinding,
) -> RunReport { ) -> RunReport {
let (tx_eq, rx_eq) = mpsc::channel(); let (tx_eq, rx_eq) = mpsc::channel();
let (tx_ex, rx_ex) = mpsc::channel(); let (tx_ex, rx_ex) = mpsc::channel();
let (tx_r, rx_r) = mpsc::channel(); let (tx_r, rx_r) = mpsc::channel();
let (tx_req, _rx_req) = mpsc::channel(); let (tx_req, _rx_req) = mpsc::channel();
let mut h = wrap_r(signal, tx_eq, tx_ex, tx_r, tx_req, stop, true, pip) let mut h = wrap_r(signal, tx_eq, tx_ex, tx_r, tx_req, stop, true, pip, binding)
.bootstrap_with_cells(point) .bootstrap_with_cells(point)
.expect("member bootstraps (point kind-checked against param_space)"); .expect("member bootstraps (point kind-checked against param_space)");
h.run(sources); h.run(sources);
@@ -1476,11 +1515,15 @@ fn run_blueprint_member(
fn blueprint_axis_probe(doc: &str, env: &project::Env) -> Composite { fn blueprint_axis_probe(doc: &str, env: &project::Env) -> Composite {
let signal = blueprint_from_json(doc, &|t| env.resolve(t)) let signal = blueprint_from_json(doc, &|t| env.resolve(t))
.expect("doc parse-validated at the dispatch boundary; reload is infallible"); .expect("doc parse-validated at the dispatch boundary; reload is infallible");
// The PROBE binding is lenient (unresolvable roles fall back to close),
// like the probe's synthetic pip: the wrap is built for its param_space
// only, never run over data — strict resolution lives on the run paths.
let probe = binding::probe_binding(signal.input_roles());
let (tx_eq, _) = mpsc::channel(); let (tx_eq, _) = mpsc::channel();
let (tx_ex, _) = mpsc::channel(); let (tx_ex, _) = mpsc::channel();
let (tx_r, _) = mpsc::channel(); let (tx_r, _) = mpsc::channel();
let (tx_req, _) = mpsc::channel(); let (tx_req, _) = mpsc::channel();
wrap_r(signal, tx_eq, tx_ex, tx_r, tx_req, StopRule::Vol { length: R_SMA_STOP_LENGTH, k: R_SMA_STOP_K }, true, SYNTHETIC_PIP_SIZE) wrap_r(signal, tx_eq, tx_ex, tx_r, tx_req, StopRule::Vol { length: R_SMA_STOP_LENGTH, k: R_SMA_STOP_K }, true, SYNTHETIC_PIP_SIZE, &probe)
} }
/// `aura sweep <blueprint.json> --list-axes`: one `<name>:<kind>` line per open /// `aura sweep <blueprint.json> --list-axes`: one `<name>:<kind>` line per open
@@ -1521,7 +1564,11 @@ fn blueprint_sweep_family(
blueprint_from_json(d, &|t| env.resolve(t)) blueprint_from_json(d, &|t| env.resolve(t))
.expect("doc parse-validated at the dispatch boundary; reload is infallible") .expect("doc parse-validated at the dispatch boundary; reload is infallible")
}; };
let topo = topology_hash(&reload(doc)); let probe_signal = reload(doc);
let topo = topology_hash(&probe_signal);
// 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 = binding::resolve_binding(probe_signal.name(), probe_signal.input_roles(), &BTreeMap::new())?;
let pip = data.pip_size(); let pip = data.pip_size();
let window = data.full_window(env); let window = data.full_window(env);
// a single throwaway floated build, only to resolve param_space (borrow) then seed // a single throwaway floated build, only to resolve param_space (borrow) then seed
@@ -1554,7 +1601,7 @@ fn blueprint_sweep_family(
.sweep(|point| { .sweep(|point| {
// fresh per-member graph (Composite is !Clone, reload per member) run through // fresh per-member graph (Composite is !Clone, reload per member) run through
// the shared reduce-mode member path — the same fn reproduction re-runs. // the shared reduce-mode member path — the same fn reproduction re-runs.
run_blueprint_member(reload(doc), point, &space, data.run_sources(env), window, 0, pip, &topo, env, StopRule::Vol { length: R_SMA_STOP_LENGTH, k: R_SMA_STOP_K }) run_blueprint_member(reload(doc), point, &space, data.run_sources(env), window, 0, pip, &topo, env, StopRule::Vol { length: R_SMA_STOP_LENGTH, k: R_SMA_STOP_K }, &binding)
}) })
// render the sweep terminal's BindError to a message (the fn's String error contract), // render the sweep terminal's BindError to a message (the fn's String error contract),
// so `UnknownKnob("nope")` still surfaces verbatim at the IO wrapper. // so `UnknownKnob("nope")` still surfaces verbatim at the IO wrapper.
@@ -1568,7 +1615,7 @@ fn blueprint_sweep_family(
/// sweep terminal (no panic, no hidden exit) for the caller to render. /// sweep terminal (no panic, no hidden exit) for the caller to render.
fn blueprint_sweep_over( fn blueprint_sweep_over(
doc: &str, axes: &[(String, Vec<Scalar>)], from: Timestamp, to: Timestamp, data: &DataSource, doc: &str, axes: &[(String, Vec<Scalar>)], from: Timestamp, to: Timestamp, data: &DataSource,
env: &project::Env, env: &project::Env, binding: &binding::ResolvedBinding,
) -> Result<(SweepFamily, Vec<usize>), BindError> { ) -> Result<(SweepFamily, Vec<usize>), BindError> {
let reload = |d: &str| { let reload = |d: &str| {
blueprint_from_json(d, &|t| env.resolve(t)) blueprint_from_json(d, &|t| env.resolve(t))
@@ -1587,7 +1634,7 @@ fn blueprint_sweep_over(
binder.sweep_with_lattice(|point| { binder.sweep_with_lattice(|point| {
let sources = data.windowed_sources(from, to, env); let sources = data.windowed_sources(from, to, env);
let window = window_of(&sources).expect("non-empty in-sample window"); let window = window_of(&sources).expect("non-empty in-sample window");
run_blueprint_member(reload(doc), point, &space, sources, window, 0, pip, &topo, env, StopRule::Vol { length: R_SMA_STOP_LENGTH, k: R_SMA_STOP_K }) run_blueprint_member(reload(doc), point, &space, sources, window, 0, pip, &topo, env, StopRule::Vol { length: R_SMA_STOP_LENGTH, k: R_SMA_STOP_K }, binding)
}) })
} }
@@ -1598,14 +1645,14 @@ fn blueprint_sweep_over(
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
fn run_oos_blueprint( fn run_oos_blueprint(
doc: &str, params: &[Cell], space: &[ParamSpec], from: Timestamp, to: Timestamp, doc: &str, params: &[Cell], space: &[ParamSpec], from: Timestamp, to: Timestamp,
topo: &str, data: &DataSource, env: &project::Env, topo: &str, data: &DataSource, env: &project::Env, binding: &binding::ResolvedBinding,
) -> (Vec<(Timestamp, f64)>, RunReport) { ) -> (Vec<(Timestamp, f64)>, RunReport) {
let reload = blueprint_from_json(doc, &|t| env.resolve(t)) let reload = blueprint_from_json(doc, &|t| env.resolve(t))
.expect("doc parse-validated at the dispatch boundary; reload is infallible"); .expect("doc parse-validated at the dispatch boundary; reload is infallible");
let pip = data.pip_size(); let pip = data.pip_size();
let sources = data.windowed_sources(from, to, env); let sources = data.windowed_sources(from, to, env);
let window = window_of(&sources).expect("non-empty out-of-sample window"); 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, StopRule::Vol { length: R_SMA_STOP_LENGTH, k: R_SMA_STOP_K }); let report = run_blueprint_member(reload, params, space, sources, window, 0, pip, topo, env, StopRule::Vol { length: R_SMA_STOP_LENGTH, k: R_SMA_STOP_K }, binding);
(Vec::new(), report) (Vec::new(), report)
} }
@@ -1628,8 +1675,16 @@ fn blueprint_walkforward_family(
} }
}; };
let space = blueprint_axis_probe(doc, env).param_space(); let space = blueprint_axis_probe(doc, env).param_space();
let topo = topology_hash(&blueprint_from_json(doc, &|t| env.resolve(t)) let probe_signal = blueprint_from_json(doc, &|t| env.resolve(t))
.expect("doc parse-validated at the dispatch boundary; reload is infallible")); .expect("doc parse-validated at the dispatch boundary; reload is infallible");
let topo = topology_hash(&probe_signal);
// Strict binding resolution, once per family; refusal is the established
// `aura: ` + exit-1 register (the roller's usage refusals stay exit 2).
let binding = binding::resolve_binding(probe_signal.name(), probe_signal.input_roles(), &BTreeMap::new())
.unwrap_or_else(|m| {
eprintln!("aura: {m}");
std::process::exit(1);
});
// Validate the `--axis` grid ONCE at the dispatch boundary, mirroring `aura sweep` // 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 // (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 // the per-window closure out across the windows in parallel, so a `BindError` raised
@@ -1642,19 +1697,19 @@ fn blueprint_walkforward_family(
.next() .next()
.expect("roller yields >= 1 window (validated above)") .expect("roller yields >= 1 window (validated above)")
.is; .is;
if let Err(e) = blueprint_sweep_over(doc, axes, first_is.0, first_is.1, data, env) { if let Err(e) = blueprint_sweep_over(doc, axes, first_is.0, first_is.1, data, env, &binding) {
eprintln!("aura: {e:?}"); eprintln!("aura: {e:?}");
std::process::exit(2); std::process::exit(2);
} }
walk_forward(roller, space.clone(), |w: WindowBounds| { walk_forward(roller, space.clone(), |w: WindowBounds| {
let (is_family, lattice) = blueprint_sweep_over(doc, axes, w.is.0, w.is.1, data, env) let (is_family, lattice) = blueprint_sweep_over(doc, axes, w.is.0, w.is.1, data, env, &binding)
.expect("axes validated in the dispatch-boundary pre-flight"); .expect("axes validated in the dispatch-boundary pre-flight");
let (best, selection) = match select_winner(&is_family, WINNER_SELECTION_METRIC, select, Some(&lattice)) { let (best, selection) = match select_winner(&is_family, WINNER_SELECTION_METRIC, select, Some(&lattice)) {
Ok(v) => v, Ok(v) => v,
Err(msg) => { eprintln!("aura: {msg}"); std::process::exit(2); } Err(msg) => { eprintln!("aura: {msg}"); std::process::exit(2); }
}; };
let (oos_equity, mut oos_report) = let (oos_equity, mut oos_report) =
run_oos_blueprint(doc, &best.params, &space, w.oos.0, w.oos.1, &topo, data, env); run_oos_blueprint(doc, &best.params, &space, w.oos.0, w.oos.1, &topo, data, env, &binding);
oos_report.manifest.selection = Some(selection); oos_report.manifest.selection = Some(selection);
WindowRun { chosen_params: best.params, oos_equity, oos_report } WindowRun { chosen_params: best.params, oos_equity, oos_report }
}) })
@@ -1686,7 +1741,11 @@ fn blueprint_mc_family(
blueprint_from_json(d, &|t| env.resolve(t)) blueprint_from_json(d, &|t| env.resolve(t))
.expect("doc parse-validated at the dispatch boundary; reload is infallible") .expect("doc parse-validated at the dispatch boundary; reload is infallible")
}; };
let topo = topology_hash(&reload(doc)); let probe_signal = reload(doc);
let topo = topology_hash(&probe_signal);
// Strict binding resolution (name defaults — mc's synthetic family binds
// no campaign overrides); the exit-free Err contract of this builder.
let binding = binding::resolve_binding(probe_signal.name(), probe_signal.input_roles(), &BTreeMap::new())?;
let pip = data.pip_size(); let pip = data.pip_size();
// probe the wrapped param_space (the same probe the sweep resolves against); // 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. // MC needs it empty. `blueprint_axis_probe` is the single source of that wrap.
@@ -1711,7 +1770,7 @@ fn blueprint_mc_family(
let family = monte_carlo(&base_point, &seeds, |seed, _base| { let family = monte_carlo(&base_point, &seeds, |seed, _base| {
let sources = synthetic_walk_sources(seed); let sources = synthetic_walk_sources(seed);
let window = window_of(&sources).expect("non-empty synthetic walk"); let window = window_of(&sources).expect("non-empty synthetic walk");
run_blueprint_member(reload(doc), &[], &space, sources, window, seed, pip, &topo, env, StopRule::Vol { length: R_SMA_STOP_LENGTH, k: R_SMA_STOP_K }) run_blueprint_member(reload(doc), &[], &space, sources, window, seed, pip, &topo, env, StopRule::Vol { length: R_SMA_STOP_LENGTH, k: R_SMA_STOP_K }, &binding)
}); });
// Silent-vacuous MC guard (refuse-don't-guess, C10): with >= 2 seeds, if every draw's // 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 — // metrics are bit-identical to the first, no seed reached a distinguishable realization —
@@ -3414,8 +3473,11 @@ mod tests {
let (tx_ex, rx_ex) = mpsc::channel(); let (tx_ex, rx_ex) = mpsc::channel();
let (tx_r, _rx_r) = mpsc::channel(); let (tx_r, _rx_r) = mpsc::channel();
let (tx_req, _rx_req) = mpsc::channel(); let (tx_req, _rx_req) = mpsc::channel();
let signal = r_meanrev_signal(Some(3), Some(0.0));
let binding = binding::resolve_binding(signal.name(), signal.input_roles(), &BTreeMap::new())
.expect("the price role resolves");
let flat = wrap_r( let flat = wrap_r(
r_meanrev_signal(Some(3), Some(0.0)), signal,
tx_eq, tx_eq,
tx_ex, tx_ex,
tx_r, tx_r,
@@ -3423,6 +3485,7 @@ mod tests {
StopRule::Vol { length: 3, k: 2.0 }, StopRule::Vol { length: 3, k: 2.0 },
false, false,
SYNTHETIC_PIP_SIZE, SYNTHETIC_PIP_SIZE,
&binding,
) )
.compile_with_params(&[]) .compile_with_params(&[])
.expect("r-meanrev signal wraps to a valid harness"); .expect("r-meanrev signal wraps to a valid harness");
@@ -3479,11 +3542,14 @@ mod tests {
// from the synthetic 0.0001 — i.e. the real-data condition. // from the synthetic 0.0001 — i.e. the real-data condition.
let pip_a = 1.0_f64; let pip_a = 1.0_f64;
let pip_b = 0.1_f64; let pip_b = 0.1_f64;
let signal_a = r_meanrev_signal(Some(3), Some(0.0));
let binding = binding::resolve_binding(signal_a.name(), signal_a.input_roles(), &BTreeMap::new())
.expect("the price role resolves");
let report_a = run_blueprint_member( let report_a = run_blueprint_member(
r_meanrev_signal(Some(3), Some(0.0)), &[], &space, make_source(), window, 0, pip_a, "topo", &env, stop, signal_a, &[], &space, make_source(), window, 0, pip_a, "topo", &env, stop, &binding,
); );
let report_b = run_blueprint_member( let report_b = run_blueprint_member(
r_meanrev_signal(Some(3), Some(0.0)), &[], &space, make_source(), window, 0, pip_b, "topo", &env, stop, r_meanrev_signal(Some(3), Some(0.0)), &[], &space, make_source(), window, 0, pip_b, "topo", &env, stop, &binding,
); );
// Guard: the run must have actually traded, else the invariant is vacuous. // Guard: the run must have actually traded, else the invariant is vacuous.
assert!( assert!(
@@ -4062,6 +4128,8 @@ mod tests {
// Mint one member under a NON-default vol-stop regime (default is length=3, k=2.0). // Mint one member under a NON-default vol-stop regime (default is length=3, k=2.0).
// run_blueprint_member stamps stop_length=8/stop_k=4.0 into the manifest params. // run_blueprint_member stamps stop_length=8/stop_k=4.0 into the manifest params.
let non_default = StopRule::Vol { length: 8, k: 4.0 }; let non_default = StopRule::Vol { length: 8, k: 4.0 };
let binding = binding::resolve_binding("stoprepro", reload().input_roles(), &BTreeMap::new())
.expect("the price role resolves");
let report = run_blueprint_member( let report = run_blueprint_member(
reload(), reload(),
&[], &[],
@@ -4073,6 +4141,7 @@ mod tests {
&topo, &topo,
&env, &env,
non_default, non_default,
&binding,
); );
// persist exactly as the sweep/campaign paths do: store the blueprint, append the family. // persist exactly as the sweep/campaign paths do: store the blueprint, append the family.