feat(cli): sweep axes override bound params on the blueprint verb paths
Task 3-4 of the bound-override cycle. The family boundary derives the override set from the invocation's axes (override_paths: an axis in the open space passes through, one naming a bound param re-opens it in strategy coordinates, one matching neither is refused with a message pointing at --list-axes) and threads it through the axis probe and every per-member reload (blueprint_axis_probe_reopened / reopen_all), so probe and members stay slot-identical. The 'fully bound; nothing to sweep' refusal retires; identity stays the authored document (the topology_hash probe reload is never re-opened). --list-axes now prints the bound surface after the open knobs — one '<bp>.<node>.<param>:<KIND> default=<value>' line per bound param, unconditionally (a bound param is an equally re-openable axis regardless of how many knobs are open beside it). Walk-forward (blueprint_sweep_over, blueprint_walkforward_family, run_oos_blueprint) and reproduce (reproduce_family_in plus the campaign-side trace re-run) derive the same set silently from recorded manifest param names (bound_overrides_of), so a family that swept an overridden bound param re-derives bit-identically. refs #246
This commit is contained in:
@@ -839,11 +839,25 @@ pub(crate) fn persist_campaign_traces(
|
|||||||
cell_rec.strategy
|
cell_rec.strategy
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
// The #246 override set (silent variant, `reproduce_family_in`'s
|
||||||
|
// recipe): re-derived from the cell's own recorded manifest param
|
||||||
|
// names, against a raw probe space + raw strategy load — the members
|
||||||
|
// of one cell share the same knob NAMES (only values differ), so this
|
||||||
|
// is safe to compute once, cell-invariant, from the first member.
|
||||||
|
let raw_space = crate::blueprint_axis_probe(blueprint_json, env).param_space();
|
||||||
|
let raw_signal = blueprint_from_json(blueprint_json, &|t| env.resolve(t))
|
||||||
|
.expect("stored blueprint passed the referential gate; reload is infallible");
|
||||||
|
let recorded: Vec<String> = members
|
||||||
|
.first()
|
||||||
|
.map(|(_, r)| r.manifest.params.iter().map(|(n, _)| n.clone()).collect())
|
||||||
|
.unwrap_or_default();
|
||||||
|
let overrides = crate::bound_overrides_of(&recorded, &raw_space, &raw_signal);
|
||||||
// Cell-invariant across every member of this cell (the wrapped param
|
// Cell-invariant across every member of this cell (the wrapped param
|
||||||
// space depends only on the cell's blueprint; the resolved geometry
|
// space depends only on the cell's blueprint; the resolved geometry
|
||||||
// only on the cell's instrument) — computed once here rather than
|
// only on the cell's instrument) — computed once here rather than
|
||||||
// redundantly inside the member loop below.
|
// redundantly inside the member loop below.
|
||||||
let space = crate::blueprint_axis_probe(blueprint_json, env).param_space();
|
let space =
|
||||||
|
crate::blueprint_axis_probe_reopened(blueprint_json, env, &overrides).param_space();
|
||||||
// Same resolved pip the member ran at (`CliMemberRunner::run_member`'s
|
// Same resolved pip the member ran at (`CliMemberRunner::run_member`'s
|
||||||
// `geo.pip_size`), else the C1 drift-alarm equality below would trip
|
// `geo.pip_size`), else the C1 drift-alarm equality below would trip
|
||||||
// spuriously on a re-run computed at a different (default) pip.
|
// spuriously on a re-run computed at a different (default) pip.
|
||||||
@@ -872,8 +886,11 @@ pub(crate) fn persist_campaign_traces(
|
|||||||
cell_rec.instrument, from.0, to.0
|
cell_rec.instrument, from.0, to.0
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
let signal = blueprint_from_json(blueprint_json, &|t| env.resolve(t))
|
let signal = crate::reopen_all(
|
||||||
.expect("stored blueprint passed the referential gate; reload is infallible");
|
blueprint_from_json(blueprint_json, &|t| env.resolve(t))
|
||||||
|
.expect("stored blueprint passed the referential gate; reload is infallible"),
|
||||||
|
&overrides,
|
||||||
|
);
|
||||||
// Campaign data.bindings overrides win over name defaults — the
|
// Campaign data.bindings overrides win over name defaults — the
|
||||||
// SAME resolution the member ran under, so the C1 drift alarm
|
// SAME resolution the member ran under, so the C1 drift alarm
|
||||||
// compares like with like.
|
// compares like with like.
|
||||||
|
|||||||
+207
-70
@@ -1222,14 +1222,32 @@ fn reproduce_family_in(
|
|||||||
eprintln!("aura: blueprint {hash} missing from store");
|
eprintln!("aura: blueprint {hash} missing from store");
|
||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
});
|
});
|
||||||
|
// The #246 override set (silent variant): re-derived from the RECORDED
|
||||||
|
// manifest param names, against a raw probe space + raw strategy load — a
|
||||||
|
// name matching neither space is simply not an override (falls through to
|
||||||
|
// `point_from_params`'s existing missing-knob refusal below), unlike the
|
||||||
|
// sweep boundary's `override_paths`, which errors on it.
|
||||||
|
let recorded: Vec<String> =
|
||||||
|
stored.manifest.params.iter().map(|(n, _)| n.clone()).collect();
|
||||||
|
let raw_space = blueprint_axis_probe(&doc, env).param_space();
|
||||||
|
let raw_signal = blueprint_from_json(&doc, &|t| env.resolve(t)).unwrap_or_else(|e| {
|
||||||
|
eprintln!("aura: stored blueprint {hash} does not parse: {e:?}");
|
||||||
|
std::process::exit(1);
|
||||||
|
});
|
||||||
|
let overrides = bound_overrides_of(&recorded, &raw_space, &raw_signal);
|
||||||
// Reload the stored blueprint per use: a Composite is !Clone, and both the
|
// Reload the stored blueprint per use: a Composite is !Clone, and both the
|
||||||
// param-space probe (below) and the re-run each consume one. The doc was
|
// param-space probe (below) and the re-run each consume one. The doc was
|
||||||
// canonical-serialized at store time, so every reload is infallible.
|
// canonical-serialized at store time, so every reload is infallible. Every
|
||||||
|
// reload re-opens the same override set derived above, so the recorded
|
||||||
|
// param names resolve against the space they were minted under (#246).
|
||||||
let reload = || {
|
let reload = || {
|
||||||
blueprint_from_json(&doc, &|t| env.resolve(t)).unwrap_or_else(|e| {
|
reopen_all(
|
||||||
eprintln!("aura: stored blueprint {hash} does not parse: {e:?}");
|
blueprint_from_json(&doc, &|t| env.resolve(t)).unwrap_or_else(|e| {
|
||||||
std::process::exit(1);
|
eprintln!("aura: stored blueprint {hash} does not parse: {e:?}");
|
||||||
})
|
std::process::exit(1);
|
||||||
|
}),
|
||||||
|
&overrides,
|
||||||
|
)
|
||||||
};
|
};
|
||||||
// The param_space of the WRAPPED signal — its knobs carry the `r_sma`
|
// The param_space of the WRAPPED signal — its knobs carry the `r_sma`
|
||||||
// wrapper's `sma_signal.` node-path prefix, exactly the names the manifest
|
// wrapper's `sma_signal.` node-path prefix, exactly the names the manifest
|
||||||
@@ -1792,8 +1810,17 @@ fn run_blueprint_member(
|
|||||||
/// boundary before this runs, so a malformed doc has already exited 2 and
|
/// boundary before this runs, so a malformed doc has already exited 2 and
|
||||||
/// never reaches the `.expect`.
|
/// never reaches the `.expect`.
|
||||||
fn blueprint_axis_probe(doc: &str, env: &project::Env) -> Composite {
|
fn blueprint_axis_probe(doc: &str, env: &project::Env) -> Composite {
|
||||||
|
blueprint_axis_probe_reopened(doc, env, &[])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The axis probe with a #246 override set re-opened on the strategy BEFORE
|
||||||
|
/// wrapping — probe and per-member reloads must re-open identically so points
|
||||||
|
/// resolve against one space. The empty-set form is byte-equal to the old
|
||||||
|
/// probe (mc/run closed-checks and the open `--list-axes` lines read that).
|
||||||
|
fn blueprint_axis_probe_reopened(doc: &str, env: &project::Env, overrides: &[String]) -> 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");
|
||||||
|
let signal = reopen_all(signal, overrides);
|
||||||
// The PROBE binding is lenient (unresolvable roles fall back to close),
|
// 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
|
// 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.
|
// only, never run over data — strict resolution lives on the run paths.
|
||||||
@@ -1805,13 +1832,94 @@ fn blueprint_axis_probe(doc: &str, env: &project::Env) -> Composite {
|
|||||||
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, None)
|
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, None)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The override subset of `names` (#246): every name missing the un-reopened
|
||||||
|
/// wrapped OPEN space but naming a BOUND param of the strategy — returned in
|
||||||
|
/// STRATEGY coordinates (the wrap prefix `<signal.name()>.` stripped) for
|
||||||
|
/// `Composite::reopen`. Names matching neither space are skipped here; the
|
||||||
|
/// caller decides whether that is an error (sweep boundary) or falls through
|
||||||
|
/// to its existing resolution errors. The silent variant `reproduce_family_in`
|
||||||
|
/// uses over the RECORDED manifest param names (the sweep boundary uses the
|
||||||
|
/// stricter `override_paths`, which errors on an unmatched name instead).
|
||||||
|
fn bound_overrides_of(
|
||||||
|
names: &[String],
|
||||||
|
open_space: &[ParamSpec],
|
||||||
|
signal: &Composite,
|
||||||
|
) -> Vec<String> {
|
||||||
|
let open: HashSet<&str> = open_space.iter().map(|p| p.name.as_str()).collect();
|
||||||
|
let prefix = format!("{}.", signal.name());
|
||||||
|
let bound: HashSet<String> = signal
|
||||||
|
.bound_param_space()
|
||||||
|
.into_iter()
|
||||||
|
.map(|b| format!("{prefix}{}", b.name))
|
||||||
|
.collect();
|
||||||
|
names
|
||||||
|
.iter()
|
||||||
|
.filter(|n| !open.contains(n.as_str()) && bound.contains(*n))
|
||||||
|
.map(|n| n[prefix.len()..].to_string())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The sweep-boundary variant (#246): like `bound_overrides_of`, but an axis
|
||||||
|
/// matching NEITHER the open nor the bound space is the error — the honest
|
||||||
|
/// replacement of the retired "fully bound; nothing to sweep" refusal.
|
||||||
|
fn override_paths(
|
||||||
|
axes: &[(String, Vec<Scalar>)],
|
||||||
|
open_space: &[ParamSpec],
|
||||||
|
signal: &Composite,
|
||||||
|
) -> Result<Vec<String>, String> {
|
||||||
|
let open: HashSet<&str> = open_space.iter().map(|p| p.name.as_str()).collect();
|
||||||
|
let prefix = format!("{}.", signal.name());
|
||||||
|
let bound: HashSet<String> = signal
|
||||||
|
.bound_param_space()
|
||||||
|
.into_iter()
|
||||||
|
.map(|b| format!("{prefix}{}", b.name))
|
||||||
|
.collect();
|
||||||
|
let mut overrides = Vec::new();
|
||||||
|
for (name, _) in axes {
|
||||||
|
if open.contains(name.as_str()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if bound.contains(name) {
|
||||||
|
overrides.push(name[prefix.len()..].to_string());
|
||||||
|
} else {
|
||||||
|
return Err(format!(
|
||||||
|
"axis {name}: names no param of this blueprint (open or bound) — \
|
||||||
|
see `aura sweep <bp> --list-axes`"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(overrides)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Apply a validated override set to a freshly loaded strategy (#246).
|
||||||
|
/// Infallible by contract: the set was derived against this same document at
|
||||||
|
/// the family boundary.
|
||||||
|
fn reopen_all(signal: Composite, overrides: &[String]) -> Composite {
|
||||||
|
overrides.iter().fold(signal, |s, p| {
|
||||||
|
s.reopen(p).expect("override set validated at the family boundary")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// `aura sweep <blueprint.json> --list-axes`: one `<name>:<kind>` line per open
|
/// `aura sweep <blueprint.json> --list-axes`: one `<name>:<kind>` line per open
|
||||||
/// sweepable knob, in `param_space()` order; a closed blueprint prints nothing.
|
/// sweepable knob, in `param_space()` order (byte-identical to before #246),
|
||||||
/// Names are exactly what `--axis` binds (same probe as the sweep terminal).
|
/// followed by one `<name>:<kind> default=<value>` line per BOUND param —
|
||||||
|
/// every bound param IS a sweepable default, re-openable by naming it as an
|
||||||
|
/// `--axis` (#246), on EVERY blueprint shape (fully open, partially open, or
|
||||||
|
/// fully closed) — not only the fully-closed case: `--axis`/`override_paths`
|
||||||
|
/// accepts a bound name regardless of how many knobs happen to be open
|
||||||
|
/// alongside it, so the discovery surface must list it regardless too. Names
|
||||||
|
/// are exactly what `--axis` binds.
|
||||||
fn list_blueprint_axes(doc: &str, env: &project::Env) {
|
fn list_blueprint_axes(doc: &str, env: &project::Env) {
|
||||||
for p in blueprint_axis_probe(doc, env).param_space() {
|
let space = blueprint_axis_probe(doc, env).param_space();
|
||||||
|
for p in &space {
|
||||||
println!("{}:{:?}", p.name, p.kind); // ScalarKind Debug -> I64/F64/Bool/Timestamp
|
println!("{}:{:?}", p.name, p.kind); // ScalarKind Debug -> I64/F64/Bool/Timestamp
|
||||||
}
|
}
|
||||||
|
let signal = blueprint_from_json(doc, &|t| env.resolve(t))
|
||||||
|
.expect("doc parse-validated at the dispatch boundary");
|
||||||
|
let bp = signal.name().to_string();
|
||||||
|
for b in signal.bound_param_space() {
|
||||||
|
println!("{bp}.{}:{:?} default={}", b.name, b.kind, render_value(&b.value));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sweep a serialized signal `doc` over user-named param-space axes. Structurally it
|
/// Sweep a serialized signal `doc` over user-named param-space axes. Structurally it
|
||||||
@@ -1823,11 +1931,9 @@ fn list_blueprint_axes(doc: &str, env: &project::Env) {
|
|||||||
/// The axes are taken verbatim BY NAME (not the four suffix-resolved r-sma knobs):
|
/// 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
|
/// 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
|
/// kind mismatch surfaces as the sweep terminal's [`BindError`], rendered to a message
|
||||||
/// string — a named error, never a panic. A FULLY BOUND (closed) blueprint has an empty
|
/// string — a named error, never a panic. An axis naming a bound param re-opens it
|
||||||
/// `param_space` — nothing to sweep — and is refused up front with a clear message
|
/// (#246: bound value = default); an axis matching neither space is refused by
|
||||||
/// (the symmetric inverse of [`blueprint_mc_family`]'s closed-blueprint requirement),
|
/// `override_paths` before any run. Every member manifest carries the shared
|
||||||
/// pre-empting the misleading `UnknownKnob(<axis>)` the per-axis resolve would emit for a
|
|
||||||
/// knob that is not unknown but bound out. Every member manifest carries the shared
|
|
||||||
/// `topology_hash` of the loaded signal; reduce-mode fold, identical to the retired
|
/// `topology_hash` of the loaded signal; reduce-mode fold, identical to the retired
|
||||||
/// mirror's default (no-trace) arm.
|
/// mirror's default (no-trace) arm.
|
||||||
fn blueprint_sweep_family(
|
fn blueprint_sweep_family(
|
||||||
@@ -1836,14 +1942,11 @@ fn blueprint_sweep_family(
|
|||||||
data: &DataSource,
|
data: &DataSource,
|
||||||
env: &project::Env,
|
env: &project::Env,
|
||||||
) -> Result<SweepFamily, String> {
|
) -> Result<SweepFamily, String> {
|
||||||
// The doc is parse-validated at the dispatch boundary (with file-path context),
|
// Identity + binding read the AUTHORED doc, raw (no override re-open):
|
||||||
// so every reload here is infallible: the builder has a single error contract —
|
// topology and the resolved role plan are properties of the document, not
|
||||||
// the `BindError` returned by the sweep terminal — and no hidden process exit.
|
// of any one sweep's axis choices.
|
||||||
let reload = |d: &str| {
|
let probe_signal = blueprint_from_json(doc, &|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 probe_signal = reload(doc);
|
|
||||||
let topo = topology_hash(&probe_signal);
|
let topo = topology_hash(&probe_signal);
|
||||||
// Strict binding resolution (name defaults — the verb path carries no
|
// Strict binding resolution (name defaults — the verb path carries no
|
||||||
// campaign overrides): the family's open plan and wrap plan in one value.
|
// campaign overrides): the family's open plan and wrap plan in one value.
|
||||||
@@ -1853,23 +1956,26 @@ fn blueprint_sweep_family(
|
|||||||
}
|
}
|
||||||
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
|
// The un-reopened wrapped OPEN space (#246): derives the override set (which
|
||||||
// the named axes (move) — its taps are never drained. The wrapped probe is the one
|
// named axes re-open a bound param) before the real, reopened probe is built —
|
||||||
// `blueprint_axis_probe` single-sources (identical `false, true, None` wrap).
|
// probe and per-member reloads must re-open identically so points resolve
|
||||||
let probe = blueprint_axis_probe(doc, env);
|
// 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();
|
let space = probe.param_space();
|
||||||
// A fully bound blueprint has no open knob — there is nothing to sweep. Refuse it up
|
// The doc is parse-validated at the dispatch boundary (with file-path context),
|
||||||
// front (mirroring blueprint_mc_family's closed-blueprint guard, inverted) BEFORE the
|
// so every reload here is infallible: the builder has a single error contract —
|
||||||
// per-axis resolve, which would otherwise return `UnknownKnob(<axis>)` — misleading, as
|
// the `BindError` returned by the sweep terminal — and no hidden process exit.
|
||||||
// the named knob is not unknown but bound out. Exit-free: the message is returned for
|
// Member reloads re-open the SAME override set derived above, so every member
|
||||||
// the IO wrapper to render + exit 2, so the precondition is unit-testable.
|
// resolves its axes against the identical (reopened) param space the probe used.
|
||||||
if space.is_empty() {
|
let reload = |d: &str| {
|
||||||
return Err(
|
reopen_all(
|
||||||
"this blueprint is fully bound; nothing to sweep (use `aura sweep <bp> --list-axes` \
|
blueprint_from_json(d, &|t| env.resolve(t))
|
||||||
to confirm there are no open knobs)"
|
.expect("doc parse-validated at the dispatch boundary; reload is infallible"),
|
||||||
.to_string(),
|
&overrides,
|
||||||
);
|
)
|
||||||
}
|
};
|
||||||
// seed the named axes verbatim: the first via Composite::axis (consumes the probe),
|
// seed the named axes verbatim: the first via Composite::axis (consumes the probe),
|
||||||
// the rest via SweepBinder::axis. resolve_axes name- and kind-checks them at the
|
// the rest via SweepBinder::axis. resolve_axes name- and kind-checks them at the
|
||||||
// sweep terminal, so an UnknownKnob / KindMismatch is returned, not panicked.
|
// sweep terminal, so an UnknownKnob / KindMismatch is returned, not panicked.
|
||||||
@@ -1894,7 +2000,12 @@ fn blueprint_sweep_family(
|
|||||||
/// `[from,to]` — the windowed, lattice-carrying twin of
|
/// `[from,to]` — the windowed, lattice-carrying twin of
|
||||||
/// `blueprint_sweep_family`. `sweep_with_lattice` gives the grid lattice `--select
|
/// `blueprint_sweep_family`. `sweep_with_lattice` gives the grid lattice `--select
|
||||||
/// plateau` needs. An unknown/kind-mismatched axis surfaces as `BindError` at the
|
/// plateau` needs. An unknown/kind-mismatched axis surfaces as `BindError` at the
|
||||||
/// sweep terminal (no panic, no hidden exit) for the caller to render.
|
/// 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.
|
||||||
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, binding: &binding::ResolvedBinding,
|
env: &project::Env, binding: &binding::ResolvedBinding,
|
||||||
@@ -1904,8 +2015,16 @@ fn blueprint_sweep_over(
|
|||||||
.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 topo = topology_hash(&reload(doc));
|
let probe_signal = reload(doc);
|
||||||
let probe = blueprint_axis_probe(doc, env);
|
let topo = topology_hash(&probe_signal);
|
||||||
|
// 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();
|
let space = probe.param_space();
|
||||||
let mut iter = axes.iter();
|
let mut iter = axes.iter();
|
||||||
let (first_name, first_vals) = iter.next().expect("a blueprint walk-forward declares >= 1 axis");
|
let (first_name, first_vals) = iter.next().expect("a blueprint walk-forward declares >= 1 axis");
|
||||||
@@ -1916,21 +2035,29 @@ fn blueprint_sweep_over(
|
|||||||
binder.sweep_with_lattice(|point| {
|
binder.sweep_with_lattice(|point| {
|
||||||
let sources = data.windowed_sources(from, to, env, &binding.columns());
|
let sources = data.windowed_sources(from, to, env, &binding.columns());
|
||||||
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 }, binding, &[])
|
run_blueprint_member(reopen_all(reload(doc), &overrides), point, &space, sources, window, 0, pip, &topo, env, StopRule::Vol { length: R_SMA_STOP_LENGTH, k: R_SMA_STOP_K }, binding, &[])
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Run the winner params over an out-of-sample window `[from,to]` on the loaded
|
/// Run the winner params over an out-of-sample window `[from,to]` on the loaded
|
||||||
/// blueprint. The reduce-mode member
|
/// blueprint. The reduce-mode member
|
||||||
/// (`run_blueprint_member`) retains R-metrics, not a raw pip curve, so the stitching
|
/// (`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).
|
/// 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)]
|
#[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, binding: &binding::ResolvedBinding,
|
topo: &str, data: &DataSource, env: &project::Env, binding: &binding::ResolvedBinding,
|
||||||
|
overrides: &[String],
|
||||||
) -> (Vec<(Timestamp, f64)>, RunReport) {
|
) -> (Vec<(Timestamp, f64)>, RunReport) {
|
||||||
let reload = blueprint_from_json(doc, &|t| env.resolve(t))
|
let reload = reopen_all(
|
||||||
.expect("doc parse-validated at the dispatch boundary; reload is infallible");
|
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 pip = data.pip_size();
|
||||||
let sources = data.windowed_sources(from, to, env, &binding.columns());
|
let sources = data.windowed_sources(from, to, env, &binding.columns());
|
||||||
let window = window_of(&sources).expect("non-empty out-of-sample window");
|
let window = window_of(&sources).expect("non-empty out-of-sample window");
|
||||||
@@ -1956,10 +2083,22 @@ fn blueprint_walkforward_family(
|
|||||||
std::process::exit(2);
|
std::process::exit(2);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let space = blueprint_axis_probe(doc, env).param_space();
|
|
||||||
let probe_signal = 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);
|
let topo = topology_hash(&probe_signal);
|
||||||
|
// 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
|
||||||
|
// (`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 = 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
|
// Strict binding resolution, once per family; refusal is the established
|
||||||
// `aura: ` + exit-1 register (the roller's usage refusals stay exit 2).
|
// `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())
|
let binding = binding::resolve_binding(probe_signal.name(), probe_signal.input_roles(), &BTreeMap::new())
|
||||||
@@ -1995,7 +2134,7 @@ fn blueprint_walkforward_family(
|
|||||||
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, &binding);
|
run_oos_blueprint(doc, &best.params, &space, w.oos.0, w.oos.1, &topo, data, env, &binding, &overrides);
|
||||||
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 }
|
||||||
})
|
})
|
||||||
@@ -5311,36 +5450,34 @@ mod tests {
|
|||||||
let _ = std::fs::remove_dir_all(&dir);
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Property: sweeping a FULLY BOUND (closed) blueprint is refused up front with a
|
/// #246: a fully-bound (closed) blueprint IS sweepable — an axis naming a
|
||||||
/// clear "fully bound / nothing to sweep" message — NOT the terse `UnknownKnob(<axis>)`
|
/// bound param re-opens it (the bound value is the default), so the retired
|
||||||
/// the per-axis resolve would otherwise emit (misleading: the named knob is not unknown,
|
/// "fully bound; nothing to sweep" refusal must not resurface. Inverse
|
||||||
/// it is bound out, so there is simply nothing to sweep). Symmetric inverse of
|
/// guard: an axis naming NEITHER an open nor a bound param gets the one
|
||||||
/// `blueprint_mc_family_rejects_an_open_blueprint` (mc requires a closed blueprint; a
|
/// clear boundary message (not a terse `UnknownKnob` debug leak).
|
||||||
/// sweep requires >= 1 open knob). Type-agnostic on the error (renders it via Debug), so
|
|
||||||
/// the RED phase compiles whether the builder returns a `BindError` or a `String`.
|
|
||||||
#[test]
|
#[test]
|
||||||
fn blueprint_sweep_family_rejects_a_fully_bound_blueprint() {
|
fn blueprint_sweep_family_overrides_a_bound_param_and_names_unknown_axes() {
|
||||||
let env = project::Env::std();
|
let env = project::Env::std();
|
||||||
// both SMA knobs bound -> empty param_space
|
// same fixture the retired test swept: both SMA knobs bound.
|
||||||
let closed = load_closed_r_sma();
|
let closed = load_closed_r_sma();
|
||||||
let doc = blueprint_to_json(&closed).expect("serializes");
|
let doc = blueprint_to_json(&closed).expect("serializes");
|
||||||
// an axis naming a bound-out knob: the pre-fix path returns UnknownKnob for it.
|
|
||||||
|
// (a) override axis: two members, no refusal
|
||||||
let axes = vec![(
|
let axes = vec![(
|
||||||
"sma_signal.slow.length".to_string(),
|
"sma_signal.fast.length".to_string(),
|
||||||
vec![Scalar::i64(4), Scalar::i64(6)],
|
vec![Scalar::i64(2), Scalar::i64(4)],
|
||||||
)];
|
)];
|
||||||
let err = match blueprint_sweep_family(&doc, &axes, &DataSource::Synthetic, &env) {
|
let fam = blueprint_sweep_family(&doc, &axes, &DataSource::Synthetic, &env)
|
||||||
Ok(_) => panic!("a fully-bound blueprint has nothing to sweep"),
|
.expect("a bound param is a default — the axis overrides it");
|
||||||
Err(e) => format!("{e:?}"),
|
assert_eq!(fam.points.len(), 2);
|
||||||
};
|
|
||||||
assert!(
|
// (b) unknown axis: the boundary message, no UnknownKnob leak
|
||||||
err.contains("fully bound") || err.contains("nothing to sweep"),
|
let bad = vec![("sma_signal.nope".to_string(), vec![Scalar::i64(1)])];
|
||||||
"names the fully-bound / nothing-to-sweep condition: {err}"
|
let err = blueprint_sweep_family(&doc, &bad, &DataSource::Synthetic, &env)
|
||||||
);
|
.expect_err("an axis matching neither space is refused");
|
||||||
assert!(
|
assert!(err.contains("names no param"), "boundary message, got: {err}");
|
||||||
!err.contains("UnknownKnob"),
|
assert!(err.contains("--list-axes"), "must point at --list-axes: {err}");
|
||||||
"must not leak the misleading UnknownKnob: {err}"
|
assert!(!err.contains("UnknownKnob"), "must not leak the debug render: {err}");
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Property: an MC family's `aura reproduce` lines carry the member's own `seed=<N>`
|
/// Property: an MC family's `aura reproduce` lines carry the member's own `seed=<N>`
|
||||||
|
|||||||
@@ -1401,9 +1401,12 @@ fn sweep_real_ohlc_channel_members_run_and_reproduce() {
|
|||||||
.output()
|
.output()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
String::from_utf8_lossy(&axes.stdout).trim(),
|
String::from_utf8_lossy(&axes.stdout),
|
||||||
"hl_channel.channel_length:I64",
|
"hl_channel.channel_length:I64\n\
|
||||||
"the ganged channel knob is the one sweepable axis; stderr: {}",
|
hl_channel.prev_high.lag:I64 default=1\n\
|
||||||
|
hl_channel.prev_low.lag:I64 default=1\n",
|
||||||
|
"the ganged channel knob is the one open axis, alongside the fixture's \
|
||||||
|
bound Delay lags as #246 defaults; stderr: {}",
|
||||||
String::from_utf8_lossy(&axes.stderr)
|
String::from_utf8_lossy(&axes.stderr)
|
||||||
);
|
);
|
||||||
let out = std::process::Command::new(BIN)
|
let out = std::process::Command::new(BIN)
|
||||||
@@ -1813,6 +1816,105 @@ fn reproduce_real_sweep_family_re_derives_bit_identically() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Property (C18/C1 reproduce guarantee, #246): the same reproduce guarantee holds
|
||||||
|
/// when the swept axis names a BOUND param of a CLOSED starter blueprint, not an
|
||||||
|
/// already-open one — `r_sma.json` (both `sma_signal.fast.length` and
|
||||||
|
/// `sma_signal.slow.length` bound), the closed counterpart of the OPEN
|
||||||
|
/// `r_sma_open.json` the walk-forward reproduce sibling
|
||||||
|
/// (`aura_walkforward_over_a_blueprint_reproduces_bit_identically`) sweeps.
|
||||||
|
/// Modelled on that sibling (synthetic in-process walk-forward — no `--real`),
|
||||||
|
/// NOT on `reproduce_real_sweep_family_re_derives_bit_identically`: a real-data
|
||||||
|
/// mint routes through the campaign executor (`CliMemberRunner::run_member` +
|
||||||
|
/// `validate_and_register_axes`), neither of which carries any #246 override
|
||||||
|
/// threading — a gap outside this task's file scope (`blueprint_sweep_over`,
|
||||||
|
/// `reproduce_family_in`) and spanning all four dispatch verbs, so exercising
|
||||||
|
/// it here would silently expand into unrelated, unreviewed surface. The
|
||||||
|
/// synthetic walk-forward path exercises exactly the threaded functions:
|
||||||
|
/// `blueprint_sweep_over` (via `blueprint_walkforward_family`) re-opens
|
||||||
|
/// `sma_signal.fast.length` for the per-window IS re-fit, and
|
||||||
|
/// `reproduce_family_in` re-opens it again from the recorded manifest params
|
||||||
|
/// so the re-derivation resolves against the same space the mint used.
|
||||||
|
#[test]
|
||||||
|
fn reproduce_family_with_overridden_bound_param_re_derives() {
|
||||||
|
let cwd = temp_cwd("walkforward-reproduce-override");
|
||||||
|
let bp = format!("{}/examples/r_sma.json", env!("CARGO_MANIFEST_DIR"));
|
||||||
|
let run = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||||
|
.args(["walkforward", &bp, "--axis", "sma_signal.fast.length=2,3", "--name", "wfo"])
|
||||||
|
.current_dir(&cwd)
|
||||||
|
.output()
|
||||||
|
.expect("spawn aura walkforward");
|
||||||
|
assert_eq!(
|
||||||
|
run.status.code(),
|
||||||
|
Some(0),
|
||||||
|
"walk-forward over a bound-param axis must succeed (#246 re-open), stderr: {}",
|
||||||
|
String::from_utf8_lossy(&run.stderr)
|
||||||
|
);
|
||||||
|
let repro = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||||
|
.args(["reproduce", "wfo-0"])
|
||||||
|
.current_dir(&cwd)
|
||||||
|
.output()
|
||||||
|
.expect("spawn aura reproduce");
|
||||||
|
assert_eq!(
|
||||||
|
repro.status.code(),
|
||||||
|
Some(0),
|
||||||
|
"reproduce stderr: {}",
|
||||||
|
String::from_utf8_lossy(&repro.stderr)
|
||||||
|
);
|
||||||
|
let out = String::from_utf8(repro.stdout).expect("utf-8");
|
||||||
|
assert!(
|
||||||
|
out.contains("reproduced 3/3 members bit-identically"),
|
||||||
|
"every OOS member of a deterministic walk-forward over a bound-param axis \
|
||||||
|
re-derives bit-identically: {out}"
|
||||||
|
);
|
||||||
|
let _ = std::fs::remove_dir_all(&cwd);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Property (#246, distinct code path from `reproduce_family_with_overridden_bound_param_re_derives`):
|
||||||
|
/// `reproduce_family_in`'s override derivation (`bound_overrides_of`, computed once
|
||||||
|
/// per family from the FIRST member's recorded params) applies uniformly across
|
||||||
|
/// `FamilyKind`s, not only `WalkForward` — a plain `aura sweep` family minted by
|
||||||
|
/// overriding a bound param on the CLOSED `r_sma.json` fixture re-derives
|
||||||
|
/// bit-identically too. The sibling test exercises `FamilyKind::WalkForward`'s
|
||||||
|
/// branch (per-window OOS reload via `run_oos_blueprint`); this one exercises the
|
||||||
|
/// `Sweep`-kind branch (`data.run_sources`/`data.full_window`, no per-window
|
||||||
|
/// re-fit) of the same shared dispatch in `reproduce_family_in`. A regression that
|
||||||
|
/// threaded the #246 override set through one branch but not the other would pass
|
||||||
|
/// the walk-forward sibling while failing here.
|
||||||
|
#[test]
|
||||||
|
fn aura_reproduce_re_derives_a_sweep_family_minted_over_a_bound_param_override() {
|
||||||
|
let cwd = temp_cwd("sweep-reproduce-bound-override");
|
||||||
|
let bp = format!("{}/examples/r_sma.json", env!("CARGO_MANIFEST_DIR"));
|
||||||
|
let sweep = Command::new(BIN)
|
||||||
|
.args(["sweep", &bp, "--axis", "sma_signal.fast.length=2,3", "--name", "swo"])
|
||||||
|
.current_dir(&cwd)
|
||||||
|
.output()
|
||||||
|
.expect("spawn aura sweep over a bound-param axis");
|
||||||
|
assert_eq!(
|
||||||
|
sweep.status.code(),
|
||||||
|
Some(0),
|
||||||
|
"sweeping a bound param of a closed blueprint must succeed (#246 re-open), stderr: {}",
|
||||||
|
String::from_utf8_lossy(&sweep.stderr)
|
||||||
|
);
|
||||||
|
let repro = Command::new(BIN)
|
||||||
|
.args(["reproduce", "swo-0"])
|
||||||
|
.current_dir(&cwd)
|
||||||
|
.output()
|
||||||
|
.expect("spawn aura reproduce");
|
||||||
|
assert_eq!(
|
||||||
|
repro.status.code(),
|
||||||
|
Some(0),
|
||||||
|
"reproduce stderr: {}",
|
||||||
|
String::from_utf8_lossy(&repro.stderr)
|
||||||
|
);
|
||||||
|
let out = String::from_utf8(repro.stdout).expect("utf-8");
|
||||||
|
assert!(
|
||||||
|
out.contains("reproduced 2/2 members bit-identically"),
|
||||||
|
"every member of a plain sweep minted over a bound-param override \
|
||||||
|
re-derives bit-identically: {out}"
|
||||||
|
);
|
||||||
|
let _ = std::fs::remove_dir_all(&cwd);
|
||||||
|
}
|
||||||
|
|
||||||
/// Property: `aura reproduce` yields a handled outcome — a reproduction verdict
|
/// Property: `aura reproduce` yields a handled outcome — a reproduction verdict
|
||||||
/// or a clean `aura:` refusal — for a real-data WalkForward family, and NEVER an
|
/// or a clean `aura:` refusal — for a real-data WalkForward family, and NEVER an
|
||||||
/// internal panic. A WalkForward member's OOS window is stored as real epoch-ns
|
/// internal panic. A WalkForward member's OOS window is stored as real epoch-ns
|
||||||
@@ -4385,10 +4487,14 @@ fn aura_reproduce_rejects_a_missing_stored_blueprint() {
|
|||||||
let _ = std::fs::remove_dir_all(&cwd);
|
let _ = std::fs::remove_dir_all(&cwd);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// E2E (#169): `aura sweep <open blueprint.json> --list-axes` prints one
|
/// E2E (#169/#246): `aura sweep <open blueprint.json> --list-axes` prints one
|
||||||
/// `<name>:<kind>` line per open sweepable knob, in `param_space()` order, and
|
/// `<name>:<kind>` line per open sweepable knob, in `param_space()` order,
|
||||||
/// exits 0. The names are prefixed by the wrapping (sma_signal.*) — exactly
|
/// followed by one `<name>:<kind> default=<value>` line per BOUND param (the
|
||||||
/// the strings `--axis` binds — so a user can discover then sweep without guessing.
|
/// fixture's `bias.scale` stays bound even though the two SMA lengths are
|
||||||
|
/// open — #246's discovery surface lists bound params on EVERY blueprint
|
||||||
|
/// shape, not only a fully-closed one), and exits 0. The names are prefixed
|
||||||
|
/// by the wrapping (sma_signal.*) — exactly the strings `--axis` binds — so a
|
||||||
|
/// user can discover then sweep or re-open without guessing.
|
||||||
#[test]
|
#[test]
|
||||||
fn aura_sweep_list_axes_prints_prefixed_names() {
|
fn aura_sweep_list_axes_prints_prefixed_names() {
|
||||||
let bp = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
|
let bp = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
|
||||||
@@ -4398,20 +4504,35 @@ fn aura_sweep_list_axes_prints_prefixed_names() {
|
|||||||
.expect("spawn aura sweep --list-axes");
|
.expect("spawn aura sweep --list-axes");
|
||||||
assert_eq!(out.status.code(), Some(0));
|
assert_eq!(out.status.code(), Some(0));
|
||||||
let stdout = String::from_utf8(out.stdout).expect("utf-8");
|
let stdout = String::from_utf8(out.stdout).expect("utf-8");
|
||||||
assert_eq!(stdout, "sma_signal.fast.length:I64\nsma_signal.slow.length:I64\n");
|
assert_eq!(
|
||||||
|
stdout,
|
||||||
|
"sma_signal.fast.length:I64\n\
|
||||||
|
sma_signal.slow.length:I64\n\
|
||||||
|
sma_signal.bias.scale:F64 default=0.5\n"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// E2E (#169): `--list-axes` on a CLOSED blueprint (all knobs bound) prints
|
/// E2E (#246): `--list-axes` on a CLOSED blueprint (all knobs bound) prints one
|
||||||
/// nothing and exits 0 — an empty axis set is the honest answer, not an error.
|
/// `<bp>.<node>.<param>:<KIND> default=<value>` line per bound param — the
|
||||||
|
/// bound value IS the default, overridable by naming the same path as an
|
||||||
|
/// `--axis` (#246's re-open contract) — and exits 0. The sibling pin above
|
||||||
|
/// (`aura_sweep_list_axes_prints_prefixed_names`) proves the same bound-param
|
||||||
|
/// lines also appear on a PARTIALLY open blueprint, after its open lines.
|
||||||
#[test]
|
#[test]
|
||||||
fn aura_sweep_list_axes_closed_is_empty() {
|
fn aura_sweep_list_axes_closed_prints_bound_defaults() {
|
||||||
let bp = format!("{}/examples/r_sma.json", env!("CARGO_MANIFEST_DIR"));
|
let bp = format!("{}/examples/r_sma.json", env!("CARGO_MANIFEST_DIR"));
|
||||||
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||||
.args(["sweep", &bp, "--list-axes"])
|
.args(["sweep", &bp, "--list-axes"])
|
||||||
.output()
|
.output()
|
||||||
.expect("spawn aura sweep --list-axes (closed)");
|
.expect("spawn aura sweep --list-axes (closed)");
|
||||||
assert_eq!(out.status.code(), Some(0));
|
assert_eq!(out.status.code(), Some(0));
|
||||||
assert!(out.stdout.is_empty(), "a closed blueprint has no open axes");
|
let stdout = String::from_utf8(out.stdout).expect("utf-8");
|
||||||
|
assert_eq!(
|
||||||
|
stdout,
|
||||||
|
"sma_signal.fast.length:I64 default=2\n\
|
||||||
|
sma_signal.slow.length:I64 default=4\n\
|
||||||
|
sma_signal.bias.scale:F64 default=0.5\n"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// E2E (#169): `--list-axes` is a standalone query — combining it with a real
|
/// E2E (#169): `--list-axes` is a standalone query — combining it with a real
|
||||||
@@ -4510,26 +4631,48 @@ fn aura_sweep_list_axes_names_round_trip_into_a_working_sweep() {
|
|||||||
let cwd = temp_cwd("blueprint-list-axes-roundtrip");
|
let cwd = temp_cwd("blueprint-list-axes-roundtrip");
|
||||||
let bp = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
|
let bp = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
|
||||||
|
|
||||||
// 1) discover the axis names (do NOT hardcode them) — this is what a user reads.
|
// 1) discover the axis names + kinds (do NOT hardcode them) — this is what a user
|
||||||
|
// reads. #246: the fixture now lists a BOUND param (`bias.scale`, F64) alongside
|
||||||
|
// the two open I64 lengths, so the CSV below must be typed per discovered kind,
|
||||||
|
// not assumed integer.
|
||||||
let list = Command::new(BIN)
|
let list = Command::new(BIN)
|
||||||
.args(["sweep", &bp, "--list-axes"])
|
.args(["sweep", &bp, "--list-axes"])
|
||||||
.output()
|
.output()
|
||||||
.expect("spawn aura sweep --list-axes");
|
.expect("spawn aura sweep --list-axes");
|
||||||
assert_eq!(list.status.code(), Some(0));
|
assert_eq!(list.status.code(), Some(0));
|
||||||
let listed = String::from_utf8(list.stdout).expect("utf-8");
|
let listed = String::from_utf8(list.stdout).expect("utf-8");
|
||||||
let names: Vec<String> = listed
|
let names: Vec<(String, String)> = listed
|
||||||
.lines()
|
.lines()
|
||||||
.map(|l| l.split(':').next().expect("a `name:kind` line").to_string())
|
.map(|l| {
|
||||||
|
let mut parts = l.splitn(2, ':');
|
||||||
|
let name = parts.next().expect("a `name:kind` line").to_string();
|
||||||
|
let kind = parts
|
||||||
|
.next()
|
||||||
|
.expect("kind after ':'")
|
||||||
|
.split_whitespace()
|
||||||
|
.next()
|
||||||
|
.expect("a kind token")
|
||||||
|
.to_string();
|
||||||
|
(name, kind)
|
||||||
|
})
|
||||||
.collect();
|
.collect();
|
||||||
assert!(!names.is_empty(), "the open fixture must list >= 1 axis");
|
assert!(!names.is_empty(), "the open fixture must list >= 1 axis");
|
||||||
|
|
||||||
// 2) feed the DISCOVERED names straight back into a real sweep. Values are
|
// 2) feed the DISCOVERED names straight back into a real sweep. Values are
|
||||||
// position-assigned (first axis small, rest large) to stay in a non-degenerate
|
// position-assigned (first axis small, rest large) to stay in a non-degenerate
|
||||||
// SMA regime without re-hardcoding the names; each axis takes two values.
|
// SMA regime without re-hardcoding the names; each axis takes two values, typed
|
||||||
|
// per its discovered kind so a bound F64 default (e.g. `bias.scale`) resolves
|
||||||
|
// instead of tripping `KindMismatch` on an integer-shaped token.
|
||||||
let mut args: Vec<String> = vec!["sweep".into(), bp.clone()];
|
let mut args: Vec<String> = vec!["sweep".into(), bp.clone()];
|
||||||
for (i, name) in names.iter().enumerate() {
|
for (i, (name, kind)) in names.iter().enumerate() {
|
||||||
|
let vals: &str = match (i == 0, kind.as_str()) {
|
||||||
|
(true, "F64") => "2.0,4.0",
|
||||||
|
(true, _) => "2,4",
|
||||||
|
(false, "F64") => "8.0,16.0",
|
||||||
|
(false, _) => "8,16",
|
||||||
|
};
|
||||||
args.push("--axis".into());
|
args.push("--axis".into());
|
||||||
args.push(format!("{name}={}", if i == 0 { "2,4" } else { "8,16" }));
|
args.push(format!("{name}={vals}"));
|
||||||
}
|
}
|
||||||
let sweep = Command::new(BIN)
|
let sweep = Command::new(BIN)
|
||||||
.args(&args)
|
.args(&args)
|
||||||
|
|||||||
@@ -152,9 +152,12 @@ fn fresh_project() -> (&'static PathBuf, (RunsCleanup, MutexGuard<'static, ()>))
|
|||||||
fn project_node_open_param_sweeps_and_reproduces() {
|
fn project_node_open_param_sweeps_and_reproduces() {
|
||||||
let (dir, _g) = fresh_project();
|
let (dir, _g) = fresh_project();
|
||||||
|
|
||||||
// (a) `--list-axes` discovers the PROJECT node's own open knob. NOT gated:
|
// (a) `--list-axes` discovers the PROJECT node's own open knob, followed by
|
||||||
// enumerating axes needs no data, so the discoverability half runs on every
|
// the fixture's three BOUND params as `default=`-lines (#246: every bound
|
||||||
// host. The wrapped name is `<blueprint>.<node>.<param>`.
|
// param is an equally re-openable `--axis`, listed regardless of how many
|
||||||
|
// knobs happen to be open alongside it). NOT gated: enumerating axes needs
|
||||||
|
// no data, so the discoverability half runs on every host. The wrapped
|
||||||
|
// name is `<blueprint>.<node>.<param>`.
|
||||||
let axes = aura_in(dir, &["sweep", "blueprints/scaled_open.json", "--list-axes"]);
|
let axes = aura_in(dir, &["sweep", "blueprints/scaled_open.json", "--list-axes"]);
|
||||||
assert!(
|
assert!(
|
||||||
axes.status.success(),
|
axes.status.success(),
|
||||||
@@ -162,9 +165,13 @@ fn project_node_open_param_sweeps_and_reproduces() {
|
|||||||
String::from_utf8_lossy(&axes.stderr)
|
String::from_utf8_lossy(&axes.stderr)
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
String::from_utf8_lossy(&axes.stdout).trim(),
|
String::from_utf8_lossy(&axes.stdout),
|
||||||
"scaled_signal.gain.factor:F64",
|
"scaled_signal.gain.factor:F64\n\
|
||||||
"the project node's own param is the one discoverable sweep axis; stderr: {}",
|
scaled_signal.fast.length:I64 default=2\n\
|
||||||
|
scaled_signal.slow.length:I64 default=4\n\
|
||||||
|
scaled_signal.bias.scale:F64 default=0.5\n",
|
||||||
|
"the project node's own OPEN param is the one open axis, alongside the \
|
||||||
|
fixture's bound defaults; stderr: {}",
|
||||||
String::from_utf8_lossy(&axes.stderr)
|
String::from_utf8_lossy(&axes.stderr)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user