Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bbac29db2d | |||
| 5e32f3ccdf | |||
| 4474814fa7 | |||
| b3b7115825 | |||
| 3e1e7e21da | |||
| a3785a6ec6 | |||
| 697d81dd22 |
@@ -38,7 +38,7 @@ Invoke it as `aura <command> …` (examples below use the plain name).
|
||||
- **Family** — the set of runs one verb produces over a blueprint (a sweep grid,
|
||||
a Monte-Carlo seed set, a walk-forward window sequence). Families are persisted
|
||||
in a content-addressed store and can be listed, ranked, and reproduced.
|
||||
- **Axis** — one named, sweepable knob of a blueprint (e.g. `graph.fast.length`),
|
||||
- **Axis** — one named, sweepable knob of a blueprint (e.g. `fast.length`),
|
||||
bound with a comma-separated value list. A **gang** fuses several sibling
|
||||
knobs into one axis (one value drives all members).
|
||||
|
||||
|
||||
@@ -106,9 +106,9 @@ fn seed_blueprint(bin: &Path, dir: &Path, file: &str, name: &str) -> Result<Stri
|
||||
"sweep",
|
||||
file,
|
||||
"--axis",
|
||||
"sma_signal.fast.length=2,4",
|
||||
"fast.length=2,4",
|
||||
"--axis",
|
||||
"sma_signal.slow.length=8,16",
|
||||
"slow.length=8,16",
|
||||
"--name",
|
||||
name,
|
||||
],
|
||||
|
||||
@@ -12,6 +12,7 @@ use aura_engine::{
|
||||
BindOpError, BlueprintDoc, CompileError, Composite, GraphSession, LoadError, Op, OpError,
|
||||
Scalar, ScalarKind,
|
||||
};
|
||||
use aura_runner::runner::render_value;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::research_docs::resolve_id_prefix;
|
||||
@@ -614,10 +615,12 @@ pub fn introspect_cmd(cmd: crate::GraphIntrospectCmd, env: &aura_runner::project
|
||||
}
|
||||
}
|
||||
} else if let Some(target) = cmd.params.as_deref() {
|
||||
// --params <FILE|ID> (#196): the RAW composite param space — exactly the
|
||||
// namespace campaign axes are validated against (`validate_campaign_refs`
|
||||
// checks the raw space; the wrapped `--list-axes` namespace on `aura sweep`
|
||||
// is the sweep-verb view, not the campaign view).
|
||||
// --params <FILE|ID> (#196): the RAW composite param space — the one
|
||||
// namespace campaign axes are validated against
|
||||
// (`validate_campaign_refs`) and `aura sweep --list-axes` prints
|
||||
// (#328: the wrapped `<blueprint>.<node>.<param>` form was retired —
|
||||
// there is exactly one axis namespace now, so the two discovery
|
||||
// surfaces agree line-for-line, see `params_lines`'s own doc comment).
|
||||
match params_lines(target, env) {
|
||||
Ok(s) => print!("{s}"),
|
||||
Err(m) => {
|
||||
@@ -864,7 +867,11 @@ fn resolve_blueprint_text(target: &str, env: &aura_runner::project::Env) -> Resu
|
||||
/// line per open param of the RAW composite (no harness wrap) — the
|
||||
/// campaign-axis namespace `validate_campaign_refs` checks axes against. The
|
||||
/// kind renders via `ScalarKind`'s `Debug` (`I64`/`F64`/`Bool`/`Timestamp`),
|
||||
/// the same form `introspect --node` already uses for param kinds.
|
||||
/// the same form `introspect --node` already uses for param kinds. Followed
|
||||
/// by one `{name}:{kind:?} default={value}` line per BOUND param (#328):
|
||||
/// `bound_param_space()`'s names are already RAW (#203), so no blueprint-name
|
||||
/// concatenation is needed — line-identical to the reconciled `--list-axes`
|
||||
/// bound pass (`list_blueprint_axes`, main.rs), same `render_value` lexicon.
|
||||
fn params_lines(target: &str, env: &aura_runner::project::Env) -> Result<String, String> {
|
||||
use std::fmt::Write as _;
|
||||
let text = resolve_blueprint_text(target, env)?;
|
||||
@@ -875,6 +882,9 @@ fn params_lines(target: &str, env: &aura_runner::project::Env) -> Result<String,
|
||||
for p in composite.param_space() {
|
||||
let _ = writeln!(out, "{}:{:?}", p.name, p.kind);
|
||||
}
|
||||
for b in composite.bound_param_space() {
|
||||
let _ = writeln!(out, "{}:{:?} default={}", b.name, b.kind, render_value(&b.value));
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
|
||||
+120
-54
@@ -45,7 +45,7 @@ use aura_measurement::information_coefficient;
|
||||
// `blueprint_axis_probe_reopened` is production code only from `verb_sugar.rs`
|
||||
// (a sibling module reaching it via `crate::blueprint_axis_probe_reopened`,
|
||||
// the crate-root re-export this `use` gives it), not from this module itself.
|
||||
use aura_runner::member::{blueprint_axis_probe, blueprint_axis_probe_reopened, run_signal_r, wrapped_bound_names, RunData};
|
||||
use aura_runner::member::{blueprint_axis_probe, blueprint_axis_probe_reopened, run_signal_r, RunData};
|
||||
use aura_runner::{TapPlan, TapSubscription};
|
||||
#[cfg(test)]
|
||||
use aura_runner::member::{run_blueprint_member, wrap_r, SYNTHETIC_PIP_SIZE};
|
||||
@@ -813,17 +813,67 @@ impl IcReport {
|
||||
/// 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.
|
||||
/// are RAW `<node>.<param>` paths (#328: the wrapped `<blueprint>.<node>.<param>`
|
||||
/// form is retired from the surface) — exactly what `--axis` binds.
|
||||
fn list_blueprint_axes(doc: &str, env: &aura_runner::project::Env) {
|
||||
let space = blueprint_axis_probe(doc, env).param_space();
|
||||
for p in &space {
|
||||
println!("{}:{:?}", p.name, p.kind); // ScalarKind Debug -> I64/F64/Bool/Timestamp
|
||||
// Strip the wrapper's one leading node segment (#328): open names print
|
||||
// RAW, matching `--axis`'s own accepted namespace.
|
||||
println!("{}:{:?}", aura_runner::axes::wrapped_to_raw_axis(&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));
|
||||
// `bound_param_space()`'s `.name` is already RAW (#203) — no blueprint-name
|
||||
// concatenation (#328: the blueprint name stays out of axis paths, C23).
|
||||
println!("{}:{:?} default={}", b.name, b.kind, render_value(&b.value));
|
||||
}
|
||||
}
|
||||
|
||||
/// The one-sentence prose for a refused WRAPPED `--axis` name (#328), shared
|
||||
/// by both intake routes so the wording carried by `refuse_wrapped_synthetic_axes`
|
||||
/// (the synthetic-sweep preflight) and `validate_and_register_axes` (the
|
||||
/// real-route/campaign preflight) cannot drift into two copies. Returns the
|
||||
/// message WITHOUT the leading `aura: ` — each caller prepends that itself
|
||||
/// (the synthetic route via its own `eprintln!`, the real route via
|
||||
/// `exit_axis_register_error`'s shared `eprintln!("aura: {m}")`), so the
|
||||
/// rendered bytes on both routes stay byte-identical.
|
||||
fn wrapped_axis_refusal(n: &str, raw: &str) -> String {
|
||||
format!(
|
||||
"axis \"{n}\": axis names are raw node.param paths — \
|
||||
use \"{raw}\" (the wrapped --list-axes form was retired, #328)"
|
||||
)
|
||||
}
|
||||
|
||||
/// `--axis` intake preflight for the SYNTHETIC (no `--real`) blueprint-sweep
|
||||
/// route (#328): this route bypasses `validate_and_register_axes` entirely (no
|
||||
/// project/registry touched), so it needs its own copy of the same WRAPPED-name
|
||||
/// refusal — the shared [`aura_runner::axes::classify_axis_intake`] predicate,
|
||||
/// intercepting only a `WrappedRetired` hit (echoing the translation pointer,
|
||||
/// exit 2, before the sweep ever runs). A name matching neither namespace is
|
||||
/// deliberately left unrefused HERE: `blueprint_sweep_family`'s own
|
||||
/// `override_paths` rejects it downstream with today's unchanged prose, so this
|
||||
/// preflight does not duplicate that message under a second wording.
|
||||
fn refuse_wrapped_synthetic_axes(
|
||||
doc: &str,
|
||||
env: &aura_runner::project::Env,
|
||||
axes: &[(String, Vec<Scalar>)],
|
||||
) {
|
||||
let wrapped_open = blueprint_axis_probe(doc, env).param_space();
|
||||
let raw_bound: HashSet<String> = blueprint_from_json(doc, &|t| env.resolve(t))
|
||||
.expect("doc parse-validated at the dispatch boundary")
|
||||
.bound_param_space()
|
||||
.into_iter()
|
||||
.map(|b| b.name)
|
||||
.collect();
|
||||
for (n, _) in axes {
|
||||
if let aura_runner::axes::AxisIntake::WrappedRetired(raw) =
|
||||
aura_runner::axes::classify_axis_intake(n, &wrapped_open, &raw_bound)
|
||||
{
|
||||
eprintln!("aura: {}", wrapped_axis_refusal(n, &raw));
|
||||
std::process::exit(2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1661,14 +1711,17 @@ enum AxisRegisterError {
|
||||
NoProject(String),
|
||||
}
|
||||
|
||||
/// Validate every `--axis` name against the blueprint's WRAPPED probe namespace
|
||||
/// (`blueprint_axis_probe`/`--list-axes`) — an axis naming a BOUND param (#246:
|
||||
/// re-openable, same as every already-open knob) passes exactly like an open
|
||||
/// one; only a name matching NEITHER space is refused — then canonicalize +
|
||||
/// register the blueprint by topology hash and strip every axis name to the RAW
|
||||
/// campaign namespace (the sweep sequence, #210 c0110). A raw-form or
|
||||
/// fat-fingered axis name is refused here, echoing exactly what the user typed,
|
||||
/// before the archive is touched or the blueprint is registered — shared by
|
||||
/// Validate every `--axis` name against the RAW campaign-axis namespace only
|
||||
/// (#328: `--list-axes`'s own namespace, open-or-bound) — a WRAPPED name (the
|
||||
/// retired `<blueprint>.<node>.<param>` form) is refused with a translation
|
||||
/// pointer to its raw candidate; a name in neither namespace gets today's
|
||||
/// unmatched-axis prose, unchanged. [`aura_runner::axes::classify_axis_intake`]
|
||||
/// is the shared predicate (deliberately not `raw_matches_wrapped`, whose own
|
||||
/// equality branch would silently accept a wrapped name — see its doc
|
||||
/// comment). Then canonicalize + register the blueprint by topology hash — no
|
||||
/// further strip: the axes are already raw by the time this returns them (the
|
||||
/// old wrapped->raw strip is gone, since a splice path's own dots, e.g.
|
||||
/// `anchor.sess.period_minutes`, must not be mangled a second time). Shared by
|
||||
/// every campaign-path dispatcher (sweep/generalize/walkforward/mc all landed
|
||||
/// the identical block; #220 slice-1 deferred this dedup to "once wf/mc land
|
||||
/// the same block", rule-of-three now exceeded 4x). The override set itself is
|
||||
@@ -1682,17 +1735,24 @@ fn validate_and_register_axes(
|
||||
axes: &[(String, Vec<Scalar>)],
|
||||
env: &aura_runner::project::Env,
|
||||
) -> Result<(String, Vec<(String, Vec<Scalar>)>), AxisRegisterError> {
|
||||
let space = blueprint_axis_probe(doc, env).param_space();
|
||||
let wrapped_open = blueprint_axis_probe(doc, env).param_space();
|
||||
let blueprint = blueprint_from_json(doc, &|t| env.resolve(t))
|
||||
.expect("doc parse-validated at the dispatch boundary");
|
||||
let bound = wrapped_bound_names(&blueprint);
|
||||
let raw_bound: HashSet<String> =
|
||||
blueprint.bound_param_space().into_iter().map(|b| b.name).collect();
|
||||
for (n, _) in axes {
|
||||
if !space.iter().any(|p| &p.name == n) && !bound.contains(n) {
|
||||
return Err(AxisRegisterError::UnknownAxis(format!(
|
||||
"axis \"{n}\" is not one of this blueprint's \
|
||||
sweepable axes — run 'aura sweep <bp> --list-axes' \
|
||||
to see them"
|
||||
)));
|
||||
match aura_runner::axes::classify_axis_intake(n, &wrapped_open, &raw_bound) {
|
||||
aura_runner::axes::AxisIntake::Raw => {}
|
||||
aura_runner::axes::AxisIntake::WrappedRetired(raw) => {
|
||||
return Err(AxisRegisterError::UnknownAxis(wrapped_axis_refusal(n, &raw)));
|
||||
}
|
||||
aura_runner::axes::AxisIntake::Unknown => {
|
||||
return Err(AxisRegisterError::UnknownAxis(format!(
|
||||
"axis \"{n}\" is not one of this blueprint's \
|
||||
sweepable axes — run 'aura sweep <bp> --list-axes' \
|
||||
to see them"
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
if env.provenance().is_none() {
|
||||
@@ -1709,11 +1769,7 @@ fn validate_and_register_axes(
|
||||
let topo = topology_hash(&blueprint);
|
||||
reg.put_blueprint(&topo, &canonical)
|
||||
.map_err(|e| AxisRegisterError::Registry(e.to_string()))?;
|
||||
let raw_axes: Vec<(String, Vec<Scalar>)> = axes
|
||||
.iter()
|
||||
.map(|(n, v)| (aura_runner::axes::wrapped_to_raw_axis(n).to_string(), v.clone()))
|
||||
.collect();
|
||||
Ok((canonical, raw_axes))
|
||||
Ok((canonical, axes.to_vec()))
|
||||
}
|
||||
|
||||
/// Exit-map for a `validate_and_register_axes` failure — the stderr line and
|
||||
@@ -2326,8 +2382,8 @@ fn dispatch_sweep(a: SweepCmd, env: &aura_runner::project::Env) {
|
||||
// strategy ref resolves against this one write; a second
|
||||
// put under `content_id_of(&canonical)` would target the
|
||||
// identical key and is not needed.
|
||||
// Axis validation + canonicalize/register + the wrapped->raw
|
||||
// strip are single-sourced in `validate_and_register_axes`
|
||||
// Axis validation (RAW-only, #328) + canonicalize/register are
|
||||
// single-sourced in `validate_and_register_axes`
|
||||
// (data-free, so this fires before the archive is touched).
|
||||
let (canonical, raw_axes) = validate_and_register_axes("sweep", &doc, &axes, env)
|
||||
.unwrap_or_else(|e| exit_axis_register_error(e));
|
||||
@@ -2361,6 +2417,10 @@ fn dispatch_sweep(a: SweepCmd, env: &aura_runner::project::Env) {
|
||||
);
|
||||
std::process::exit(2);
|
||||
}
|
||||
// #328: this route bypasses `validate_and_register_axes` (no
|
||||
// project/registry involved), so it needs its own WRAPPED-name
|
||||
// refusal ahead of the sweep.
|
||||
refuse_wrapped_synthetic_axes(&doc, env, &axes);
|
||||
run_blueprint_sweep(
|
||||
&doc, &axes, &name, persist,
|
||||
DataSource::from_choice(data, env), env,
|
||||
@@ -2418,8 +2478,8 @@ fn dispatch_walkforward(a: WalkforwardCmd, env: &aura_runner::project::Env) {
|
||||
eprintln!("aura: {m}");
|
||||
std::process::exit(2);
|
||||
});
|
||||
// Axis validation + canonicalize/register + the wrapped->raw
|
||||
// strip are single-sourced in `validate_and_register_axes`
|
||||
// Axis validation (RAW-only, #328) + canonicalize/register are
|
||||
// single-sourced in `validate_and_register_axes`
|
||||
// (data-free, so this fires before the archive is touched).
|
||||
let (canonical, raw_axes) = validate_and_register_axes("walkforward", &doc, &axes, env)
|
||||
.unwrap_or_else(|e| exit_axis_register_error(e));
|
||||
@@ -2463,10 +2523,11 @@ fn dispatch_walkforward(a: WalkforwardCmd, env: &aura_runner::project::Env) {
|
||||
exit_on_campaign_result(result);
|
||||
return;
|
||||
}
|
||||
// Synthetic in-process family path — unchanged (#220 non-goal); it
|
||||
// persists no per-window taps, so `--trace` stays refused here (#224
|
||||
// delivered only the `--real` campaign path above), mirroring
|
||||
// `dispatch_sweep`'s synthetic-arm refusal with its own named pointer.
|
||||
// Synthetic in-process family path (#220 non-goal, unchanged beyond
|
||||
// the #328 tidy below); it persists no per-window taps, so
|
||||
// `--trace` stays refused here (#224 delivered only the `--real`
|
||||
// campaign path above), mirroring `dispatch_sweep`'s synthetic-arm
|
||||
// refusal with its own named pointer.
|
||||
if a.trace.is_some() {
|
||||
eprintln!(
|
||||
"aura: --trace is not yet available on a synthetic walkforward (no --real); see #224"
|
||||
@@ -2485,6 +2546,10 @@ fn dispatch_walkforward(a: WalkforwardCmd, env: &aura_runner::project::Env) {
|
||||
None => Selection::Argmax,
|
||||
};
|
||||
let name = a.name.clone().unwrap_or_else(|| "walkforward".to_string());
|
||||
// #328: this route bypasses `validate_and_register_axes` (no
|
||||
// project/registry involved, same gap the plain synthetic sweep
|
||||
// arm had) — the same WRAPPED-name preflight closes it here too.
|
||||
refuse_wrapped_synthetic_axes(&doc, env, &axes);
|
||||
run_blueprint_walkforward(&doc, &axes, &name, DataSource::Synthetic, select, env);
|
||||
}
|
||||
None => {
|
||||
@@ -2532,8 +2597,8 @@ fn dispatch_mc(a: McCmd, env: &aura_runner::project::Env) {
|
||||
eprintln!("aura: {}", usage());
|
||||
std::process::exit(2);
|
||||
}
|
||||
// Axis validation + canonicalize/register + the wrapped->raw
|
||||
// strip are single-sourced in `validate_and_register_axes`
|
||||
// Axis validation (RAW-only, #328) + canonicalize/register are
|
||||
// single-sourced in `validate_and_register_axes`
|
||||
// (data-free, so this fires before the archive is touched).
|
||||
let (canonical, raw_axes) = validate_and_register_axes("mc", &doc, &axes, env)
|
||||
.unwrap_or_else(|e| exit_axis_register_error(e));
|
||||
@@ -3532,11 +3597,11 @@ mod tests {
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
/// #246: a fully-bound (closed) blueprint IS sweepable — an 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).
|
||||
/// #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();
|
||||
@@ -3544,9 +3609,9 @@ mod tests {
|
||||
let closed = load_closed_r_sma();
|
||||
let doc = blueprint_to_json(&closed).expect("serializes");
|
||||
|
||||
// (a) override axis: two members, no refusal
|
||||
// (a) override axis (RAW name, #328): two members, no refusal
|
||||
let axes = vec![(
|
||||
"sma_signal.fast.length".to_string(),
|
||||
"fast.length".to_string(),
|
||||
vec![Scalar::i64(2), Scalar::i64(4)],
|
||||
)];
|
||||
let fam = blueprint_sweep_family(&doc, &axes, &DataSource::Synthetic, &env)
|
||||
@@ -3554,7 +3619,7 @@ mod tests {
|
||||
assert_eq!(fam.points.len(), 2);
|
||||
|
||||
// (b) unknown axis: the boundary message, no UnknownKnob leak
|
||||
let bad = vec![("sma_signal.nope".to_string(), vec![Scalar::i64(1)])];
|
||||
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}");
|
||||
@@ -3562,35 +3627,36 @@ mod tests {
|
||||
assert!(!err.contains("UnknownKnob"), "must not leak the debug render: {err}");
|
||||
}
|
||||
|
||||
/// #249: an axis-reopened bound param flows through `params` ("what
|
||||
/// #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` 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.
|
||||
/// 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![("sma_signal.fast.length".to_string(), vec![Scalar::i64(2)])];
|
||||
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(&"sma_signal.fast.length"), "swept axis is a param: {param_names:?}");
|
||||
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(&"sma_signal.fast.length"),
|
||||
!default_names.contains(&"fast.length"),
|
||||
"the reopened default must not also appear in defaults: {default_names:?}"
|
||||
);
|
||||
assert_eq!(
|
||||
default_names,
|
||||
["sma_signal.slow.length", "sma_signal.bias.scale"],
|
||||
"the untouched bound params stay in defaults: {default_names:?}"
|
||||
["slow.length", "bias.scale"],
|
||||
"the untouched bound params stay in defaults, RAW: {default_names:?}"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -462,8 +462,14 @@ pub(crate) fn ref_fault_prose(f: &RefFault) -> String {
|
||||
RefFault::StrategyUnloadable { id, error } => {
|
||||
format!("strategy {id} cannot be loaded: {error}")
|
||||
}
|
||||
RefFault::AxisNotInParamSpace { strategy, axis } => {
|
||||
format!("strategy {strategy}: axis \"{axis}\" is not in the param space")
|
||||
RefFault::AxisNotInParamSpace { strategy, axis, raw_candidate } => {
|
||||
let mut msg = format!("strategy {strategy}: axis \"{axis}\" is not in the param space");
|
||||
if let Some(candidate) = raw_candidate {
|
||||
msg.push_str(&format!(
|
||||
"; axis names are raw node.param paths — did you mean \"{candidate}\"?"
|
||||
));
|
||||
}
|
||||
msg
|
||||
}
|
||||
RefFault::AxisKindMismatch { strategy, axis } => {
|
||||
format!("strategy {strategy}: axis \"{axis}\" declares a kind that is not the param's kind")
|
||||
@@ -761,6 +767,7 @@ mod tests {
|
||||
ref_fault_prose(&RefFault::AxisNotInParamSpace {
|
||||
strategy: "9f3a".into(),
|
||||
axis: "nope".into(),
|
||||
raw_candidate: None,
|
||||
}),
|
||||
ref_fault_prose(&RefFault::AxisKindMismatch {
|
||||
strategy: "9f3a".into(),
|
||||
@@ -781,6 +788,39 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// #328: `ref_fault_prose` appends the did-you-mean clause, contiguous
|
||||
/// and verbatim, exactly when `raw_candidate` is present — the document-
|
||||
/// side refusal seam's mirror of the sweep `--axis` intake translation
|
||||
/// (`aura-runner`'s `axes.rs` classify predicate), sharing the same
|
||||
/// `axis names are raw node.param paths` clause pinned there.
|
||||
#[test]
|
||||
fn ref_fault_prose_renders_the_did_you_mean_when_a_raw_candidate_is_present() {
|
||||
let msg = ref_fault_prose(&RefFault::AxisNotInParamSpace {
|
||||
strategy: "9f3a".into(),
|
||||
axis: "graph.fast.length".into(),
|
||||
raw_candidate: Some("fast.length".into()),
|
||||
});
|
||||
assert_eq!(
|
||||
msg,
|
||||
"strategy 9f3a: axis \"graph.fast.length\" is not in the param space; \
|
||||
axis names are raw node.param paths — did you mean \"fast.length\"?"
|
||||
);
|
||||
}
|
||||
|
||||
/// #328 (negative): a rejected axis with no `raw_candidate` (stripping one
|
||||
/// leading segment yields no param-space hit, or the axis has no leading
|
||||
/// segment to strip at all) renders today's prose unchanged — no
|
||||
/// speculative suggestion.
|
||||
#[test]
|
||||
fn ref_fault_prose_omits_the_did_you_mean_when_no_raw_candidate() {
|
||||
let msg = ref_fault_prose(&RefFault::AxisNotInParamSpace {
|
||||
strategy: "9f3a".into(),
|
||||
axis: "nope".into(),
|
||||
raw_candidate: None,
|
||||
});
|
||||
assert_eq!(msg, "strategy 9f3a: axis \"nope\" is not in the param space");
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// #194 (the prefix trap): a doc ref that carries the display `content:`
|
||||
/// prefix (a copy-paste from register/introspect output) is bare-only in
|
||||
|
||||
@@ -234,8 +234,9 @@ std vocabulary, anchored by `Aura.toml`. There is no crate and no build step.
|
||||
- Run: `aura run blueprints/signal.json` (the starter is closed — all
|
||||
params bound; bound values are defaults — any `--axis` may override them
|
||||
(#246))
|
||||
- Sweep: `aura sweep blueprints/signal.json --axis __NAME_SNAKE___signal.fast.length=2,4,8`
|
||||
(`aura sweep <bp> --list-axes` lists the open + bound-overridable axes)
|
||||
- Sweep: `aura sweep blueprints/signal.json --axis fast.length=2,4,8`
|
||||
(`aura sweep <bp> --list-axes` lists the open + bound-overridable axes, RAW
|
||||
`<node>.<param>` names — #328)
|
||||
- Native nodes: when the project needs its first project-specific node,
|
||||
`aura nodes new <name>` scaffolds a node crate beside this project and
|
||||
attaches it via `[nodes]` in `Aura.toml` (build it with `cargo build`).
|
||||
@@ -459,10 +460,11 @@ mod tests {
|
||||
let claude = render_project(CLAUDE_MD_PROJECT, "demo-lab");
|
||||
assert!(claude.contains("demo-lab"));
|
||||
// The CLAUDE.md sweep quickstart targets the closed starter itself
|
||||
// with the rendered blueprint-name axis prefix (#246: a bound param
|
||||
// is a default an axis overrides).
|
||||
// with the RAW axis name (#246: a bound param is a default an axis
|
||||
// overrides; #328: the axis namespace is raw, no blueprint-name
|
||||
// prefix).
|
||||
assert!(claude.contains("blueprints/signal.json"));
|
||||
assert!(claude.contains("demo_lab_signal.fast.length"));
|
||||
assert!(claude.contains("--axis fast.length"));
|
||||
}
|
||||
|
||||
/// #315: the scaffolded project CLAUDE.md teaches the execution semantics
|
||||
|
||||
+317
-198
File diff suppressed because it is too large
Load Diff
@@ -660,16 +660,44 @@ fn register_refuses_undescribed_composites_end_to_end() {
|
||||
|
||||
/// Property (#196, the campaign-axis namespace): `--params <FILE>` prints the
|
||||
/// RAW composite param space — one `{name}:{kind:?}` line per open param, in
|
||||
/// lowering order, WITHOUT the harness-wrap prefix `aura sweep --list-axes`
|
||||
/// shows. The open fixture leaves exactly the two SMA lengths unbound
|
||||
/// (`bias.scale` is bound in the document).
|
||||
/// lowering order (#328: the one raw axis namespace — `aura sweep
|
||||
/// --list-axes` prints the identical lines for the same blueprint, see
|
||||
/// `graph_params_and_sweep_list_axes_are_line_identical` below). The open
|
||||
/// fixture leaves exactly the two SMA lengths unbound (`bias.scale` is bound
|
||||
/// in the document).
|
||||
#[test]
|
||||
fn graph_params_lists_raw_axis_namespace() {
|
||||
let dir = temp_cwd("params");
|
||||
let (stdout, stderr, code) =
|
||||
run_in(&dir, &["graph", "introspect", "--params", &fixture("r_sma_open.json")]);
|
||||
assert_eq!(code, Some(0), "stdout: {stdout} stderr: {stderr}");
|
||||
assert_eq!(stdout, "fast.length:I64\nslow.length:I64\n", "the raw open params, in order");
|
||||
assert_eq!(
|
||||
stdout, "fast.length:I64\nslow.length:I64\nbias.scale:F64 default=0.5\n",
|
||||
"the raw open params, then the raw bound param with its default"
|
||||
);
|
||||
}
|
||||
|
||||
/// Property (#328, cycle-close tidy fix): `graph introspect --params` and
|
||||
/// `aura sweep --list-axes` are two independently-dispatched discovery
|
||||
/// surfaces over the SAME raw axis namespace — the open pass shares one
|
||||
/// wrap-strip convention, the bound pass shares one `bound_param_space()` +
|
||||
/// `render_value` lexicon (`list_blueprint_axes`, main.rs; `params_lines`,
|
||||
/// this crate). Pinned here as a same-fixture LINE EQUALITY (not just two
|
||||
/// independently-asserted shapes) so the duplicated formatting cannot
|
||||
/// silently drift apart across an edit to either surface.
|
||||
#[test]
|
||||
fn graph_params_and_sweep_list_axes_are_line_identical() {
|
||||
let dir = temp_cwd("params-vs-list-axes");
|
||||
let bp = fixture("r_sma_open.json");
|
||||
let (params_out, params_err, params_code) =
|
||||
run_in(&dir, &["graph", "introspect", "--params", &bp]);
|
||||
assert_eq!(params_code, Some(0), "graph introspect --params: {params_out} {params_err}");
|
||||
let (axes_out, axes_err, axes_code) = run_in(&dir, &["sweep", &bp, "--list-axes"]);
|
||||
assert_eq!(axes_code, Some(0), "sweep --list-axes: {axes_out} {axes_err}");
|
||||
assert_eq!(
|
||||
params_out, axes_out,
|
||||
"the two discovery surfaces must print byte-identical lines for the same blueprint"
|
||||
);
|
||||
}
|
||||
|
||||
/// Property (#196, file-mode --content-id): a blueprint FILE's printed content
|
||||
@@ -719,7 +747,10 @@ fn graph_params_by_content_id_matches_params_by_file() {
|
||||
run_in(&dir, &["graph", "introspect", "--params", &bp]);
|
||||
assert_eq!(by_file_code, Some(0), "stdout: {by_file_out} stderr: {by_file_err}");
|
||||
assert_eq!(by_id_out, by_file_out, "by-id and by-file agree on the raw axis namespace");
|
||||
assert_eq!(by_id_out, "fast.length:I64\nslow.length:I64\n", "the raw open params, in order");
|
||||
assert_eq!(
|
||||
by_id_out, "fast.length:I64\nslow.length:I64\nbias.scale:F64 default=0.5\n",
|
||||
"the raw open params, then the raw bound param with its default"
|
||||
);
|
||||
}
|
||||
|
||||
/// #194 (the prefix trap): a `--params <ID>` target may carry the display
|
||||
@@ -736,7 +767,10 @@ fn graph_params_tolerates_content_prefix_on_target() {
|
||||
let (pfx_out, pfx_err, pfx_code) =
|
||||
run_in(&dir, &["graph", "introspect", "--params", &format!("content:{id}")]);
|
||||
assert_eq!(pfx_code, Some(0), "content:-prefixed --params must resolve: stdout {pfx_out} stderr {pfx_err}");
|
||||
assert_eq!(pfx_out, "fast.length:I64\nslow.length:I64\n", "same raw axis namespace as the bare id");
|
||||
assert_eq!(
|
||||
pfx_out, "fast.length:I64\nslow.length:I64\nbias.scale:F64 default=0.5\n",
|
||||
"same raw axis namespace as the bare id"
|
||||
);
|
||||
}
|
||||
|
||||
/// Property (#196, negative): `--params <ID>` with a well-shaped 64-hex id
|
||||
@@ -965,7 +999,10 @@ fn shipped_r_sma_example_is_genuinely_closed() {
|
||||
let (stdout, stderr, code) =
|
||||
run_in(&dir, &["graph", "introspect", "--params", &example("r_sma.json")]);
|
||||
assert_eq!(code, Some(0), "stdout: {stdout} stderr: {stderr}");
|
||||
assert_eq!(stdout, "", "the closed example must leave zero params unbound");
|
||||
assert_eq!(
|
||||
stdout, "fast.length:I64 default=2\nslow.length:I64 default=4\nbias.scale:F64 default=0.5\n",
|
||||
"zero OPEN params — every knob is now printed as a raw bound default instead"
|
||||
);
|
||||
}
|
||||
|
||||
/// Property (#159): the open fixture (`tests/fixtures/r_sma_open.json`) is a
|
||||
@@ -983,8 +1020,8 @@ fn open_r_sma_fixture_lists_its_axis_namespace() {
|
||||
run_in(&dir, &["graph", "introspect", "--params", &fixture("r_sma_open.json")]);
|
||||
assert_eq!(code, Some(0), "stdout: {stdout} stderr: {stderr}");
|
||||
assert_eq!(
|
||||
stdout, "fast.length:I64\nslow.length:I64\n",
|
||||
"the raw open params, in order, from the public gallery copy"
|
||||
stdout, "fast.length:I64\nslow.length:I64\nbias.scale:F64 default=0.5\n",
|
||||
"the raw open params, then the raw bound param, from the public gallery copy"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1021,12 +1058,17 @@ fn shipped_r_breakout_example_is_genuinely_closed() {
|
||||
let (stdout, stderr, code) =
|
||||
run_in(&dir, &["graph", "introspect", "--params", &example("r_breakout.json")]);
|
||||
assert_eq!(code, Some(0), "stdout: {stdout} stderr: {stderr}");
|
||||
assert_eq!(stdout, "", "the closed example must leave zero params unbound");
|
||||
assert_eq!(
|
||||
stdout,
|
||||
"delay.lag:I64 default=1\nchannel_hi.length:I64 default=3\nchannel_lo.length:I64 default=3\n",
|
||||
"zero OPEN params — every knob is now printed as a raw bound default instead"
|
||||
);
|
||||
}
|
||||
|
||||
/// Property (#159 cut 2): the open fixture (`tests/fixtures/r_breakout_open.json`)
|
||||
/// lists its ONE ganged channel axis (#61: the two rolling windows are structurally
|
||||
/// one knob).
|
||||
/// one knob) followed by the still-bound `delay.lag` default (#328: bound params
|
||||
/// are listed too, line-identical to `--list-axes`).
|
||||
#[test]
|
||||
fn open_r_breakout_fixture_lists_its_axis_namespace() {
|
||||
let dir = temp_cwd("r-breakout-example-open-params");
|
||||
@@ -1034,8 +1076,9 @@ fn open_r_breakout_fixture_lists_its_axis_namespace() {
|
||||
run_in(&dir, &["graph", "introspect", "--params", &fixture("r_breakout_open.json")]);
|
||||
assert_eq!(code, Some(0), "stdout: {stdout} stderr: {stderr}");
|
||||
assert_eq!(
|
||||
stdout, "channel_length:I64\n",
|
||||
"the ganged channel knob — one public axis for the two rolling windows (#61)"
|
||||
stdout, "channel_length:I64\ndelay.lag:I64 default=1\n",
|
||||
"the ganged channel knob — one public axis for the two rolling windows (#61) — \
|
||||
then the still-bound delay.lag default"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1064,7 +1107,7 @@ fn open_r_breakout_fixture_gang_axis_matches_the_closed_example() {
|
||||
let sweep_dir = temp_cwd("r-breakout-gang-axis-sweep");
|
||||
let (sweep_stdout, sweep_stderr, sweep_code) = run_in(
|
||||
&sweep_dir,
|
||||
&["sweep", &fixture("r_breakout_open.json"), "--axis", "r_breakout_signal.channel_length=3"],
|
||||
&["sweep", &fixture("r_breakout_open.json"), "--axis", "channel_length=3"],
|
||||
);
|
||||
assert_eq!(sweep_code, Some(0), "sweep stderr: {sweep_stderr}");
|
||||
let lines: Vec<&str> = sweep_stdout.lines().collect();
|
||||
@@ -1086,7 +1129,11 @@ fn shipped_r_meanrev_example_is_genuinely_closed() {
|
||||
let (stdout, stderr, code) =
|
||||
run_in(&dir, &["graph", "introspect", "--params", &example("r_meanrev.json")]);
|
||||
assert_eq!(code, Some(0), "stdout: {stdout} stderr: {stderr}");
|
||||
assert_eq!(stdout, "", "the closed example must leave zero params unbound");
|
||||
assert_eq!(
|
||||
stdout,
|
||||
"mean_window.length:I64 default=3\nvar_window.length:I64 default=3\nband.factor:F64 default=2\n",
|
||||
"zero OPEN params — every knob is now printed as a raw bound default instead"
|
||||
);
|
||||
}
|
||||
|
||||
/// Property (#159 cut 3): the open fixture (`tests/fixtures/r_meanrev_open.json`)
|
||||
@@ -1124,8 +1171,8 @@ fn open_r_meanrev_fixture_gang_plus_plain_axis_matches_the_closed_example() {
|
||||
&sweep_dir,
|
||||
&[
|
||||
"sweep", &fixture("r_meanrev_open.json"),
|
||||
"--axis", "r_meanrev_signal.window=3",
|
||||
"--axis", "r_meanrev_signal.band.factor=2.0",
|
||||
"--axis", "window=3",
|
||||
"--axis", "band.factor=2.0",
|
||||
],
|
||||
);
|
||||
assert_eq!(sweep_code, Some(0), "sweep stderr: {sweep_stderr}");
|
||||
@@ -1890,8 +1937,8 @@ fn graph_build_use_end_to_end_axes() {
|
||||
run_in(&dir, &["sweep", consumer_bp.to_str().unwrap(), "--list-axes"]);
|
||||
assert_eq!(axes_code, Some(0), "list-axes: {axes_out} {axes_err}");
|
||||
assert_eq!(
|
||||
axes_out, "graph.trend.sma.length:I64\n",
|
||||
"the spliced instance's open param surfaces path-qualified, bare (unbound) form"
|
||||
axes_out, "trend.sma.length:I64\n",
|
||||
"the spliced instance's open param surfaces path-qualified, bare (unbound) RAW form (#328)"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -117,9 +117,9 @@ fn project_registry_anchors_at_discovered_root_not_invocation_cwd() {
|
||||
"sweep",
|
||||
&closed_bp,
|
||||
"--axis",
|
||||
"sma_signal.fast.length=2,4",
|
||||
"fast.length=2,4",
|
||||
"--axis",
|
||||
"sma_signal.slow.length=8,16",
|
||||
"slow.length=8,16",
|
||||
"--name",
|
||||
"proj-anchor",
|
||||
])
|
||||
|
||||
@@ -135,7 +135,7 @@ fn data_only_project_sweeps_without_any_build() {
|
||||
assert!(new.status.success(), "{}", String::from_utf8_lossy(&new.stderr));
|
||||
let proj = base.join("scratch");
|
||||
let out = aura(
|
||||
&["sweep", "blueprints/signal.json", "--axis", "scratch_signal.fast.length=2,4"],
|
||||
&["sweep", "blueprints/signal.json", "--axis", "fast.length=2,4"],
|
||||
&proj,
|
||||
);
|
||||
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
|
||||
@@ -157,7 +157,7 @@ fn data_only_project_sweep_over_the_starter_opens_one_member_per_axis_value() {
|
||||
assert!(new.status.success(), "{}", String::from_utf8_lossy(&new.stderr));
|
||||
let proj = base.join("scratch");
|
||||
let out = aura(
|
||||
&["sweep", "blueprints/signal.json", "--axis", "scratch_signal.fast.length=2,4,8"],
|
||||
&["sweep", "blueprints/signal.json", "--axis", "fast.length=2,4,8"],
|
||||
&proj,
|
||||
);
|
||||
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
|
||||
|
||||
@@ -160,8 +160,8 @@ fn project_node_open_param_sweeps_and_reproduces() {
|
||||
// the fixture's three BOUND params as `default=`-lines (#246: every bound
|
||||
// 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>`.
|
||||
// no data, so the discoverability half runs on every host. The name is RAW
|
||||
// `<node>.<param>` (#328: the blueprint name stays out of axis paths).
|
||||
let axes = aura_in(dir, &["sweep", "blueprints/scaled_open.json", "--list-axes"]);
|
||||
assert!(
|
||||
axes.status.success(),
|
||||
@@ -170,10 +170,10 @@ fn project_node_open_param_sweeps_and_reproduces() {
|
||||
);
|
||||
assert_eq!(
|
||||
String::from_utf8_lossy(&axes.stdout),
|
||||
"scaled_signal.gain.factor:F64\n\
|
||||
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",
|
||||
"gain.factor:F64\n\
|
||||
fast.length:I64 default=2\n\
|
||||
slow.length:I64 default=4\n\
|
||||
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)
|
||||
@@ -198,7 +198,7 @@ fn project_node_open_param_sweeps_and_reproduces() {
|
||||
"--to",
|
||||
GER40_TO_MS,
|
||||
"--axis",
|
||||
"scaled_signal.gain.factor=0.5,1.0",
|
||||
"gain.factor=0.5,1.0",
|
||||
"--name",
|
||||
"knob",
|
||||
],
|
||||
@@ -212,9 +212,11 @@ fn project_node_open_param_sweeps_and_reproduces() {
|
||||
let lines: Vec<&str> = stdout.lines().collect();
|
||||
assert_eq!(lines.len(), 2, "one member line per factor point: {stdout}");
|
||||
|
||||
// Each member's manifest carries the swept PROJECT-node param binding under
|
||||
// its wrapped name — the load-bearing proof that it is MY node's knob that
|
||||
// varied across the family, not some incidental std axis.
|
||||
// Each member's manifest carries the swept PROJECT-node param binding
|
||||
// under its RAW name (this cycle's tidy fix, #328 batch 2: the
|
||||
// real/campaign route's own `manifest.params` mints raw too now) — the
|
||||
// load-bearing proof that it is MY node's knob that varied across the
|
||||
// family, not some incidental std axis.
|
||||
for (line, factor) in lines.iter().zip([0.5_f64, 1.0]) {
|
||||
let v: serde_json::Value = serde_json::from_str(line).expect("member line parses as JSON");
|
||||
assert_eq!(
|
||||
@@ -227,7 +229,7 @@ fn project_node_open_param_sweeps_and_reproduces() {
|
||||
.expect("manifest.params is an array");
|
||||
let bound = params
|
||||
.iter()
|
||||
.find(|p| p[0].as_str() == Some("scaled_signal.gain.factor"))
|
||||
.find(|p| p[0].as_str() == Some("gain.factor"))
|
||||
.and_then(|p| p[1]["F64"].as_f64());
|
||||
assert_eq!(
|
||||
bound,
|
||||
|
||||
@@ -329,9 +329,9 @@ fn campaign_validate_in_project_reports_referential_tier_end_to_end() {
|
||||
"sweep",
|
||||
&closed_bp,
|
||||
"--axis",
|
||||
"sma_signal.fast.length=2,4",
|
||||
"fast.length=2,4",
|
||||
"--axis",
|
||||
"sma_signal.slow.length=8,16",
|
||||
"slow.length=8,16",
|
||||
"--name",
|
||||
"campaign-ref-seed",
|
||||
],
|
||||
@@ -377,10 +377,11 @@ fn campaign_validate_in_project_reports_referential_tier_end_to_end() {
|
||||
.trim_start_matches("content:")
|
||||
.to_string();
|
||||
|
||||
// "fast.length": the axis name is the RAW composite's `param_space` name.
|
||||
// "fast.length": the axis name is the RAW composite's `param_space` name —
|
||||
// the SAME namespace `aura sweep --axis` itself binds against now (#328).
|
||||
// `validate_campaign_refs` loads the stored blueprint bare, unlike the
|
||||
// sweep's `wrap_r`-wrapped axis probe, so it does NOT carry the
|
||||
// "sma_signal." prefix `aura sweep --axis` binds against.
|
||||
// sweep's `wrap_r`-wrapped axis probe, so it never carries a wrap prefix
|
||||
// to begin with.
|
||||
let campaign = format!(
|
||||
r#"{{
|
||||
"format_version": 1,
|
||||
@@ -448,9 +449,9 @@ fn campaign_validate_resolves_identity_ref_via_index_first_lookup_then_index_hit
|
||||
"sweep",
|
||||
&closed_bp,
|
||||
"--axis",
|
||||
"sma_signal.fast.length=2,4",
|
||||
"fast.length=2,4",
|
||||
"--axis",
|
||||
"sma_signal.slow.length=8,16",
|
||||
"slow.length=8,16",
|
||||
"--name",
|
||||
"campaign-identity-seed",
|
||||
],
|
||||
@@ -1288,9 +1289,9 @@ fn seed_blueprint(dir: &Path, name: &str) -> String {
|
||||
"sweep",
|
||||
&closed_bp,
|
||||
"--axis",
|
||||
"sma_signal.fast.length=2,4",
|
||||
"fast.length=2,4",
|
||||
"--axis",
|
||||
"sma_signal.slow.length=8,16",
|
||||
"slow.length=8,16",
|
||||
"--name",
|
||||
name,
|
||||
],
|
||||
|
||||
@@ -421,7 +421,18 @@ pub enum RefFault {
|
||||
StrategyNotFound(String),
|
||||
IdentityUnmatched(String),
|
||||
StrategyUnloadable { id: String, error: String },
|
||||
AxisNotInParamSpace { strategy: String, axis: String },
|
||||
AxisNotInParamSpace {
|
||||
strategy: String,
|
||||
axis: String,
|
||||
/// #328 translation aid: stripping ONE leading segment off `axis`
|
||||
/// (the wrapped `<blueprint>.<node>.<param>` shape a stale transcript
|
||||
/// might quote) and finding that remainder in the strategy's
|
||||
/// `param_space()` or `bound_param_space()` — `Some` iff the strip-
|
||||
/// then-hit lands, computed at fault-construction time so the prose
|
||||
/// layer never re-derives it. `None` when no leading segment strips
|
||||
/// to a hit (today's prose stays unchanged, no speculation).
|
||||
raw_candidate: Option<String>,
|
||||
},
|
||||
AxisKindMismatch { strategy: String, axis: String },
|
||||
/// An open param of the resolved strategy is bound by no campaign axis —
|
||||
/// the executor's every-open-knob-required rule, mirrored at validate
|
||||
@@ -523,10 +534,22 @@ impl Registry {
|
||||
});
|
||||
}
|
||||
}
|
||||
None => faults.push(RefFault::AxisNotInParamSpace {
|
||||
strategy: label.clone(),
|
||||
axis: axis.clone(),
|
||||
}),
|
||||
None => {
|
||||
// #328 did-you-mean: strip one leading segment off the
|
||||
// rejected axis (the wrapped-name shape a stale
|
||||
// transcript might quote) and check whether the
|
||||
// remainder is a legal raw axis here — never
|
||||
// speculating when it isn't.
|
||||
let raw_candidate = axis.split_once('.').map(|(_, rest)| rest).filter(|stripped| {
|
||||
space.iter().any(|p| p.name == *stripped)
|
||||
|| bound.iter().any(|b| b.name == *stripped)
|
||||
}).map(str::to_string);
|
||||
faults.push(RefFault::AxisNotInParamSpace {
|
||||
strategy: label.clone(),
|
||||
axis: axis.clone(),
|
||||
raw_candidate,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
// The reverse direction: every open param must be bound by some
|
||||
@@ -2251,6 +2274,98 @@ mod tests {
|
||||
assert_eq!(reg.validate_campaign_refs(&by_identity, &resolve).expect("io ok"), Vec::new());
|
||||
}
|
||||
|
||||
/// #328: a rejected axis shaped like a stale wrapped transcript (one
|
||||
/// bogus leading segment glued in front of a real raw param name) carries
|
||||
/// that real name as `AxisNotInParamSpace::raw_candidate` — computed at
|
||||
/// fault-construction time in `validate_campaign_refs`, never re-derived
|
||||
/// by the prose layer.
|
||||
#[test]
|
||||
fn axis_not_in_param_space_carries_a_raw_candidate_when_strippable() {
|
||||
use aura_engine::blueprint_to_json;
|
||||
|
||||
let reg = Registry::open(temp_family_dir("axis_raw_candidate_hit"));
|
||||
let resolve = |t: &str| aura_vocabulary::std_vocabulary(t);
|
||||
let composite = bias_fixture();
|
||||
let real = composite.param_space().first().expect("fixture has an open param").clone();
|
||||
let real_name = real.name.clone();
|
||||
|
||||
let blueprint_json = blueprint_to_json(&composite).expect("serializes");
|
||||
let bp_id = aura_research::content_id_of(&blueprint_json);
|
||||
reg.put_blueprint(&bp_id, &blueprint_json).expect("seed blueprint");
|
||||
let process = r#"{"format_version":1,"kind":"process","name":"p","pipeline":[{"block":"std::generalize","metric":"sqn"}]}"#;
|
||||
let proc_id = reg.put_process(process).expect("seed process");
|
||||
|
||||
let wrapped_axis = format!("graph.{real_name}");
|
||||
let campaign_text = format!(
|
||||
concat!(
|
||||
r#"{{"format_version":1,"kind":"campaign","name":"c","#,
|
||||
r#""data":{{"instruments":["GER40"],"windows":[{{"from_ms":1,"to_ms":2}}]}},"#,
|
||||
r#""strategies":[{{"ref":{{"content_id":"{bp}"}},"#,
|
||||
r#""axes":{{"{axis}":{{"kind":"F64","values":[0.5]}}}}}}],"#,
|
||||
r#""process":{{"ref":{{"content_id":"{proc}"}}}},"#,
|
||||
r#""seed":1,"presentation":{{"persist_taps":[],"emit":["family_table"]}}}}"#
|
||||
),
|
||||
bp = bp_id,
|
||||
axis = wrapped_axis,
|
||||
proc = proc_id,
|
||||
);
|
||||
let doc = aura_research::parse_campaign(&campaign_text).expect("campaign parses");
|
||||
let faults = reg.validate_campaign_refs(&doc, &resolve).expect("io ok");
|
||||
let hit = faults
|
||||
.iter()
|
||||
.find(|f| matches!(f, RefFault::AxisNotInParamSpace { axis, .. } if axis == &wrapped_axis));
|
||||
match hit {
|
||||
Some(RefFault::AxisNotInParamSpace { raw_candidate, .. }) => {
|
||||
assert_eq!(raw_candidate.as_deref(), Some(real_name.as_str()));
|
||||
}
|
||||
other => panic!("expected AxisNotInParamSpace for {wrapped_axis:?}, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// #328 (negative): a rejected axis whose stripped remainder ALSO misses
|
||||
/// the param space carries no `raw_candidate` — the fault never
|
||||
/// speculates when stripping one leading segment lands nowhere.
|
||||
#[test]
|
||||
fn axis_not_in_param_space_carries_no_candidate_when_stripped_remainder_also_misses() {
|
||||
use aura_engine::blueprint_to_json;
|
||||
|
||||
let reg = Registry::open(temp_family_dir("axis_raw_candidate_miss"));
|
||||
let resolve = |t: &str| aura_vocabulary::std_vocabulary(t);
|
||||
let composite = bias_fixture();
|
||||
|
||||
let blueprint_json = blueprint_to_json(&composite).expect("serializes");
|
||||
let bp_id = aura_research::content_id_of(&blueprint_json);
|
||||
reg.put_blueprint(&bp_id, &blueprint_json).expect("seed blueprint");
|
||||
let process = r#"{"format_version":1,"kind":"process","name":"p","pipeline":[{"block":"std::generalize","metric":"sqn"}]}"#;
|
||||
let proc_id = reg.put_process(process).expect("seed process");
|
||||
|
||||
let bogus_axis = "bogus.alsobogus";
|
||||
let campaign_text = format!(
|
||||
concat!(
|
||||
r#"{{"format_version":1,"kind":"campaign","name":"c","#,
|
||||
r#""data":{{"instruments":["GER40"],"windows":[{{"from_ms":1,"to_ms":2}}]}},"#,
|
||||
r#""strategies":[{{"ref":{{"content_id":"{bp}"}},"#,
|
||||
r#""axes":{{"{axis}":{{"kind":"F64","values":[0.5]}}}}}}],"#,
|
||||
r#""process":{{"ref":{{"content_id":"{proc}"}}}},"#,
|
||||
r#""seed":1,"presentation":{{"persist_taps":[],"emit":["family_table"]}}}}"#
|
||||
),
|
||||
bp = bp_id,
|
||||
axis = bogus_axis,
|
||||
proc = proc_id,
|
||||
);
|
||||
let doc = aura_research::parse_campaign(&campaign_text).expect("campaign parses");
|
||||
let faults = reg.validate_campaign_refs(&doc, &resolve).expect("io ok");
|
||||
let hit = faults
|
||||
.iter()
|
||||
.find(|f| matches!(f, RefFault::AxisNotInParamSpace { axis, .. } if axis == bogus_axis));
|
||||
match hit {
|
||||
Some(RefFault::AxisNotInParamSpace { raw_candidate, .. }) => {
|
||||
assert_eq!(raw_candidate, &None);
|
||||
}
|
||||
other => panic!("expected AxisNotInParamSpace for {bogus_axis:?}, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Property (#191): the identity-index sidecar is a last-line-wins cache
|
||||
/// over identity id → content id — a missing file reads as empty (no
|
||||
/// error, since the scan is the truth path), a later append for the same
|
||||
|
||||
@@ -45,6 +45,75 @@ pub fn raw_matches_wrapped(raw: &str, wrapped: &str) -> bool {
|
||||
wrapped == raw || wrapped_to_raw_axis(wrapped) == raw
|
||||
}
|
||||
|
||||
/// (a) #328: the explicit `--axis` acceptance predicate, shared by BOTH
|
||||
/// intake routes (`validate_and_register_axes`'s real route and the synthetic
|
||||
/// sweep-verb intake ahead of `run_blueprint_sweep`). RAW is checked FIRST and
|
||||
/// independently — a legal RAW name (an open param's [`wrapped_to_raw_axis`]
|
||||
/// suffix, or a bound param's own already-raw name, #203) is accepted
|
||||
/// outright, so a pathological name that is both raw-legal and wrapped-exact
|
||||
/// (e.g. an unwrapped param) resolves as raw, never as a translation refusal.
|
||||
/// Only once that fails is a WRAPPED hit considered (an exact `param_space()`
|
||||
/// name, or one leading segment stripped off `name` landing on a bound
|
||||
/// param): that is a translation refusal naming the raw candidate, never a
|
||||
/// silent alias. Anything matching neither namespace is [`AxisIntake::Unknown`]
|
||||
/// — the caller's own unmatched-axis prose applies, unchanged.
|
||||
///
|
||||
/// Deliberately NOT built on [`raw_matches_wrapped`]: that predicate's own
|
||||
/// equality branch also matches an EXACT wrapped name (see its doc comment),
|
||||
/// so using it as the acceptance gate would silently accept the retired form
|
||||
/// instead of refusing it.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum AxisIntake {
|
||||
/// A legal RAW name (`--list-axes`'s own namespace, #328) — accept as-is.
|
||||
Raw,
|
||||
/// A retired WRAPPED name; the carried string is its translated RAW
|
||||
/// candidate — never applied silently, only ever surfaced in a refusal.
|
||||
WrappedRetired(String),
|
||||
/// Neither namespace: the caller's own unknown-axis prose applies.
|
||||
Unknown,
|
||||
}
|
||||
|
||||
/// Classify one `--axis` name against `wrapped_open` (the WRAPPED open-param
|
||||
/// probe, `blueprint_axis_probe(..).param_space()`) and `raw_bound` (the
|
||||
/// strategy's own `bound_param_space()` names — already RAW, #203). See
|
||||
/// [`AxisIntake`] for the precedence.
|
||||
pub fn classify_axis_intake(
|
||||
name: &str,
|
||||
wrapped_open: &[ParamSpec],
|
||||
raw_bound: &HashSet<String>,
|
||||
) -> AxisIntake {
|
||||
let is_raw = wrapped_open.iter().any(|p| wrapped_to_raw_axis(&p.name) == name)
|
||||
|| raw_bound.contains(name);
|
||||
if is_raw {
|
||||
return AxisIntake::Raw;
|
||||
}
|
||||
let is_wrapped = wrapped_open.iter().any(|p| p.name == name)
|
||||
|| raw_bound.iter().any(|b| name.split_once('.').map(|(_, rest)| rest) == Some(b.as_str()));
|
||||
if is_wrapped {
|
||||
AxisIntake::WrappedRetired(wrapped_to_raw_axis(name).to_string())
|
||||
} else {
|
||||
AxisIntake::Unknown
|
||||
}
|
||||
}
|
||||
|
||||
/// Translate one RAW axis name onto the ONE wrapped `space` slot it
|
||||
/// suffix-matches ([`raw_matches_wrapped`]), falling back to `raw` unchanged
|
||||
/// when no wrapped slot matches — the caller's own downstream name
|
||||
/// resolution (e.g. the sweep terminal's `UnknownKnob`) surfaces that case,
|
||||
/// never panicked here. #328: the shared translation `blueprint_sweep_family`
|
||||
/// established for its `SweepBinder::axis` call, extracted so
|
||||
/// `blueprint_sweep_over`/`validate_axis_grid` (the walkforward synthetic
|
||||
/// route's own binder calls, which key by the same exact wrapped
|
||||
/// `param_space()` name) can translate a RAW `--axis` name too instead of
|
||||
/// requiring the retired wrapped form.
|
||||
pub fn wrapped_name_of(raw: &str, space: &[ParamSpec]) -> String {
|
||||
space
|
||||
.iter()
|
||||
.find(|p| raw_matches_wrapped(raw, &p.name))
|
||||
.map(|p| p.name.clone())
|
||||
.unwrap_or_else(|| raw.to_string())
|
||||
}
|
||||
|
||||
/// Suffix-join each raw campaign axis onto exactly one wrapped param
|
||||
/// ([`raw_matches_wrapped`] — wrapped == raw, or stripping the wrapper's one
|
||||
/// node segment yields raw), then require every wrapped slot to be covered.
|
||||
@@ -139,6 +208,53 @@ mod tests {
|
||||
ParamSpec { name: name.to_string(), kind: ScalarKind::I64 }
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// #328: a legal RAW name — either an open param's raw suffix or a bound
|
||||
/// param's own (already-raw) name — classifies `Raw`, accepted as-is.
|
||||
fn classify_axis_intake_accepts_a_raw_open_or_bound_name() {
|
||||
let wrapped_open = vec![spec("sma_signal.fast.length")];
|
||||
let raw_bound: HashSet<String> = ["bias.scale".to_string()].into_iter().collect();
|
||||
assert_eq!(classify_axis_intake("fast.length", &wrapped_open, &raw_bound), AxisIntake::Raw);
|
||||
assert_eq!(classify_axis_intake("bias.scale", &wrapped_open, &raw_bound), AxisIntake::Raw);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// #328: a WRAPPED name — the exact `param_space()` string, or a bound
|
||||
/// param prefixed by one wrap segment — classifies `WrappedRetired`,
|
||||
/// carrying its raw candidate, never accepted silently.
|
||||
fn classify_axis_intake_flags_a_wrapped_name_with_its_raw_candidate() {
|
||||
let wrapped_open = vec![spec("sma_signal.fast.length")];
|
||||
let raw_bound: HashSet<String> = ["bias.scale".to_string()].into_iter().collect();
|
||||
assert_eq!(
|
||||
classify_axis_intake("sma_signal.fast.length", &wrapped_open, &raw_bound),
|
||||
AxisIntake::WrappedRetired("fast.length".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
classify_axis_intake("sma_signal.bias.scale", &wrapped_open, &raw_bound),
|
||||
AxisIntake::WrappedRetired("bias.scale".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// #328: a name matching neither namespace is `Unknown` — the caller's own
|
||||
/// unmatched-axis prose applies, unchanged by this predicate.
|
||||
fn classify_axis_intake_reports_unknown_for_neither_space() {
|
||||
let wrapped_open = vec![spec("sma_signal.fast.length")];
|
||||
let raw_bound: HashSet<String> = ["bias.scale".to_string()].into_iter().collect();
|
||||
assert_eq!(classify_axis_intake("nope", &wrapped_open, &raw_bound), AxisIntake::Unknown);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// #328: raw is checked FIRST — an unwrapped (no-dot) param whose raw name
|
||||
/// happens to equal its own wrapped `param_space()` string (a root-level
|
||||
/// knob with no node-path prefix) resolves as `Raw`, never misclassified
|
||||
/// as a wrapped-exact hit needing translation.
|
||||
fn classify_axis_intake_prefers_raw_when_a_name_is_both() {
|
||||
let wrapped_open = vec![spec("length")]; // no dot: raw == wrapped
|
||||
let raw_bound: HashSet<String> = HashSet::new();
|
||||
assert_eq!(classify_axis_intake("length", &wrapped_open, &raw_bound), AxisIntake::Raw);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// The only shape treated as a direct store address is a bare 64-char
|
||||
/// lowercase-hex token; anything else (wrong length, uppercase, non-hex)
|
||||
|
||||
@@ -254,16 +254,30 @@ const DEFLATION_SEED: u64 = 0xDEF1_A7ED;
|
||||
/// 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) => 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 } => format!(
|
||||
"axis {knob}: expected {expected:?}, supplied {got:?} — \
|
||||
see `aura sweep <bp> --list-axes`"
|
||||
),
|
||||
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 — \
|
||||
@@ -394,11 +408,17 @@ fn validate_axis_grid(
|
||||
) -> 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(first_name, first_vals.clone());
|
||||
let mut binder = probe.axis(&crate::axes::wrapped_name_of(first_name, &space), first_vals.clone());
|
||||
for (n, vals) in iter {
|
||||
binder = binder.axis(n, vals.clone());
|
||||
binder = binder.axis(&crate::axes::wrapped_name_of(n, &space), vals.clone());
|
||||
}
|
||||
binder.sweep_with_lattice(|_| axis_grid_probe_report()).map(|_| ())
|
||||
}
|
||||
@@ -463,15 +483,38 @@ pub fn blueprint_sweep_family(
|
||||
&overrides,
|
||||
)
|
||||
};
|
||||
// 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
|
||||
// sweep terminal, so an UnknownKnob / KindMismatch is returned, not panicked.
|
||||
// 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(first_name, first_vals.clone());
|
||||
let mut binder = probe.axis(&wrapped_name_of(first_name), first_vals.clone());
|
||||
for (n, vals) in iter {
|
||||
binder = binder.axis(n, vals.clone());
|
||||
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 —
|
||||
@@ -485,7 +528,7 @@ pub fn blueprint_sweep_family(
|
||||
// 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, &space, data.run_sources(env, &binding.columns()), window, 0, pip, &topo, env, DEFAULT_STOP, &binding, &[], NO_INSTRUMENT_CONTEXT)
|
||||
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) => {
|
||||
@@ -545,11 +588,16 @@ pub fn blueprint_sweep_over(
|
||||
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(first_name, first_vals.clone());
|
||||
let mut binder = probe.axis(&crate::axes::wrapped_name_of(first_name, &space), first_vals.clone());
|
||||
for (n, vals) in iter {
|
||||
binder = binder.axis(n, vals.clone());
|
||||
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| {
|
||||
|
||||
@@ -12,7 +12,7 @@ use std::collections::BTreeMap;
|
||||
use aura_core::{Scalar, Timestamp};
|
||||
use aura_engine::{Composite, Harness, MeasurementReport, RunManifest};
|
||||
|
||||
use crate::member::{key_supply, resolve_run_data, wrapped_bound_defaults, RunData};
|
||||
use crate::member::{key_supply, raw_bound_defaults, resolve_run_data, RunData};
|
||||
use crate::project::Env;
|
||||
use crate::tap_plan::{bind_tap_plan, TapPlan};
|
||||
|
||||
@@ -79,7 +79,7 @@ pub fn run_measurement(
|
||||
std::process::exit(1);
|
||||
}
|
||||
let names: Vec<String> = signal.param_space().iter().map(|p| p.name.clone()).collect();
|
||||
let defaults = wrapped_bound_defaults(&signal);
|
||||
let defaults = raw_bound_defaults(&signal);
|
||||
let (sources, window, _pip_size) = resolve_run_data(&data, env, &binding);
|
||||
|
||||
// Compile the signal DIRECTLY — no wrap_r, no broker/executor/eq-ex-r sinks.
|
||||
|
||||
@@ -82,22 +82,20 @@ pub fn sim_optimal_manifest(
|
||||
}
|
||||
}
|
||||
|
||||
/// The wrap-prefixed BOUND-param defaults of `signal` (#249): every
|
||||
/// `bound_param_space()` entry as a `(<signal.name()>.<param>, value)` pair, in
|
||||
/// `bound_param_space()` order — the `RunManifest.defaults` field. Must be read
|
||||
/// off `signal` BEFORE it is consumed by `wrap_r`/reopening: an axis-reopened
|
||||
/// bound param has already left `bound_param_space()` by construction
|
||||
/// (`Composite::reopen` forgets the bound value), so a caller that reopens
|
||||
/// overrides before calling this naturally excludes them — no separate filter
|
||||
/// needed. Same prefixing rule as `wrapped_bound_names`, kept separate because
|
||||
/// that helper discards the value this one needs.
|
||||
pub fn wrapped_bound_defaults(signal: &Composite) -> Vec<(String, Scalar)> {
|
||||
let prefix = format!("{}.", signal.name());
|
||||
signal
|
||||
.bound_param_space()
|
||||
.into_iter()
|
||||
.map(|b| (format!("{prefix}{}", b.name), b.value))
|
||||
.collect()
|
||||
/// The RAW-namespace BOUND-param defaults of `signal` (#249/#328): every
|
||||
/// `bound_param_space()` entry as a `(<param>, value)` pair, already RAW
|
||||
/// (`bound_param_space()`'s own path-qualified name, #203 — no wrap-prefix
|
||||
/// concatenation), in `bound_param_space()` order — the `RunManifest.defaults`
|
||||
/// field. Must be read off `signal` BEFORE it is consumed by
|
||||
/// `wrap_r`/reopening: an axis-reopened bound param has already left
|
||||
/// `bound_param_space()` by construction (`Composite::reopen` forgets the
|
||||
/// bound value), so a caller that reopens overrides before calling this
|
||||
/// naturally excludes them — no separate filter needed. Contrast
|
||||
/// `wrapped_bound_names`, which DOES wrap-prefix (it matches against the
|
||||
/// WRAPPED open `param_space()`, a different coordinate system than this
|
||||
/// manifest-recording helper needs).
|
||||
pub fn raw_bound_defaults(signal: &Composite) -> Vec<(String, Scalar)> {
|
||||
signal.bound_param_space().into_iter().map(|b| (b.name, b.value)).collect()
|
||||
}
|
||||
|
||||
/// The honest broker label for the dual-tap r-sma harness: it runs a RiskExecutor
|
||||
@@ -506,7 +504,7 @@ pub fn run_signal_r(
|
||||
.iter()
|
||||
.map(|p| p.name.clone())
|
||||
.collect();
|
||||
let defaults = wrapped_bound_defaults(&signal); // read before `signal` is consumed below
|
||||
let defaults = raw_bound_defaults(&signal); // read before `signal` is consumed below
|
||||
let (tx_eq, rx_eq) = mpsc::channel();
|
||||
let (tx_ex, rx_ex) = mpsc::channel();
|
||||
let (tx_r, rx_r) = mpsc::channel();
|
||||
@@ -580,7 +578,7 @@ pub fn run_blueprint_member(
|
||||
cost: &[aura_research::CostSpec],
|
||||
instrument: &str,
|
||||
) -> RunReport {
|
||||
let defaults = wrapped_bound_defaults(&signal); // read before `signal` is consumed below
|
||||
let defaults = raw_bound_defaults(&signal); // read before `signal` is consumed below
|
||||
let (tx_eq, rx_eq) = mpsc::channel();
|
||||
let (tx_ex, rx_ex) = mpsc::channel();
|
||||
let (tx_r, rx_r) = mpsc::channel();
|
||||
@@ -706,55 +704,72 @@ pub fn wrapped_bound_names(signal: &Composite) -> HashSet<String> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The override subset of `names` (#246): every WRAPPED-coordinate 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). Contrast
|
||||
/// `axes::raw_bound_overrides_of`, whose `names` are already RAW (a campaign
|
||||
/// document's own namespace, #203) and needs no such stripping.
|
||||
/// 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 segment 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`/`runner.rs`
|
||||
/// use over the RECORDED manifest param names (the sweep boundary uses the
|
||||
/// stricter `override_paths`, which errors on an unmatched name instead).
|
||||
///
|
||||
/// #328: matches via [`crate::axes::raw_matches_wrapped`], so `names` may be
|
||||
/// WRAPPED (every route this cycle leaves untouched) OR RAW (a `Sweep` family
|
||||
/// minted by `blueprint_sweep_family`, whose manifest now records the raw
|
||||
/// campaign namespace, #328) — `reproduce_family_in` reproduces families from
|
||||
/// either route generically, so this helper must tolerate both shapes rather
|
||||
/// than assume one. Contrast `axes::raw_bound_overrides_of`, whose `names` are
|
||||
/// ALWAYS RAW (a campaign document's own namespace, #203) by contract.
|
||||
pub fn wrapped_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 = wrapped_bound_names(signal);
|
||||
names
|
||||
.iter()
|
||||
.filter(|n| !open.contains(n.as_str()) && bound.contains(*n))
|
||||
.map(|n| n[prefix.len()..].to_string())
|
||||
.filter(|n| !open_space.iter().any(|p| crate::axes::raw_matches_wrapped(n, &p.name)))
|
||||
.filter_map(|n| {
|
||||
bound
|
||||
.iter()
|
||||
.find(|b| crate::axes::raw_matches_wrapped(n, b))
|
||||
.map(|b| crate::axes::wrapped_to_raw_axis(b).to_string())
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The sweep-boundary variant (#246): like `wrapped_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.
|
||||
///
|
||||
/// #328: matches via [`crate::axes::raw_matches_wrapped`] rather than exact
|
||||
/// wrapped-name equality, so a RAW incoming name (`fast.length`) resolves
|
||||
/// against the WRAPPED open/bound space exactly like an exact wrapped hit
|
||||
/// (`sma_signal.fast.length`) would — the one shared suffix convention
|
||||
/// `axes.rs` defines, reused rather than re-derived here. This is the
|
||||
/// resolution mechanism, not an acceptance gate (contrast the CLI's own
|
||||
/// `--axis` intake, which refuses a wrapped name before this ever runs on the
|
||||
/// sweep-verb route); callers whose axes are not intake-filtered this cycle
|
||||
/// (the walk-forward routes) keep accepting either form unchanged.
|
||||
pub 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 = wrapped_bound_names(signal);
|
||||
let mut overrides = Vec::new();
|
||||
for (name, _) in axes {
|
||||
if open.contains(name.as_str()) {
|
||||
if open_space.iter().any(|p| crate::axes::raw_matches_wrapped(name, &p.name)) {
|
||||
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`"
|
||||
));
|
||||
match bound.iter().find(|b| crate::axes::raw_matches_wrapped(name, b)) {
|
||||
Some(b) => overrides.push(crate::axes::wrapped_to_raw_axis(b).to_string()),
|
||||
None => {
|
||||
return Err(format!(
|
||||
"axis {name}: names no param of this blueprint (open or bound) — \
|
||||
see `aura sweep <bp> --list-axes`"
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(overrides)
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::mpsc;
|
||||
|
||||
use aura_core::Scalar;
|
||||
use aura_engine::{blueprint_from_json, window_of};
|
||||
use aura_registry::{group_families, Family, FamilyKind, Registry};
|
||||
use aura_backtest::point_from_params;
|
||||
@@ -142,7 +143,31 @@ pub fn reproduce_family_in(
|
||||
});
|
||||
}
|
||||
let space = crate::member::wrap_r(reload()?, tx_eq, tx_ex, tx_r, tx_req, stop, true, SYNTHETIC_PIP_SIZE, &binding, None).param_space();
|
||||
let point = point_from_params(&space, &stored.manifest.params)
|
||||
// #328: `stored.manifest.params` is RAW on every mint route now (the
|
||||
// synthetic sweep family and, since this cycle's tidy fix, the
|
||||
// real/campaign route too) — but a family minted before this fix may
|
||||
// still carry WRAPPED names on disk (C29: no retroactive rewrite of a
|
||||
// registered artifact), so `reproduce_family_in` reproduces either
|
||||
// shape generically: each recorded name is translated onto its
|
||||
// matching WRAPPED `space` slot (`raw_matches_wrapped`, tolerant of
|
||||
// both shapes) before `point_from_params`, which still keys by the
|
||||
// exact wrapped name. Non-axis stamps (`stop_length`, `cost[k].<knob>`)
|
||||
// carry no wrap segment either way and translate to themselves (no
|
||||
// `space` hit).
|
||||
let wrapped_params: Vec<(String, Scalar)> = stored
|
||||
.manifest
|
||||
.params
|
||||
.iter()
|
||||
.map(|(n, v)| {
|
||||
let wrapped = space
|
||||
.iter()
|
||||
.find(|p| crate::axes::raw_matches_wrapped(n, &p.name))
|
||||
.map(|p| p.name.clone())
|
||||
.unwrap_or_else(|| n.clone());
|
||||
(wrapped, *v)
|
||||
})
|
||||
.collect();
|
||||
let point = point_from_params(&space, &wrapped_params)
|
||||
.map_err(|m| RunnerError { exit_code: 1, message: m })?;
|
||||
// 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
|
||||
|
||||
@@ -116,6 +116,21 @@ impl MemberRunner for DefaultMemberRunner<'_> {
|
||||
.param_space();
|
||||
let point = bind_axes(&space, &cell.strategy_id, params)?;
|
||||
let signal = reopen_all(signal, &overrides);
|
||||
// manifest.params records RAW names (#328: the real/campaign route was
|
||||
// the one mint left wrapped after the synthetic route's own switch) —
|
||||
// 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 (`space` itself stays wrapped for `bind_axes`
|
||||
// above, which still keys by the exact wrapped `param_space()` name).
|
||||
let manifest_space: Vec<aura_core::ParamSpec> = space
|
||||
.iter()
|
||||
.map(|p| aura_core::ParamSpec {
|
||||
name: crate::axes::wrapped_to_raw_axis(&p.name).to_string(),
|
||||
kind: p.kind,
|
||||
})
|
||||
.collect();
|
||||
|
||||
// The member's resolved input binding (campaign data.bindings
|
||||
// overrides win over name defaults). A refusal is a member fault,
|
||||
@@ -173,7 +188,7 @@ impl MemberRunner for DefaultMemberRunner<'_> {
|
||||
let mut report = run_blueprint_member(
|
||||
signal,
|
||||
&point,
|
||||
&space,
|
||||
&manifest_space,
|
||||
sources,
|
||||
(from, to),
|
||||
0,
|
||||
@@ -490,7 +505,28 @@ pub fn persist_campaign_traces(
|
||||
// is the member report's own `manifest.window` — already
|
||||
// epoch-ns (`run_blueprint_member` stamped the post-seam
|
||||
// bounds), so no second ms->ns crossing here.
|
||||
let point = point_from_params(&space, &member_report.manifest.params)?;
|
||||
//
|
||||
// #328: `member_report.manifest.params` is RAW (this cycle's tidy
|
||||
// fix put the real/campaign mint on the raw frame too), but
|
||||
// `point_from_params` still keys by the exact WRAPPED `space`
|
||||
// name — translate each recorded name onto its matching wrapped
|
||||
// slot first (`raw_matches_wrapped`, tolerant of either shape, the
|
||||
// same `reproduce_family_in` recipe), never re-resolving by a
|
||||
// renamed identity.
|
||||
let wrapped_params: Vec<(String, Scalar)> = member_report
|
||||
.manifest
|
||||
.params
|
||||
.iter()
|
||||
.map(|(n, v)| {
|
||||
let wrapped = space
|
||||
.iter()
|
||||
.find(|p| crate::axes::raw_matches_wrapped(n, &p.name))
|
||||
.map(|p| p.name.clone())
|
||||
.unwrap_or_else(|| n.clone());
|
||||
(wrapped, *v)
|
||||
})
|
||||
.collect();
|
||||
let point = point_from_params(&space, &wrapped_params)?;
|
||||
let (from, to) = member_report.manifest.window;
|
||||
let no_data = || {
|
||||
format!(
|
||||
|
||||
+37
-13
@@ -40,7 +40,7 @@ the same shape every node in `aura-std` already follows.
|
||||
repo (two tiers)") — a strategy over the std vocabulary is a
|
||||
blueprint/campaign document, not Rust: the scaffold ships **one** closed
|
||||
`signal.json` starter that serves both verbs — `aura run` uses its bound
|
||||
values as-is, `aura sweep --axis <bp>.fast.length=2,4,8` overrides them
|
||||
values as-is, `aura sweep --axis fast.length=2,4,8` overrides them
|
||||
(bound = default, not fixed); `--list-axes` lists the open knobs and the
|
||||
bound defaults alike, so every override target is discoverable.
|
||||
- **A project-specific *native* node type** — the moment §0's three-part
|
||||
@@ -284,7 +284,7 @@ A param therefore has three states: **open** (an axis every sweep MUST bind),
|
||||
**bound** (a default any axis MAY override, #246), and **ganged** (open, but
|
||||
fused with its siblings under ONE public knob declared by a `gang` op; the
|
||||
member addresses are unbindable and only the gang's own single-segment name —
|
||||
wrapped like any knob, e.g. `graph.channel_length` — appears in `--list-axes`).
|
||||
e.g. `channel_length` — appears in `--list-axes`).
|
||||
|
||||
### Worked example: declaring a measurement tap
|
||||
|
||||
@@ -344,6 +344,14 @@ separate in-session bool — `bars_since_open` alone is the contract. DST is
|
||||
handled by `chrono-tz`, so the same local minute reads the same index in
|
||||
summer (CEST) and winter (CET).
|
||||
|
||||
The index is derived from the clock (`ctx.now()`), never counted from trigger
|
||||
firings — so the trigger's cadence only sets how *often* the node emits, not
|
||||
what it emits. Feeding a denser stream (e.g. a raw m1 `price`) is equally
|
||||
valid: the node fires once per input tick and reports the same
|
||||
`period_minutes`-bar index throughout that bar. The once-per-bar wiring above
|
||||
is the convention when you want exactly one emission per completed bar; it is
|
||||
not a correctness requirement.
|
||||
|
||||
Wire it like any node —
|
||||
`{"op":"add","type":"SessionFrankfurt","bind":{"period_minutes":{"I64":15}}}`,
|
||||
feed its `trigger` from a once-per-bar stream, and read `bars_since_open`.
|
||||
@@ -405,23 +413,39 @@ slow.length:I64
|
||||
bias.scale:F64
|
||||
```
|
||||
|
||||
These printed names are the **raw param-space namespace**: op-script params
|
||||
and a campaign document's `strategies[].axes` keys (§3) share this one raw
|
||||
form. There is a third, *wrapped* surface: the dissolved `aura sweep
|
||||
<blueprint> --axis` CLI (glossary `sweep`) accepts only the names `aura
|
||||
sweep <blueprint> --list-axes` prints — the raw name prefixed with the
|
||||
root-composite instance name, `graph.<param>` — never the raw form; the
|
||||
campaign document the sweep sugar generates still stores the raw form
|
||||
(#210):
|
||||
These printed names are the **one** axis namespace (#328): op-script params, a
|
||||
campaign document's `strategies[].axes` keys (§3), and `aura sweep <blueprint>
|
||||
--list-axes` / `--axis` (glossary `sweep`) all speak the same raw
|
||||
`<node>.<param>` form — `--params` and `--list-axes` are line-identical (open
|
||||
params bare, bound params trailing `default=<value>`), and every discovered
|
||||
name is verbatim legal as a document axis key, bound params included (#246's
|
||||
re-open contract):
|
||||
|
||||
```
|
||||
raw (--params, campaign axes): fast.length
|
||||
wrapped (--list-axes, --axis): graph.fast.length
|
||||
$ aura sweep smacross.json --axis fast.length=3:9:2 # synthetic: raw works
|
||||
$ aura sweep smacross.json --axis bias.scale=0.25,0.5 --real ... # real: raw works
|
||||
```
|
||||
|
||||
The older `<blueprint>.<node>.<param>` wrapped form (e.g. `graph.fast.length`,
|
||||
what pre-#328 transcripts and `--list-axes` output used to print) is retired
|
||||
from the surface; naming it refuses with a translation pointer to the raw
|
||||
candidate, on both intake seams:
|
||||
|
||||
```
|
||||
$ aura sweep smacross.json --axis graph.fast.length=...
|
||||
aura: axis "graph.fast.length": axis names are raw node.param paths —
|
||||
use "fast.length" (the wrapped --list-axes form was retired, #328)
|
||||
|
||||
$ aura campaign validate wrapped.json # a document quoting an old transcript
|
||||
aura: campaign references do not resolve:
|
||||
strategy f5fdf729d0333f0286c19d57143fb7933137f27a6128e72489c508096c7a2240:
|
||||
axis "graph.fast.length" is not in the param space; axis names are raw
|
||||
node.param paths — did you mean "fast.length"?
|
||||
```
|
||||
|
||||
A ganged knob's raw address has one path segment less than a member address
|
||||
would — it sits at the composite's own level, like a role name (e.g.
|
||||
`channel_length`, not `channel_hi.length`) — and wraps identically (`graph.channel_length`).
|
||||
`channel_length`, not `channel_hi.length`).
|
||||
|
||||
`--content-id`, `--identity-id`, `--params`, and `graph register` all accept
|
||||
**either** shape: the raw op-script array or an already-built `#155`
|
||||
|
||||
@@ -133,6 +133,18 @@ show <content-id>` prints a registered document's canonical bytes (#300), so the
|
||||
generate → retrieve → hand-extend → re-register loop needs no direct store
|
||||
filesystem access.
|
||||
|
||||
**The bound-override coincidence (#246, ratified #328).** A campaign axis naming a
|
||||
**bound** param of the referenced blueprint is deliberately accepted alongside one
|
||||
naming an open param: `validate_campaign_refs` checks each axis's name against
|
||||
`param_space()` **OR** `bound_param_space()` — the bound value stands as the
|
||||
axis's default, and the axis's own values re-open it for that campaign. There is
|
||||
no schema flag distinguishing the two cases (no `open`/`bound` marker on the axis
|
||||
entry); name coincidence against one of the two namespaces IS the mechanism, and
|
||||
the referenced blueprint is the single source of truth for which namespace a name
|
||||
falls in. Pinned by
|
||||
`referential_tier_accepts_a_kind_correct_axis_over_a_bound_param`
|
||||
(`aura-registry/src/lib.rs`).
|
||||
|
||||
**The campaign executor.** `aura campaign run <file|content-id>` executes a
|
||||
campaign (a file is register-then-run sugar; the content id is canonical): a
|
||||
zero-fault referential gate, then the process pipeline. The executable shape is
|
||||
|
||||
@@ -128,9 +128,14 @@ there is no default; pin a knob you do not want to vary with a single-value axis
|
||||
single `blueprint_axis_probe` (`aura-runner`) single-sources the wrapped probe for the
|
||||
sweep terminal, the MC closed-check, and the listing, so **listed == swept by
|
||||
construction** (and stays so across the harness retirement — the listing tracks
|
||||
whatever the sweep actually resolves). The names are prefixed by the current wrapping
|
||||
(`sma_signal.fast.length`, the nested-composite prefix), which is why discovery lives
|
||||
on the sweep verb (it owns the wrapping), not `graph introspect`. `--trace` on
|
||||
whatever the sweep actually resolves). One raw namespace, `<node>.<param>` (a splice
|
||||
path keeps its interior path, e.g. `anchor.sess.period_minutes`), is the only
|
||||
user-facing axis name (#328): `graph introspect --params` and `--list-axes` are
|
||||
line-identical (open params bare, bound params with `default=`) and both print
|
||||
exactly what `--axis` binds, on both sweep routes. The wrapped
|
||||
`<blueprint>.<node>.<param>` form the probe still resolves against internally is
|
||||
retired from the surface, with a translation refusal naming the raw candidate on
|
||||
both intake seams (`--axis` and `campaign validate`). `--trace` on
|
||||
`sweep` / `walkforward` writes per-member traces on the real-data campaign path
|
||||
(depth-2 fan-out, chartable by the printed family handle, #224); the synthetic path
|
||||
still refuses (`run` / `mc` refuse `--trace` outright). The live trace-writer is the
|
||||
|
||||
+2
-2
@@ -331,7 +331,7 @@ The harness's structural parameterization — which strategy, instrument(s), bro
|
||||
|
||||
### sweep
|
||||
**Avoid:** param-sweep, parameter sweep
|
||||
An orchestration axis varying tuning params (grid or random) within a fixed structure. The inner, param-tuning loop, distinct from the structural experiment matrix. On a loaded blueprint every open knob (`--list-axes`) is **required** — a subset is refused with the missing knob named; pin an unwanted knob with a single-value axis (`--axis name=<one-value>`), there is no default. `aura sweep --axis` takes the `--list-axes`-printed, root-composite-wrapped name (e.g. `graph.fast.length`) — not the raw `param_space` name (`fast.length`) that `graph introspect --params` and a campaign document's axes use; the CLI strips exactly one leading wrapper segment (#210). The `aura sweep` CLI verb is now thin sugar over the `campaign document` path — its blueprint form (`<bp.json> --real`) translates to a generated, content-addressed campaign run through the one executor (#210); the built-in `--strategy` sweep surface was retired by #159 (its hard-wired harnesses removed) — sweep now runs from a blueprint + `--axis`, and a retired `--strategy` token falls to the generic usage error. A ganged pair contributes ONE axis.
|
||||
An orchestration axis varying tuning params (grid or random) within a fixed structure. The inner, param-tuning loop, distinct from the structural experiment matrix. On a loaded blueprint every open knob (`--list-axes`) is **required** — a subset is refused with the missing knob named; pin an unwanted knob with a single-value axis (`--axis name=<one-value>`), there is no default. One raw namespace, `<node>.<param>` (a splice path keeps its interior path), is the only axis name (#328): `graph introspect --params` and `aura sweep --list-axes` are line-identical (open params bare, bound params with `default=`), and `--axis` accepts exactly that raw form on both sweep routes — the older `<blueprint>.<node>.<param>` wrapped form (e.g. `graph.fast.length`) is retired from the surface; naming it on `--axis` refuses (`axis names are raw node.param paths — use "fast.length" …`), and quoting it in a campaign document gets a did-you-mean toward the raw candidate. The `aura sweep` CLI verb is now thin sugar over the `campaign document` path — its blueprint form (`<bp.json> --real`) translates to a generated, content-addressed campaign run through the one executor (#210); the built-in `--strategy` sweep surface was retired by #159 (its hard-wired harnesses removed) — sweep now runs from a blueprint + `--axis`, and a retired `--strategy` token falls to the generic usage error. A ganged pair contributes ONE axis.
|
||||
|
||||
### tap
|
||||
**Avoid:** probe, monitor, scope (for the observation slot — "probe" is taken by the sweep-terminal `blueprint_axis_probe` sense; the #77 `Recorder`→`Probe` rename was retired, 2026-07-21)
|
||||
@@ -347,7 +347,7 @@ The `content id` of a run's signal blueprint in its run-record role: stamped int
|
||||
|
||||
### use (op)
|
||||
**Avoid:** —
|
||||
The construction op that splices a **registered** blueprint into the building graph as a nested `composite` under a chosen instance name (`{"op":"use","ref":{"content_id"|"name":…},"name":…,"bind":{…}}`, #317): the reference — a content id, a unique content-id prefix, or a `blueprint label` — resolves CLI-side, before the engine ever sees it; the fetched composite is C29-gated, spliced, and its resolution echoed on stderr (`aura: note:`, the existing benign marker). The emitted blueprint carries the splice inline as an ordinary nested composite — no reference or registry dependency survives into the artifact. An instance's open input roles become its `<instance>.<role>` in-ports, its output boundary fields its `<instance>.<field>` out-fields — the existing nested-composite param-prefix discipline extends unchanged (`graph.<instance>.<node>.<param>` on `sweep --list-axes`). Pairs with the `input` op: a pattern meant to be `use`d declares its formal parameters as open `input` roles, left unbound at `finish()` (#317's open-pattern amendment) — only running it standalone requires them bound.
|
||||
The construction op that splices a **registered** blueprint into the building graph as a nested `composite` under a chosen instance name (`{"op":"use","ref":{"content_id"|"name":…},"name":…,"bind":{…}}`, #317): the reference — a content id, a unique content-id prefix, or a `blueprint label` — resolves CLI-side, before the engine ever sees it; the fetched composite is C29-gated, spliced, and its resolution echoed on stderr (`aura: note:`, the existing benign marker). The emitted blueprint carries the splice inline as an ordinary nested composite — no reference or registry dependency survives into the artifact. An instance's open input roles become its `<instance>.<role>` in-ports, its output boundary fields its `<instance>.<field>` out-fields — the existing nested-composite param-prefix discipline extends unchanged (`<instance>.<node>.<param>`, the raw axis form since #328). Pairs with the `input` op: a pattern meant to be `use`d declares its formal parameters as open `input` roles, left unbound at `finish()` (#317's open-pattern amendment) — only running it standalone requires them bound.
|
||||
|
||||
### veto
|
||||
**Avoid:** risk-manager
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
# Fieldtest transcript — cycle #328 (axis-namespace reconciliation)
|
||||
|
||||
Per-cycle field test of the one-raw-namespace axis surface: the raw
|
||||
`<node>.<param>` form as the only user-facing sweep-axis name across every
|
||||
surface (discovery, intake on all three verbs, run manifests, campaign
|
||||
documents), with the wrapped `<blueprint>.<node>.<param>` form retired behind
|
||||
translation refusals.
|
||||
|
||||
Binary: `target/release/aura`, built from HEAD `4474814` via
|
||||
`cargo build --release -p aura-cli` (Finished in 5.45s — HEAD builds clean).
|
||||
Scratch project `aura new ax_lab` in `/tmp/axwork/ax_lab`; real data from the
|
||||
built-in archive (US500). Public interface only (README, authoring-guide,
|
||||
glossary, design ledger, `aura … --help`) — no crate source read.
|
||||
|
||||
Base blueprint `cycle328_crossover`: an SMA-crossover → bias signal with a
|
||||
deliberate mix — `fast.length` **open**, `slow.length` **bound** (default=6),
|
||||
`bias.scale` **bound** (default=0.5) — so both discovery line shapes and the
|
||||
bound-override contract are exercised. Content id
|
||||
`f5fdf729d0333f0286c19d57143fb7933137f27a6128e72489c508096c7a2240`.
|
||||
|
||||
---
|
||||
|
||||
## Example 1 — discovery-surface identity (`--params` vs `--list-axes`)
|
||||
|
||||
```
|
||||
$ aura graph introspect --params cycle328_crossover.bp.json
|
||||
fast.length:I64
|
||||
slow.length:I64 default=6
|
||||
bias.scale:F64 default=0.5
|
||||
$ aura sweep cycle328_crossover.bp.json --list-axes
|
||||
fast.length:I64
|
||||
slow.length:I64 default=6
|
||||
bias.scale:F64 default=0.5
|
||||
$ diff params.txt listaxes.txt # → IDENTICAL (no diff)
|
||||
```
|
||||
|
||||
Byte-identical, raw `<node>.<param>` form, bound params carrying `default=<v>`.
|
||||
`--params` discovers bound params too. (Finding W1.)
|
||||
|
||||
## Example 2 — document-first loop (paste verbatim, incl. bound-override)
|
||||
|
||||
Axes pasted verbatim from `--params` into a hand-written campaign document
|
||||
(`cycle328_2_campaign.json`), including two bound-override axes (`slow.length`,
|
||||
`bias.scale` — bound defaults re-opened by name, #246 coincidence contract):
|
||||
|
||||
```
|
||||
$ aura campaign validate cycle328_2_campaign.json # inside project, refs registered
|
||||
campaign document valid (intrinsic): 1 strategy(ies), 3 axes (4 points), 1 instrument(s), 1 window(s), 1 regime(s) (default) — 1 cell(s)
|
||||
campaign document valid (referential): all references resolve, axes are in the param space
|
||||
campaign document valid (executable): pipeline shape and static guards pass
|
||||
$ aura campaign run cycle328_2_campaign.json # exit 0
|
||||
aura: campaign run 0 recorded: 1 cells
|
||||
# manifest.params (raw): fast.length, slow.length, bias.scale (+ stop knobs)
|
||||
```
|
||||
|
||||
All three tiers pass; the bound-override axes validate; the run manifest records
|
||||
raw names. (Finding W2.)
|
||||
|
||||
## Example 3 — translation seams (wrapped form on every intake)
|
||||
|
||||
```
|
||||
$ aura sweep cycle328_crossover.bp.json --axis graph.fast.length=2,4 ... (exit 2)
|
||||
$ aura sweep cycle328_crossover.bp.json --real US500 --axis graph.fast.length=... (exit 2)
|
||||
$ aura walkforward cycle328_crossover.bp.json --real US500 --axis graph.fast.length=... (exit 2)
|
||||
aura: axis "graph.fast.length": axis names are raw node.param paths — use "fast.length" (the wrapped --list-axes form was retired, #328)
|
||||
|
||||
$ aura campaign validate cycle328_3d_wrapped_campaign.json (exit 1)
|
||||
aura: campaign references do not resolve:
|
||||
strategy f5fdf729d0333f0286c19d57143fb7933137f27a6128e72489c508096c7a2240: axis "graph.fast.length" is not in the param space; axis names are raw node.param paths — did you mean "fast.length"?
|
||||
```
|
||||
|
||||
All three verbs refuse identically, naming the raw candidate; campaign validate
|
||||
appends a did-you-mean. Note the campaign-validate strategy identifier is the
|
||||
FULL 64-hex content id, where the authoring guide's transcript shows a 4-hex
|
||||
`strategy 9f3a:` placeholder (Finding SG1). Edge probes:
|
||||
|
||||
```
|
||||
$ aura sweep ... --axis fastt.length=2,4 # typo
|
||||
aura: axis fastt.length: names no param of this blueprint (open or bound) — see `aura sweep <bp> --list-axes`
|
||||
$ aura sweep ... --axis graph.fast.bogus=1 # wrapped, tail not a param
|
||||
aura: axis graph.fast.bogus: names no param of this blueprint (open or bound) — see `aura sweep <bp> --list-axes`
|
||||
$ aura sweep ... --axis project.graph.fast.length=2 # two leading segments (> one)
|
||||
aura: axis project.graph.fast.length: names no param of this blueprint (open or bound) — see `aura sweep <bp> --list-axes`
|
||||
```
|
||||
|
||||
Typos and >1-segment wrappings fall to a clean generic error — consistent with
|
||||
the documented "one leading segment strips to a hit" contract. (Finding W3.)
|
||||
|
||||
## Example 4 — manifest raw names + reproduce
|
||||
|
||||
```
|
||||
$ aura sweep cycle328_crossover.bp.json --axis fast.length=2,4 --axis slow.length=8,16 --name cycle328-sweep-bo
|
||||
# each member manifest:
|
||||
# params: [["fast.length",..],["slow.length",..],["stop_length",..],["stop_k",..]] (raw)
|
||||
# defaults: [["bias.scale",{"F64":0.5}]] (raw)
|
||||
$ aura reproduce cycle328-sweep-bo-0 (exit 0)
|
||||
cycle328-sweep-bo-0 member fast.length=2, slow.length=8, stop_length=3, stop_k=2 reproduced: bit-identical
|
||||
... reproduced 4/4 members bit-identically
|
||||
```
|
||||
|
||||
Positive real/walkforward routes also record raw (params AND defaults) and run
|
||||
clean (exit 0): `cycle328_6a_sweep_real.*`, `cycle328_6b_walkforward.*`. No
|
||||
wrapped name appears on any surface. (Finding W4.)
|
||||
|
||||
---
|
||||
|
||||
Not exercised: the "old wrapped-name manifests remain reproducible" backward-
|
||||
compat claim (no pre-#328 on-disk family is reachable through the public
|
||||
interface to a source-blind consumer).
|
||||
@@ -0,0 +1,3 @@
|
||||
fast.length:I64
|
||||
slow.length:I64 default=6
|
||||
bias.scale:F64 default=0.5
|
||||
@@ -0,0 +1,3 @@
|
||||
fast.length:I64
|
||||
slow.length:I64 default=6
|
||||
bias.scale:F64 default=0.5
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"format_version": 1,
|
||||
"kind": "campaign",
|
||||
"name": "cycle328-doc-first-boundoverride",
|
||||
"seed": 7,
|
||||
"data": {
|
||||
"instruments": ["US500"],
|
||||
"windows": [ { "from_ms": 1725148800000, "to_ms": 1725580800000 } ]
|
||||
},
|
||||
"strategies": [
|
||||
{
|
||||
"ref": { "content_id": "f5fdf729d0333f0286c19d57143fb7933137f27a6128e72489c508096c7a2240" },
|
||||
"axes": {
|
||||
"fast.length": { "kind": "I64", "values": [2, 4] },
|
||||
"slow.length": { "kind": "I64", "values": [8, 16] },
|
||||
"bias.scale": { "kind": "F64", "values": [0.5] }
|
||||
}
|
||||
}
|
||||
],
|
||||
"process": { "ref": { "content_id": "f14023a1fef5640311163690f1140532412ea5e1d1cbb93e293e63fcf0bf2bf9" } },
|
||||
"presentation": { "persist_taps": [], "emit": ["family_table"] }
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"format_version":1,"kind":"process","name":"ax-sweep-select","description":"Sweep the grid and select the argmax-SQN cell.","pipeline":[{"block":"std::sweep","metric":"sqn","select":"argmax"}]}
|
||||
@@ -0,0 +1 @@
|
||||
aura: campaign run 0 recorded: 1 cells
|
||||
@@ -0,0 +1,5 @@
|
||||
{"family_id":"844fe329-0-US500-w0-r0-s0-0","report":{"manifest":{"commit":"4474814fa79e227cd2d93e34a6982b49f31ba846","params":[["fast.length",{"I64":2}],["slow.length",{"I64":8}],["bias.scale",{"F64":0.5}],["stop_length",{"I64":3}],["stop_k",{"F64":2.0}]],"defaults":[],"window":[1725148800000000000,1725580800000000000],"seed":0,"broker":"sim-optimal+risk-executor(pip_size=1)","instrument":"US500","topology_hash":"f5fdf729d0333f0286c19d57143fb7933137f27a6128e72489c508096c7a2240","project":{"commit":"6bb76063046d41062b68fbde79950b9d06fbd41d"}},"metrics":{"total_pips":46.422499999968466,"max_drawdown":125.89999999998915,"bias_sign_flips":861,"r":{"expectancy_r":0.052960279961470944,"n_trades":846,"win_rate":0.31796690307328607,"avg_win_r":1.8016249253621428,"avg_loss_r":-0.7622750573223781,"profit_factor":1.1018669053593053,"max_r_drawdown":29.11884107504975,"n_open_at_end":1,"sqn":0.8388429079176243,"sqn_normalized":0.2884001324999155,"net_expectancy_r":0.052960279961470944,"conviction_terciles_r":[-0.05618014716431794,0.13546662237309962,0.07959436467563116]}}}}
|
||||
{"family_id":"844fe329-0-US500-w0-r0-s0-0","report":{"manifest":{"commit":"4474814fa79e227cd2d93e34a6982b49f31ba846","params":[["fast.length",{"I64":2}],["slow.length",{"I64":16}],["bias.scale",{"F64":0.5}],["stop_length",{"I64":3}],["stop_k",{"F64":2.0}]],"defaults":[],"window":[1725148800000000000,1725580800000000000],"seed":0,"broker":"sim-optimal+risk-executor(pip_size=1)","instrument":"US500","topology_hash":"f5fdf729d0333f0286c19d57143fb7933137f27a6128e72489c508096c7a2240","project":{"commit":"6bb76063046d41062b68fbde79950b9d06fbd41d"}},"metrics":{"total_pips":23.387499999972977,"max_drawdown":113.9349999999979,"bias_sign_flips":521,"r":{"expectancy_r":0.09527242267306019,"n_trades":529,"win_rate":0.3005671077504726,"avg_win_r":2.0866957092558716,"avg_loss_r":-0.7605013680476617,"profit_factor":1.179110545808399,"max_r_drawdown":26.058017560225167,"n_open_at_end":1,"sqn":1.126033935020108,"sqn_normalized":0.4895799717478731,"net_expectancy_r":0.09527242267306019,"conviction_terciles_r":[-0.07002826150651001,0.25569891492505703,0.10011907679313316]}}}}
|
||||
{"family_id":"844fe329-0-US500-w0-r0-s0-0","report":{"manifest":{"commit":"4474814fa79e227cd2d93e34a6982b49f31ba846","params":[["fast.length",{"I64":4}],["slow.length",{"I64":8}],["bias.scale",{"F64":0.5}],["stop_length",{"I64":3}],["stop_k",{"F64":2.0}]],"defaults":[],"window":[1725148800000000000,1725580800000000000],"seed":0,"broker":"sim-optimal+risk-executor(pip_size=1)","instrument":"US500","topology_hash":"f5fdf729d0333f0286c19d57143fb7933137f27a6128e72489c508096c7a2240","project":{"commit":"6bb76063046d41062b68fbde79950b9d06fbd41d"}},"metrics":{"total_pips":80.28249999994746,"max_drawdown":87.96499999997366,"bias_sign_flips":780,"r":{"expectancy_r":0.13213069018419635,"n_trades":821,"win_rate":0.3617539585870889,"avg_win_r":1.848016846738723,"avg_loss_r":-0.8404231046568231,"profit_factor":1.2463301607589143,"max_r_drawdown":28.220480388404972,"n_open_at_end":1,"sqn":1.8795915057542347,"sqn_normalized":0.655981958519425,"net_expectancy_r":0.13213069018419635,"conviction_terciles_r":[0.11958074358694365,0.22575289096933593,0.05101263327150186]}}}}
|
||||
{"family_id":"844fe329-0-US500-w0-r0-s0-0","report":{"manifest":{"commit":"4474814fa79e227cd2d93e34a6982b49f31ba846","params":[["fast.length",{"I64":4}],["slow.length",{"I64":16}],["bias.scale",{"F64":0.5}],["stop_length",{"I64":3}],["stop_k",{"F64":2.0}]],"defaults":[],"window":[1725148800000000000,1725580800000000000],"seed":0,"broker":"sim-optimal+risk-executor(pip_size=1)","instrument":"US500","topology_hash":"f5fdf729d0333f0286c19d57143fb7933137f27a6128e72489c508096c7a2240","project":{"commit":"6bb76063046d41062b68fbde79950b9d06fbd41d"}},"metrics":{"total_pips":19.558750000014825,"max_drawdown":85.75000000000286,"bias_sign_flips":399,"r":{"expectancy_r":0.11821373408670934,"n_trades":479,"win_rate":0.31315240083507306,"avg_win_r":2.3901102129922043,"avg_loss_r":-0.9176053292440638,"profit_factor":1.1875649234489034,"max_r_drawdown":28.108663585145585,"n_open_at_end":1,"sqn":1.0959164423645071,"sqn_normalized":0.5007370035157714,"net_expectancy_r":0.11821373408670934,"conviction_terciles_r":[0.2576229784937875,0.13377757213522506,-0.035888040591340105]}}}}
|
||||
{"campaign_run":{"campaign":"844fe3290562346f37c756a74f3d76bab9a3c7257341ec8dafec0025ac2f68db","process":"f14023a1fef5640311163690f1140532412ea5e1d1cbb93e293e63fcf0bf2bf9","run":0,"seed":7,"cells":[{"strategy":"f5fdf729d0333f0286c19d57143fb7933137f27a6128e72489c508096c7a2240","instrument":"US500","window_ms":[1725148800000,1725580800000],"stages":[{"block":"std::sweep","family_id":"844fe329-0-US500-w0-r0-s0-0","selection":{"winner_ordinal":2,"params":[["bias.scale",{"F64":0.5}],["fast.length",{"I64":4}],["slow.length",{"I64":8}]],"selection":{"selection_metric":"sqn","n_trials":4,"raw_winner_metric":1.8795915057542347,"mode":"Argmax"}}}],"coverage":{"effective_from_ms":1725235200000,"effective_to_ms":1725580800000}}]}}
|
||||
@@ -0,0 +1,3 @@
|
||||
campaign document valid (intrinsic): 1 strategy(ies), 3 axes (4 points), 1 instrument(s), 1 window(s), 1 regime(s) (default) — 1 cell(s)
|
||||
campaign document valid (referential): all references resolve, axes are in the param space
|
||||
campaign document valid (executable): pipeline shape and static guards pass
|
||||
@@ -0,0 +1 @@
|
||||
aura: axis "graph.fast.length": axis names are raw node.param paths — use "fast.length" (the wrapped --list-axes form was retired, #328)
|
||||
@@ -0,0 +1 @@
|
||||
aura: axis "graph.fast.length": axis names are raw node.param paths — use "fast.length" (the wrapped --list-axes form was retired, #328)
|
||||
@@ -0,0 +1 @@
|
||||
aura: axis "graph.fast.length": axis names are raw node.param paths — use "fast.length" (the wrapped --list-axes form was retired, #328)
|
||||
@@ -0,0 +1,4 @@
|
||||
campaign document valid (intrinsic): 1 strategy(ies), 1 axes (2 points), 1 instrument(s), 1 window(s), 1 regime(s) (default) — 1 cell(s)
|
||||
aura: campaign references do not resolve:
|
||||
strategy f5fdf729d0333f0286c19d57143fb7933137f27a6128e72489c508096c7a2240: axis "graph.fast.length" is not in the param space; axis names are raw node.param paths — did you mean "fast.length"?
|
||||
strategy f5fdf729d0333f0286c19d57143fb7933137f27a6128e72489c508096c7a2240: open param "fast.length" is bound by no campaign axis
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"format_version": 1,
|
||||
"kind": "campaign",
|
||||
"name": "cycle328-wrapped-axis-in-doc",
|
||||
"seed": 7,
|
||||
"data": {
|
||||
"instruments": ["US500"],
|
||||
"windows": [ { "from_ms": 1725148800000, "to_ms": 1725580800000 } ]
|
||||
},
|
||||
"strategies": [
|
||||
{
|
||||
"ref": { "content_id": "f5fdf729d0333f0286c19d57143fb7933137f27a6128e72489c508096c7a2240" },
|
||||
"axes": {
|
||||
"graph.fast.length": { "kind": "I64", "values": [2, 4] }
|
||||
}
|
||||
}
|
||||
],
|
||||
"process": { "ref": { "content_id": "f14023a1fef5640311163690f1140532412ea5e1d1cbb93e293e63fcf0bf2bf9" } },
|
||||
"presentation": { "persist_taps": [], "emit": ["family_table"] }
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
{"family_id":"844fe329-0-US500-w0-r0-s0-0","kind":"Sweep","members":4}
|
||||
{"family_id":"cycle328-sweep-bo-0","kind":"Sweep","members":4}
|
||||
@@ -0,0 +1,4 @@
|
||||
{"manifest":{"commit":"4474814fa79e227cd2d93e34a6982b49f31ba846","params":[["fast.length",{"I64":2}],["slow.length",{"I64":8}],["stop_length",{"I64":3}],["stop_k",{"F64":2.0}]],"defaults":[["bias.scale",{"F64":0.5}]],"window":[1,18],"seed":0,"broker":"sim-optimal+risk-executor(pip_size=0.0001)","topology_hash":"f5fdf729d0333f0286c19d57143fb7933137f27a6128e72489c508096c7a2240","project":{"commit":"6bb76063046d41062b68fbde79950b9d06fbd41d"}},"metrics":{"total_pips":-0.23484999999997555,"max_drawdown":0.38129999999997777,"bias_sign_flips":2,"r":{"expectancy_r":0.05853072923940165,"n_trades":3,"win_rate":0.3333333333333333,"avg_win_r":1.127161364109783,"avg_loss_r":-0.47578458819578906,"profit_factor":1.18452908319715,"max_r_drawdown":0.2739524763392649,"n_open_at_end":1,"sqn":0.1070277396717771,"sqn_normalized":0.1070277396717771,"net_expectancy_r":0.05853072923940165,"conviction_terciles_r":[-0.2739524763392649,1.127161364109783,-0.6776167000523132]}}}
|
||||
{"manifest":{"commit":"4474814fa79e227cd2d93e34a6982b49f31ba846","params":[["fast.length",{"I64":2}],["slow.length",{"I64":16}],["stop_length",{"I64":3}],["stop_k",{"F64":2.0}]],"defaults":[["bias.scale",{"F64":0.5}]],"window":[1,18],"seed":0,"broker":"sim-optimal+risk-executor(pip_size=0.0001)","topology_hash":"f5fdf729d0333f0286c19d57143fb7933137f27a6128e72489c508096c7a2240","project":{"commit":"6bb76063046d41062b68fbde79950b9d06fbd41d"}},"metrics":{"total_pips":0.037062500000010136,"max_drawdown":0.04543749999999568,"bias_sign_flips":0,"r":{"expectancy_r":0.1570020451313814,"n_trades":1,"win_rate":1.0,"avg_win_r":0.1570020451313814,"avg_loss_r":0.0,"profit_factor":0.0,"max_r_drawdown":0.0,"n_open_at_end":1,"sqn":0.0,"sqn_normalized":0.0,"net_expectancy_r":0.1570020451313814,"conviction_terciles_r":[0.0,0.0,0.0]}}}
|
||||
{"manifest":{"commit":"4474814fa79e227cd2d93e34a6982b49f31ba846","params":[["fast.length",{"I64":4}],["slow.length",{"I64":8}],["stop_length",{"I64":3}],["stop_k",{"F64":2.0}]],"defaults":[["bias.scale",{"F64":0.5}]],"window":[1,18],"seed":0,"broker":"sim-optimal+risk-executor(pip_size=0.0001)","topology_hash":"f5fdf729d0333f0286c19d57143fb7933137f27a6128e72489c508096c7a2240","project":{"commit":"6bb76063046d41062b68fbde79950b9d06fbd41d"}},"metrics":{"total_pips":-0.5274999999999815,"max_drawdown":0.5609499999999865,"bias_sign_flips":2,"r":{"expectancy_r":-0.6426407768510923,"n_trades":3,"win_rate":0.3333333333333333,"avg_win_r":0.5474739899790243,"avg_loss_r":-1.2376981602661505,"profit_factor":0.22116619687844463,"max_r_drawdown":1.245647494511426,"n_open_at_end":1,"sqn":-1.0799322573025962,"sqn_normalized":-1.0799322573025962,"net_expectancy_r":-0.6426407768510923,"conviction_terciles_r":[-1.2456474945114262,0.5474739899790243,-1.229748826020875]}}}
|
||||
{"manifest":{"commit":"4474814fa79e227cd2d93e34a6982b49f31ba846","params":[["fast.length",{"I64":4}],["slow.length",{"I64":16}],["stop_length",{"I64":3}],["stop_k",{"F64":2.0}]],"defaults":[["bias.scale",{"F64":0.5}]],"window":[1,18],"seed":0,"broker":"sim-optimal+risk-executor(pip_size=0.0001)","topology_hash":"f5fdf729d0333f0286c19d57143fb7933137f27a6128e72489c508096c7a2240","project":{"commit":"6bb76063046d41062b68fbde79950b9d06fbd41d"}},"metrics":{"total_pips":0.0076125000000051846,"max_drawdown":0.02868749999999659,"bias_sign_flips":0,"r":{"expectancy_r":0.1570020451313814,"n_trades":1,"win_rate":1.0,"avg_win_r":0.1570020451313814,"avg_loss_r":0.0,"profit_factor":0.0,"max_r_drawdown":0.0,"n_open_at_end":1,"sqn":0.0,"sqn_normalized":0.0,"net_expectancy_r":0.1570020451313814,"conviction_terciles_r":[0.0,0.0,0.0]}}}
|
||||
@@ -0,0 +1,5 @@
|
||||
cycle328-sweep-bo-0 member fast.length=2, slow.length=8, stop_length=3, stop_k=2 reproduced: bit-identical
|
||||
cycle328-sweep-bo-0 member fast.length=2, slow.length=16, stop_length=3, stop_k=2 reproduced: bit-identical
|
||||
cycle328-sweep-bo-0 member fast.length=4, slow.length=8, stop_length=3, stop_k=2 reproduced: bit-identical
|
||||
cycle328-sweep-bo-0 member fast.length=4, slow.length=16, stop_length=3, stop_k=2 reproduced: bit-identical
|
||||
reproduced 4/4 members bit-identically
|
||||
@@ -0,0 +1,4 @@
|
||||
{"family_id":"cycle328-sweep-bo-0","report":{"manifest":{"commit":"4474814fa79e227cd2d93e34a6982b49f31ba846","params":[["fast.length",{"I64":2}],["slow.length",{"I64":8}],["stop_length",{"I64":3}],["stop_k",{"F64":2.0}]],"defaults":[["bias.scale",{"F64":0.5}]],"window":[1,18],"seed":0,"broker":"sim-optimal+risk-executor(pip_size=0.0001)","topology_hash":"f5fdf729d0333f0286c19d57143fb7933137f27a6128e72489c508096c7a2240","project":{"commit":"6bb76063046d41062b68fbde79950b9d06fbd41d"}},"metrics":{"total_pips":-0.23484999999997555,"max_drawdown":0.38129999999997777,"bias_sign_flips":2,"r":{"expectancy_r":0.05853072923940165,"n_trades":3,"win_rate":0.3333333333333333,"avg_win_r":1.127161364109783,"avg_loss_r":-0.47578458819578906,"profit_factor":1.18452908319715,"max_r_drawdown":0.2739524763392649,"n_open_at_end":1,"sqn":0.1070277396717771,"sqn_normalized":0.1070277396717771,"net_expectancy_r":0.05853072923940165,"conviction_terciles_r":[-0.2739524763392649,1.127161364109783,-0.6776167000523132]}}}}
|
||||
{"family_id":"cycle328-sweep-bo-0","report":{"manifest":{"commit":"4474814fa79e227cd2d93e34a6982b49f31ba846","params":[["fast.length",{"I64":2}],["slow.length",{"I64":16}],["stop_length",{"I64":3}],["stop_k",{"F64":2.0}]],"defaults":[["bias.scale",{"F64":0.5}]],"window":[1,18],"seed":0,"broker":"sim-optimal+risk-executor(pip_size=0.0001)","topology_hash":"f5fdf729d0333f0286c19d57143fb7933137f27a6128e72489c508096c7a2240","project":{"commit":"6bb76063046d41062b68fbde79950b9d06fbd41d"}},"metrics":{"total_pips":0.037062500000010136,"max_drawdown":0.04543749999999568,"bias_sign_flips":0,"r":{"expectancy_r":0.1570020451313814,"n_trades":1,"win_rate":1.0,"avg_win_r":0.1570020451313814,"avg_loss_r":0.0,"profit_factor":0.0,"max_r_drawdown":0.0,"n_open_at_end":1,"sqn":0.0,"sqn_normalized":0.0,"net_expectancy_r":0.1570020451313814,"conviction_terciles_r":[0.0,0.0,0.0]}}}}
|
||||
{"family_id":"cycle328-sweep-bo-0","report":{"manifest":{"commit":"4474814fa79e227cd2d93e34a6982b49f31ba846","params":[["fast.length",{"I64":4}],["slow.length",{"I64":8}],["stop_length",{"I64":3}],["stop_k",{"F64":2.0}]],"defaults":[["bias.scale",{"F64":0.5}]],"window":[1,18],"seed":0,"broker":"sim-optimal+risk-executor(pip_size=0.0001)","topology_hash":"f5fdf729d0333f0286c19d57143fb7933137f27a6128e72489c508096c7a2240","project":{"commit":"6bb76063046d41062b68fbde79950b9d06fbd41d"}},"metrics":{"total_pips":-0.5274999999999815,"max_drawdown":0.5609499999999865,"bias_sign_flips":2,"r":{"expectancy_r":-0.6426407768510923,"n_trades":3,"win_rate":0.3333333333333333,"avg_win_r":0.5474739899790243,"avg_loss_r":-1.2376981602661505,"profit_factor":0.22116619687844463,"max_r_drawdown":1.245647494511426,"n_open_at_end":1,"sqn":-1.0799322573025962,"sqn_normalized":-1.0799322573025962,"net_expectancy_r":-0.6426407768510923,"conviction_terciles_r":[-1.2456474945114262,0.5474739899790243,-1.229748826020875]}}}}
|
||||
{"family_id":"cycle328-sweep-bo-0","report":{"manifest":{"commit":"4474814fa79e227cd2d93e34a6982b49f31ba846","params":[["fast.length",{"I64":4}],["slow.length",{"I64":16}],["stop_length",{"I64":3}],["stop_k",{"F64":2.0}]],"defaults":[["bias.scale",{"F64":0.5}]],"window":[1,18],"seed":0,"broker":"sim-optimal+risk-executor(pip_size=0.0001)","topology_hash":"f5fdf729d0333f0286c19d57143fb7933137f27a6128e72489c508096c7a2240","project":{"commit":"6bb76063046d41062b68fbde79950b9d06fbd41d"}},"metrics":{"total_pips":0.0076125000000051846,"max_drawdown":0.02868749999999659,"bias_sign_flips":0,"r":{"expectancy_r":0.1570020451313814,"n_trades":1,"win_rate":1.0,"avg_win_r":0.1570020451313814,"avg_loss_r":0.0,"profit_factor":0.0,"max_r_drawdown":0.0,"n_open_at_end":1,"sqn":0.0,"sqn_normalized":0.0,"net_expectancy_r":0.1570020451313814,"conviction_terciles_r":[0.0,0.0,0.0]}}}}
|
||||
@@ -0,0 +1 @@
|
||||
aura: axis fastt.length: names no param of this blueprint (open or bound) — see `aura sweep <bp> --list-axes`
|
||||
@@ -0,0 +1 @@
|
||||
aura: axis graph.fast.bogus: names no param of this blueprint (open or bound) — see `aura sweep <bp> --list-axes`
|
||||
@@ -0,0 +1 @@
|
||||
aura: axis project.graph.fast.length: names no param of this blueprint (open or bound) — see `aura sweep <bp> --list-axes`
|
||||
@@ -0,0 +1,2 @@
|
||||
{"family_id":"1102542f-0-US500-w0-r0-s0-0","report":{"manifest":{"commit":"4474814fa79e227cd2d93e34a6982b49f31ba846","params":[["fast.length",{"I64":2}],["slow.length",{"I64":8}],["stop_length",{"I64":3}],["stop_k",{"F64":2.0}]],"defaults":[["bias.scale",{"F64":0.5}]],"window":[1725235200000000000,1725580800000000000],"seed":0,"broker":"sim-optimal+risk-executor(pip_size=1)","instrument":"US500","topology_hash":"f5fdf729d0333f0286c19d57143fb7933137f27a6128e72489c508096c7a2240","project":{"commit":"6bb76063046d41062b68fbde79950b9d06fbd41d"}},"metrics":{"total_pips":46.422499999968466,"max_drawdown":125.89999999998915,"bias_sign_flips":861,"r":{"expectancy_r":0.052960279961470944,"n_trades":846,"win_rate":0.31796690307328607,"avg_win_r":1.8016249253621428,"avg_loss_r":-0.7622750573223781,"profit_factor":1.1018669053593053,"max_r_drawdown":29.11884107504975,"n_open_at_end":1,"sqn":0.8388429079176243,"sqn_normalized":0.2884001324999155,"net_expectancy_r":0.052960279961470944,"conviction_terciles_r":[-0.05618014716431794,0.13546662237309962,0.07959436467563116]}}}}
|
||||
{"family_id":"1102542f-0-US500-w0-r0-s0-0","report":{"manifest":{"commit":"4474814fa79e227cd2d93e34a6982b49f31ba846","params":[["fast.length",{"I64":4}],["slow.length",{"I64":8}],["stop_length",{"I64":3}],["stop_k",{"F64":2.0}]],"defaults":[["bias.scale",{"F64":0.5}]],"window":[1725235200000000000,1725580800000000000],"seed":0,"broker":"sim-optimal+risk-executor(pip_size=1)","instrument":"US500","topology_hash":"f5fdf729d0333f0286c19d57143fb7933137f27a6128e72489c508096c7a2240","project":{"commit":"6bb76063046d41062b68fbde79950b9d06fbd41d"}},"metrics":{"total_pips":80.28249999994746,"max_drawdown":87.96499999997366,"bias_sign_flips":780,"r":{"expectancy_r":0.13213069018419635,"n_trades":821,"win_rate":0.3617539585870889,"avg_win_r":1.848016846738723,"avg_loss_r":-0.8404231046568231,"profit_factor":1.2463301607589143,"max_r_drawdown":28.220480388404972,"n_open_at_end":1,"sqn":1.8795915057542347,"sqn_normalized":0.655981958519425,"net_expectancy_r":0.13213069018419635,"conviction_terciles_r":[0.11958074358694365,0.22575289096933593,0.05101263327150186]}}}}
|
||||
@@ -0,0 +1,2 @@
|
||||
{"family_id":"3fbe4493-0-US500-w0-r0-s1-0","report":{"manifest":{"commit":"4474814fa79e227cd2d93e34a6982b49f31ba846","params":[["fast.length",{"I64":4}],["slow.length",{"I64":8}],["stop_length",{"I64":3}],["stop_k",{"F64":2.0}]],"defaults":[["bias.scale",{"F64":0.5}]],"window":[1730986785000000000,1732903979999000000],"seed":0,"broker":"sim-optimal+risk-executor(pip_size=1)","selection":{"selection_metric":"sqn_normalized","n_trials":2,"raw_winner_metric":-0.047813708639866226,"mode":"Argmax","deflated_score":-0.2491637240312123,"overfit_probability":0.9090909090909091,"n_resamples":1000,"block_len":5,"seed":0},"instrument":"US500","topology_hash":"f5fdf729d0333f0286c19d57143fb7933137f27a6128e72489c508096c7a2240","project":{"commit":"6bb76063046d41062b68fbde79950b9d06fbd41d"}},"metrics":{"total_pips":86.88750000012675,"max_drawdown":212.54749999991324,"bias_sign_flips":3244,"r":{"expectancy_r":0.030790024810340468,"n_trades":3446,"win_rate":0.3444573418456181,"avg_win_r":1.6171611559204189,"avg_loss_r":-0.8027746200004905,"profit_factor":1.0585080262251694,"max_r_drawdown":132.69624561269114,"n_open_at_end":1,"sqn":0.9012552216989966,"sqn_normalized":0.15352890674784425,"net_expectancy_r":0.030790024810340468,"conviction_terciles_r":[0.03406326248492066,0.038379965173779666,0.019929695543143295]}}}}
|
||||
{"walkforward":{"oos_r":{"avg_loss_r":-0.8027746200004905,"avg_win_r":1.6171611559204189,"conviction_terciles_r":[0.0,0.0,0.0],"expectancy_r":0.030790024810340468,"max_r_drawdown":132.69624561269114,"n_open_at_end":0,"n_trades":3446,"net_expectancy_r":0.030790024810340468,"profit_factor":1.0585080262251694,"sqn":0.9012552216989966,"sqn_normalized":0.15352890674784425,"win_rate":0.3444573418456181},"param_stability":[{"mean":4.0,"p25":4.0,"p5":4.0,"p50":4.0,"p75":4.0,"p95":4.0},{"mean":8.0,"p25":8.0,"p5":8.0,"p50":8.0,"p75":8.0,"p95":8.0},{"mean":3.0,"p25":3.0,"p5":3.0,"p50":3.0,"p75":3.0,"p95":3.0},{"mean":2.0,"p25":2.0,"p5":2.0,"p50":2.0,"p75":2.0,"p95":2.0}],"stitched_total_pips":86.88750000012675,"windows":1}}
|
||||
@@ -0,0 +1 @@
|
||||
{"format_version":1,"blueprint":{"name":"graph","doc":"SMA crossover bias: fast minus slow moving average, scaled to a bounded directional bias","nodes":[{"primitive":{"type":"SMA","name":"fast"}},{"primitive":{"type":"SMA","name":"slow","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":6}}]}},{"primitive":{"type":"Sub","name":"sub"}},{"primitive":{"type":"Bias","name":"bias","bound":[{"pos":0,"name":"scale","kind":"F64","value":{"F64":0.5}}]}}],"edges":[{"from":0,"to":2,"slot":0,"from_field":0},{"from":1,"to":2,"slot":1,"from_field":0},{"from":2,"to":3,"slot":0,"from_field":0}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0},{"node":1,"slot":0}],"source":"F64"}],"output":[{"node":3,"field":0,"name":"bias"}]}}
|
||||
@@ -0,0 +1,13 @@
|
||||
[
|
||||
{"op":"doc","text":"SMA crossover bias: fast minus slow moving average, scaled to a bounded directional bias"},
|
||||
{"op":"source","role":"price","kind":"F64"},
|
||||
{"op":"add","type":"SMA","name":"fast"},
|
||||
{"op":"add","type":"SMA","name":"slow","bind":{"length":{"I64":6}}},
|
||||
{"op":"add","type":"Sub","name":"sub"},
|
||||
{"op":"add","type":"Bias","name":"bias","bind":{"scale":{"F64":0.5}}},
|
||||
{"op":"feed","role":"price","into":["fast.series","slow.series"]},
|
||||
{"op":"connect","from":"fast.value","to":"sub.lhs"},
|
||||
{"op":"connect","from":"slow.value","to":"sub.rhs"},
|
||||
{"op":"connect","from":"sub.value","to":"bias.signal"},
|
||||
{"op":"expose","from":"bias.bias","as":"bias"}
|
||||
]
|
||||
@@ -0,0 +1,134 @@
|
||||
# Fieldtest transcript — milestone-37 (data-authorability boundary)
|
||||
|
||||
Milestone-scope field test: the three data-authorability sites as ONE
|
||||
downstream story — fold selection as run-mode data (#310), a registered
|
||||
composite spliced by name (#317), construction args configuring non-scalar
|
||||
structure (#271) — and the boundary refusing what stays Rust-only.
|
||||
|
||||
Binary: `target/release/aura`, built from HEAD `eb2b0a1` via
|
||||
`cargo build --workspace --release` (Finished in 5.53s, incremental — HEAD
|
||||
builds clean). Scratch project `aura new m37_lab` in `/tmp/m37_lab`; real data
|
||||
from the built-in Pepperstone archive. Public interface only (README,
|
||||
authoring-guide, glossary, design ledger, `aura … --help`, `graph introspect`) —
|
||||
no crate source read.
|
||||
|
||||
---
|
||||
|
||||
## Scenario 1 — `m37_1_ny_momentum` — Session args + fold-selected taps, e2e
|
||||
|
||||
A NY first-session-bar momentum strategy authored purely as data: `Session`
|
||||
configured to `America/New_York` / `09:30` through the `args` channel, gating a
|
||||
SMA(5)−SMA(20) sign onto the first 15m session bar. Four declared taps
|
||||
(`spread`, `first_bar`, `gated_signal`, `slow_ma`).
|
||||
|
||||
```
|
||||
$ aura graph build < m37_1_ny_momentum.ops.json > m37_1_ny_momentum.bp.json
|
||||
$ head -c 25 m37_1_ny_momentum.bp.json
|
||||
{"format_version":2,"blue # arg-bearing Session -> v2
|
||||
$ aura graph introspect --params m37_1_ny_momentum.ops.json
|
||||
# (empty = closed, runnable)
|
||||
$ aura graph introspect --content-id --identity-id < m37_1_ny_momentum.ops.json
|
||||
c8e123f0a8948034592eb5b15f32e9a3d6b0705c4c942e7563f655faa0d1e9f1
|
||||
31de7ebd09122b9cef690618dbbdc08257056745b948840734982cd62a611265
|
||||
|
||||
# default plan (record all four taps):
|
||||
$ aura run m37_1_ny_momentum.bp.json --real US500 --from 1725148800000 --to 1725580800000
|
||||
{"manifest":{... "topology_hash":"c8e123f0..." ...},"metrics":{"total_pips":-2.099...,
|
||||
"r":{"expectancy_r":0.1566...,"n_trades":10, ...}}} (exit 0, no stderr)
|
||||
|
||||
# fold-selected plan: spread=mean, first_bar=count, gated_signal=last; slow_ma unlisted
|
||||
$ aura run m37_1_ny_momentum.bp.json --real US500 --from 1725148800000 --to 1725580800000 \
|
||||
--tap spread=mean --tap first_bar=count --tap gated_signal=last
|
||||
{"manifest":{...IDENTICAL manifest+metrics as the default run...}} (exit 0)
|
||||
# stderr: aura: note: declared tap "slow_ma" unbound this run
|
||||
```
|
||||
|
||||
The fold summary rows (the reason to pick a fold over `record`) land ONLY in
|
||||
the trace store, not on the run's stdout (see `m37_1_fold_summary_rows.txt`):
|
||||
|
||||
```
|
||||
runs/traces/graph/index.json "taps":["spread","first_bar","gated_signal"] # slow_ma excluded
|
||||
spread.json {"tap":"spread","kinds":["F64"],"ts":[...],"columns":[[-0.2033544...]]} # mean
|
||||
first_bar.json {"tap":"first_bar","kinds":["I64"],"ts":[...],"columns":[[5239.0]]} # count = warm rows
|
||||
gated_signal.json {"tap":"gated_signal","kinds":["F64"],"ts":[...],"columns":[[0.0]]} # last
|
||||
```
|
||||
|
||||
`aura run` prints only manifest+metrics; `aura chart graph` emits HTML (exit 0),
|
||||
index-gated (the stale `slow_ma.json` from the default run lingers on disk but
|
||||
is NOT in the index and does NOT leak into the chart). There is no CLI verb that
|
||||
prints a fold's scalar summary as text. (Findings F1, SG1.)
|
||||
|
||||
---
|
||||
|
||||
## Scenario 2 — `m37_2` — registered composite spliced by name + args, swept
|
||||
|
||||
A London-open session anchor (open `input` pattern, `Session` → `Europe/London`
|
||||
/ `08:00` via `args`) registered under a label, then spliced by name into a
|
||||
swept consumer.
|
||||
|
||||
```
|
||||
$ aura graph build < m37_2_london_anchor.ops.json > m37_2_london_anchor.bp.json # v2
|
||||
$ aura graph register m37_2_london_anchor.bp.json --name london_anchor
|
||||
registered blueprint 2deeddd943c9...
|
||||
label "london_anchor" -> 2deeddd943c9...
|
||||
$ aura graph introspect --registered
|
||||
london_anchor 2deeddd943c9 London cash-open session anchor: bars elapsed since the 08:00 Europe/London open
|
||||
|
||||
$ aura graph build < m37_2_consumer.ops.json > m37_2_consumer.bp.json
|
||||
# stderr: aura: note: use "anchor": london_anchor -> 2deeddd943c9...
|
||||
$ head -c 25 m37_2_consumer.bp.json
|
||||
{"format_version":2,"blue # v2 propagates THROUGH the splice
|
||||
$ aura graph introspect --content-id --identity-id < m37_2_consumer.ops.json
|
||||
38369f968b9c10e6a56bca6ac1c347955492e4f58b125510cb09f4609502ad46
|
||||
f964f3b0dadb8d04e73696bc61119222755c7feef81c55e35e5b85b91bcee7ec
|
||||
|
||||
$ aura sweep m37_2_consumer.bp.json --list-axes
|
||||
graph.fast.length:I64
|
||||
graph.slow.length:I64
|
||||
graph.anchor.sess.period_minutes:I64 default=15 # spliced-instance param, path-qualified
|
||||
...
|
||||
$ aura sweep m37_2_consumer.bp.json --real GER40 --from 1725148800000 --to 1725580800000 \
|
||||
--axis graph.fast.length=5,10 --axis graph.slow.length=20 --name m37_london
|
||||
# 2 members, exit 0
|
||||
$ aura reproduce d5259c94-0-GER40-w0-r0-s0-0
|
||||
... member graph.fast.length=5, ... reproduced: bit-identical
|
||||
... member graph.fast.length=10, ... reproduced: bit-identical
|
||||
reproduced 2/2 members bit-identically
|
||||
```
|
||||
|
||||
Everything worked first try. (Finding W3.)
|
||||
|
||||
---
|
||||
|
||||
## Scenario 3 — the boundary refuses what stays Rust-only
|
||||
|
||||
```
|
||||
# genuinely-new logic as a namespaced type -> refused, NAMES the Rust escalation verb
|
||||
$ aura graph build < m37_3a_new_logic.ops.json
|
||||
aura: op 2 (add): unknown node type "my_lab::ThirdCandle" — type id looks
|
||||
project-namespaced but this project binds no node crate — attach one
|
||||
with `aura nodes new <name>` (exit 1)
|
||||
|
||||
# same, but bare (un-namespaced) type -> refused, but NO escalation pointer
|
||||
$ echo '[...{"op":"add","type":"ThirdCandle",...}...]' | aura graph build
|
||||
aura: op 1 (add): unknown node type "ThirdCandle" (exit 1) # F2
|
||||
|
||||
# arg-bearing type entered half-configured -> MissingArg
|
||||
$ aura graph build < m37_3b_session_no_args.ops.json
|
||||
aura: op 2 (add): node sess is missing required arg "tz" (exit 1)
|
||||
|
||||
# construction args on an arg-less type -> refused
|
||||
$ aura graph build < m37_3c_args_on_argless.ops.json
|
||||
aura: op 2 (add): node fast takes no construction args (exit 1)
|
||||
|
||||
# a fold that is not a registered fold -> roster-enumerating refusal
|
||||
$ aura run m37_1_ny_momentum.bp.json --real US500 --from ... --to ... --tap spread=median
|
||||
aura: unknown fold 'median' — available: count, first, last, max, mean, min, record, sum (exit 1)
|
||||
|
||||
# splice a composite that was never registered -> refused, enumerates labels
|
||||
$ aura graph build < m37_3e_unknown_label.ops.json
|
||||
aura: op 2 (use "anchor"): no registered blueprint labeled "tokyo_anchor"
|
||||
— registered labels: london_anchor (exit 1)
|
||||
```
|
||||
|
||||
Every boundary holds: exit 1, actionable prose. (Finding W4.)
|
||||
@@ -0,0 +1,10 @@
|
||||
# fold summary rows persisted by the --tap plan (spread=mean, first_bar=count, gated_signal=last)
|
||||
## index.json taps:
|
||||
"taps":["spread","first_bar","gated_signal"]
|
||||
|
||||
## spread (mean):
|
||||
{"tap":"spread","kinds":["F64"],"ts":[1725580800000000000],"columns":[[-0.20335440613026667]]}
|
||||
## first_bar (count):
|
||||
{"tap":"first_bar","kinds":["I64"],"ts":[1725580800000000000],"columns":[[5239.0]]}
|
||||
## gated_signal (last):
|
||||
{"tap":"gated_signal","kinds":["F64"],"ts":[1725580800000000000],"columns":[[0.0]]}
|
||||
@@ -0,0 +1 @@
|
||||
{"format_version":2,"blueprint":{"name":"graph","doc":"NY first-session-bar momentum: SMA(5)-SMA(20) sign, traded only on the first 09:30 America/New_York 15m session bar","nodes":[{"primitive":{"type":"SMA","name":"fast","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":5}}]}},{"primitive":{"type":"SMA","name":"slow","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":20}}]}},{"primitive":{"type":"Sub","name":"sub"}},{"primitive":{"type":"Sign","name":"sgn"}},{"primitive":{"type":"Session","name":"sess","args":[{"name":"tz","value":"America/New_York"},{"name":"open","value":"09:30"}],"bound":[{"pos":0,"name":"period_minutes","kind":"I64","value":{"I64":15}}]}},{"primitive":{"type":"EqConst","name":"isfirst","bound":[{"pos":0,"name":"target","kind":"I64","value":{"I64":1}}]}},{"primitive":{"type":"Const","name":"flat","bound":[{"pos":0,"name":"value","kind":"F64","value":{"F64":0.0}}]}},{"primitive":{"type":"Select","name":"gate"}},{"primitive":{"type":"Bias","name":"bias","bound":[{"pos":0,"name":"scale","kind":"F64","value":{"F64":1.0}}]}}],"edges":[{"from":0,"to":2,"slot":0,"from_field":0},{"from":1,"to":2,"slot":1,"from_field":0},{"from":2,"to":3,"slot":0,"from_field":0},{"from":4,"to":5,"slot":0,"from_field":0},{"from":5,"to":7,"slot":0,"from_field":0},{"from":3,"to":7,"slot":1,"from_field":0},{"from":6,"to":7,"slot":2,"from_field":0},{"from":7,"to":8,"slot":0,"from_field":0}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0},{"node":1,"slot":0},{"node":4,"slot":0},{"node":6,"slot":0}],"source":"F64"}],"output":[{"node":8,"field":0,"name":"bias"}],"taps":[{"name":"spread","from":{"node":2,"field":0}},{"name":"first_bar","from":{"node":5,"field":0}},{"name":"gated_signal","from":{"node":7,"field":0}},{"name":"slow_ma","from":{"node":1,"field":0}}]}}
|
||||
@@ -0,0 +1 @@
|
||||
{"manifest":{"commit":"eb2b0a132c9d07b2435981e6e9494a3527515304","params":[],"defaults":[["graph.fast.length",{"I64":5}],["graph.slow.length",{"I64":20}],["graph.sess.period_minutes",{"I64":15}],["graph.isfirst.target",{"I64":1}],["graph.flat.value",{"F64":0.0}],["graph.bias.scale",{"F64":1.0}]],"window":[1725235200000000000,1725580800000000000],"seed":0,"broker":"sim-optimal+risk-executor(pip_size=1)","topology_hash":"c8e123f0a8948034592eb5b15f32e9a3d6b0705c4c942e7563f655faa0d1e9f1","project":{"commit":"1a838a8eb2ff0610ba90b5ec28052eb8abf14ae2-dirty"}},"metrics":{"total_pips":-2.0999999999994543,"max_drawdown":29.899999999999636,"bias_sign_flips":12,"r":{"expectancy_r":0.15668981035489754,"n_trades":10,"win_rate":0.3,"avg_win_r":2.6354203218756065,"avg_loss_r":-0.9056232660111208,"profit_factor":1.2471696505846321,"max_r_drawdown":5.2917804259042285,"n_open_at_end":0,"sqn":0.23053858556757656,"sqn_normalized":0.23053858556757656,"net_expectancy_r":0.15668981035489754,"conviction_terciles_r":[-0.2831414429722934,0.6217793811679744,0.13774607224048319]}}}
|
||||
@@ -0,0 +1,31 @@
|
||||
[
|
||||
{"op": "doc", "text": "NY first-session-bar momentum: SMA(5)-SMA(20) sign, traded only on the first 09:30 America/New_York 15m session bar"},
|
||||
{"op": "source", "role": "price", "kind": "F64"},
|
||||
{"op": "add", "type": "SMA", "name": "fast", "bind": {"length": {"I64": 5}}},
|
||||
{"op": "add", "type": "SMA", "name": "slow", "bind": {"length": {"I64": 20}}},
|
||||
{"op": "feed", "role": "price", "into": ["fast.series", "slow.series"]},
|
||||
{"op": "add", "type": "Sub", "name": "sub"},
|
||||
{"op": "connect", "from": "fast.value", "to": "sub.lhs"},
|
||||
{"op": "connect", "from": "slow.value", "to": "sub.rhs"},
|
||||
{"op": "add", "type": "Sign", "name": "sgn"},
|
||||
{"op": "connect", "from": "sub.value", "to": "sgn.value"},
|
||||
{"op": "add", "type": "Session", "name": "sess",
|
||||
"args": {"tz": "America/New_York", "open": "09:30"},
|
||||
"bind": {"period_minutes": {"I64": 15}}},
|
||||
{"op": "feed", "role": "price", "into": ["sess.trigger"]},
|
||||
{"op": "add", "type": "EqConst", "name": "isfirst", "bind": {"target": {"I64": 1}}},
|
||||
{"op": "connect", "from": "sess.bars_since_open", "to": "isfirst.value"},
|
||||
{"op": "add", "type": "Const", "name": "flat", "bind": {"value": {"F64": 0.0}}},
|
||||
{"op": "feed", "role": "price", "into": ["flat.clock"]},
|
||||
{"op": "add", "type": "Select", "name": "gate"},
|
||||
{"op": "connect", "from": "isfirst.value", "to": "gate.cond"},
|
||||
{"op": "connect", "from": "sgn.value", "to": "gate.then"},
|
||||
{"op": "connect", "from": "flat.value", "to": "gate.otherwise"},
|
||||
{"op": "add", "type": "Bias", "name": "bias", "bind": {"scale": {"F64": 1.0}}},
|
||||
{"op": "connect", "from": "gate.value", "to": "bias.signal"},
|
||||
{"op": "tap", "from": "sub.value", "as": "spread"},
|
||||
{"op": "tap", "from": "isfirst.value", "as": "first_bar"},
|
||||
{"op": "tap", "from": "gate.value", "as": "gated_signal"},
|
||||
{"op": "tap", "from": "slow.value", "as": "slow_ma"},
|
||||
{"op": "expose", "from": "bias.bias", "as": "bias"}
|
||||
]
|
||||
@@ -0,0 +1 @@
|
||||
{"manifest":{"commit":"eb2b0a132c9d07b2435981e6e9494a3527515304","params":[],"defaults":[["graph.fast.length",{"I64":5}],["graph.slow.length",{"I64":20}],["graph.sess.period_minutes",{"I64":15}],["graph.isfirst.target",{"I64":1}],["graph.flat.value",{"F64":0.0}],["graph.bias.scale",{"F64":1.0}]],"window":[1725235200000000000,1725580800000000000],"seed":0,"broker":"sim-optimal+risk-executor(pip_size=1)","topology_hash":"c8e123f0a8948034592eb5b15f32e9a3d6b0705c4c942e7563f655faa0d1e9f1","project":{"commit":"1a838a8eb2ff0610ba90b5ec28052eb8abf14ae2-dirty"}},"metrics":{"total_pips":-2.0999999999994543,"max_drawdown":29.899999999999636,"bias_sign_flips":12,"r":{"expectancy_r":0.15668981035489754,"n_trades":10,"win_rate":0.3,"avg_win_r":2.6354203218756065,"avg_loss_r":-0.9056232660111208,"profit_factor":1.2471696505846321,"max_r_drawdown":5.2917804259042285,"n_open_at_end":0,"sqn":0.23053858556757656,"sqn_normalized":0.23053858556757656,"net_expectancy_r":0.15668981035489754,"conviction_terciles_r":[-0.2831414429722934,0.6217793811679744,0.13774607224048319]}}}
|
||||
@@ -0,0 +1 @@
|
||||
{"format_version":2,"blueprint":{"name":"graph","doc":"London-open breakout: SMA-fast>slow sign traded only on the first London session bar, blend period and MA lengths left open to sweep","nodes":[{"composite":{"name":"anchor","doc":"London cash-open session anchor: bars elapsed since the 08:00 Europe/London open","nodes":[{"primitive":{"type":"Session","name":"sess","args":[{"name":"tz","value":"Europe/London"},{"name":"open","value":"08:00"}],"bound":[{"pos":0,"name":"period_minutes","kind":"I64","value":{"I64":15}}]}}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0}]}],"output":[{"node":0,"field":0,"name":"bars"}]}},{"primitive":{"type":"SMA","name":"fast"}},{"primitive":{"type":"SMA","name":"slow"}},{"primitive":{"type":"Sub","name":"sub"}},{"primitive":{"type":"Sign","name":"sgn"}},{"primitive":{"type":"EqConst","name":"isfirst","bound":[{"pos":0,"name":"target","kind":"I64","value":{"I64":1}}]}},{"primitive":{"type":"Const","name":"flat","bound":[{"pos":0,"name":"value","kind":"F64","value":{"F64":0.0}}]}},{"primitive":{"type":"Select","name":"gate"}},{"primitive":{"type":"Bias","name":"bias","bound":[{"pos":0,"name":"scale","kind":"F64","value":{"F64":1.0}}]}}],"edges":[{"from":1,"to":3,"slot":0,"from_field":0},{"from":2,"to":3,"slot":1,"from_field":0},{"from":3,"to":4,"slot":0,"from_field":0},{"from":0,"to":5,"slot":0,"from_field":0},{"from":5,"to":7,"slot":0,"from_field":0},{"from":4,"to":7,"slot":1,"from_field":0},{"from":6,"to":7,"slot":2,"from_field":0},{"from":7,"to":8,"slot":0,"from_field":0}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0},{"node":1,"slot":0},{"node":2,"slot":0},{"node":6,"slot":0}],"source":"F64"}],"output":[{"node":8,"field":0,"name":"bias"}]}}
|
||||
@@ -0,0 +1 @@
|
||||
aura: note: use "anchor": london_anchor -> 2deeddd943c92252a401e33563f4ba8f1b85cd8a14cdf269fa1b795e4cc88eb3
|
||||
@@ -0,0 +1,25 @@
|
||||
[
|
||||
{"op": "doc", "text": "London-open breakout: SMA-fast>slow sign traded only on the first London session bar, blend period and MA lengths left open to sweep"},
|
||||
{"op": "source", "role": "price", "kind": "F64"},
|
||||
{"op": "use", "ref": {"name": "london_anchor"}, "name": "anchor"},
|
||||
{"op": "feed", "role": "price", "into": ["anchor.price"]},
|
||||
{"op": "add", "type": "SMA", "name": "fast"},
|
||||
{"op": "add", "type": "SMA", "name": "slow"},
|
||||
{"op": "feed", "role": "price", "into": ["fast.series", "slow.series"]},
|
||||
{"op": "add", "type": "Sub", "name": "sub"},
|
||||
{"op": "connect", "from": "fast.value", "to": "sub.lhs"},
|
||||
{"op": "connect", "from": "slow.value", "to": "sub.rhs"},
|
||||
{"op": "add", "type": "Sign", "name": "sgn"},
|
||||
{"op": "connect", "from": "sub.value", "to": "sgn.value"},
|
||||
{"op": "add", "type": "EqConst", "name": "isfirst", "bind": {"target": {"I64": 1}}},
|
||||
{"op": "connect", "from": "anchor.bars", "to": "isfirst.value"},
|
||||
{"op": "add", "type": "Const", "name": "flat", "bind": {"value": {"F64": 0.0}}},
|
||||
{"op": "feed", "role": "price", "into": ["flat.clock"]},
|
||||
{"op": "add", "type": "Select", "name": "gate"},
|
||||
{"op": "connect", "from": "isfirst.value", "to": "gate.cond"},
|
||||
{"op": "connect", "from": "sgn.value", "to": "gate.then"},
|
||||
{"op": "connect", "from": "flat.value", "to": "gate.otherwise"},
|
||||
{"op": "add", "type": "Bias", "name": "bias", "bind": {"scale": {"F64": 1.0}}},
|
||||
{"op": "connect", "from": "gate.value", "to": "bias.signal"},
|
||||
{"op": "expose", "from": "bias.bias", "as": "bias"}
|
||||
]
|
||||
@@ -0,0 +1 @@
|
||||
{"format_version":2,"blueprint":{"name":"graph","doc":"London cash-open session anchor: bars elapsed since the 08:00 Europe/London open","nodes":[{"primitive":{"type":"Session","name":"sess","args":[{"name":"tz","value":"Europe/London"},{"name":"open","value":"08:00"}],"bound":[{"pos":0,"name":"period_minutes","kind":"I64","value":{"I64":15}}]}}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0}]}],"output":[{"node":0,"field":0,"name":"bars"}]}}
|
||||
@@ -0,0 +1,9 @@
|
||||
[
|
||||
{"op": "doc", "text": "London cash-open session anchor: bars elapsed since the 08:00 Europe/London open"},
|
||||
{"op": "input", "role": "price"},
|
||||
{"op": "add", "type": "Session", "name": "sess",
|
||||
"args": {"tz": "Europe/London", "open": "08:00"},
|
||||
"bind": {"period_minutes": {"I64": 15}}},
|
||||
{"op": "feed", "role": "price", "into": ["sess.trigger"]},
|
||||
{"op": "expose", "from": "sess.bars_since_open", "as": "bars"}
|
||||
]
|
||||
@@ -0,0 +1,2 @@
|
||||
{"family_id":"d5259c94-0-GER40-w0-r0-s0-0","report":{"manifest":{"commit":"eb2b0a132c9d07b2435981e6e9494a3527515304","params":[["graph.fast.length",{"I64":5}],["graph.slow.length",{"I64":20}],["stop_length",{"I64":3}],["stop_k",{"F64":2.0}]],"defaults":[["graph.anchor.sess.period_minutes",{"I64":15}],["graph.isfirst.target",{"I64":1}],["graph.flat.value",{"F64":0.0}],["graph.bias.scale",{"F64":1.0}]],"window":[1725236099999000000,1725566340000000000],"seed":0,"broker":"sim-optimal+risk-executor(pip_size=1)","instrument":"GER40","topology_hash":"38369f968b9c10e6a56bca6ac1c347955492e4f58b125510cb09f4609502ad46","project":{"commit":"1a838a8eb2ff0610ba90b5ec28052eb8abf14ae2-dirty"}},"metrics":{"total_pips":6.5,"max_drawdown":73.5,"bias_sign_flips":11,"r":{"expectancy_r":-0.12406127289073887,"n_trades":8,"win_rate":0.25,"avg_win_r":1.9588513586755996,"avg_loss_r":-0.8183654834128516,"profit_factor":0.7978714475699166,"max_r_drawdown":4.3468217126941955,"n_open_at_end":0,"sqn":-0.22997299620402561,"sqn_normalized":-0.22997299620402561,"net_expectancy_r":-0.12406127289073887,"conviction_terciles_r":[1.2415119190137232,-0.23860471549936846,-0.9198999582184172]}}}}
|
||||
{"family_id":"d5259c94-0-GER40-w0-r0-s0-0","report":{"manifest":{"commit":"eb2b0a132c9d07b2435981e6e9494a3527515304","params":[["graph.fast.length",{"I64":10}],["graph.slow.length",{"I64":20}],["stop_length",{"I64":3}],["stop_k",{"F64":2.0}]],"defaults":[["graph.anchor.sess.period_minutes",{"I64":15}],["graph.isfirst.target",{"I64":1}],["graph.flat.value",{"F64":0.0}],["graph.bias.scale",{"F64":1.0}]],"window":[1725236099999000000,1725566340000000000],"seed":0,"broker":"sim-optimal+risk-executor(pip_size=1)","instrument":"GER40","topology_hash":"38369f968b9c10e6a56bca6ac1c347955492e4f58b125510cb09f4609502ad46","project":{"commit":"1a838a8eb2ff0610ba90b5ec28052eb8abf14ae2-dirty"}},"metrics":{"total_pips":-22.5,"max_drawdown":87.5,"bias_sign_flips":12,"r":{"expectancy_r":-0.31488681817637804,"n_trades":11,"win_rate":0.2727272727272727,"avg_win_r":1.3490747111619477,"avg_loss_r":-0.9388723916782501,"profit_factor":0.5388410833781364,"max_r_drawdown":6.818086529508443,"n_open_at_end":0,"sqn":-0.7651422293114423,"sqn_normalized":-0.7651422293114423,"net_expectancy_r":-0.31488681817637804,"conviction_terciles_r":[0.27036658787590495,-0.6769671918050129,-0.3917464990869556]}}}}
|
||||
@@ -0,0 +1,9 @@
|
||||
[
|
||||
{"op": "doc", "text": "attempt to author genuinely-new logic as data: a node type that is not in the closed vocabulary"},
|
||||
{"op": "source", "role": "price", "kind": "F64"},
|
||||
{"op": "add", "type": "my_lab::ThirdCandle", "name": "tc"},
|
||||
{"op": "feed", "role": "price", "into": ["tc.series"]},
|
||||
{"op": "add", "type": "Bias", "name": "bias", "bind": {"scale": {"F64": 1.0}}},
|
||||
{"op": "connect", "from": "tc.value", "to": "bias.signal"},
|
||||
{"op": "expose", "from": "bias.bias", "as": "bias"}
|
||||
]
|
||||
@@ -0,0 +1,7 @@
|
||||
[
|
||||
{"op": "doc", "text": "attempt to enter an arg-bearing type half-configured: Session with no construction args"},
|
||||
{"op": "source", "role": "price", "kind": "F64"},
|
||||
{"op": "add", "type": "Session", "name": "sess", "bind": {"period_minutes": {"I64": 15}}},
|
||||
{"op": "feed", "role": "price", "into": ["sess.trigger"]},
|
||||
{"op": "expose", "from": "sess.bars_since_open", "as": "bars"}
|
||||
]
|
||||
@@ -0,0 +1,9 @@
|
||||
[
|
||||
{"op": "doc", "text": "attempt to pass construction args to an arg-less type: SMA with an args object"},
|
||||
{"op": "source", "role": "price", "kind": "F64"},
|
||||
{"op": "add", "type": "SMA", "name": "fast", "args": {"window": "5"}, "bind": {"length": {"I64": 5}}},
|
||||
{"op": "add", "type": "Bias", "name": "bias", "bind": {"scale": {"F64": 1.0}}},
|
||||
{"op": "feed", "role": "price", "into": ["fast.series"]},
|
||||
{"op": "connect", "from": "fast.value", "to": "bias.signal"},
|
||||
{"op": "expose", "from": "bias.bias", "as": "bias"}
|
||||
]
|
||||
@@ -0,0 +1,9 @@
|
||||
[
|
||||
{"op": "doc", "text": "attempt to splice a composite that was never registered: use by an unknown label"},
|
||||
{"op": "source", "role": "price", "kind": "F64"},
|
||||
{"op": "use", "ref": {"name": "tokyo_anchor"}, "name": "anchor"},
|
||||
{"op": "feed", "role": "price", "into": ["anchor.price"]},
|
||||
{"op": "add", "type": "Bias", "name": "bias", "bind": {"scale": {"F64": 1.0}}},
|
||||
{"op": "connect", "from": "anchor.bars", "to": "bias.signal"},
|
||||
{"op": "expose", "from": "bias.bias", "as": "bias"}
|
||||
]
|
||||
Reference in New Issue
Block a user