diff --git a/crates/aura-cli/src/graph_construct.rs b/crates/aura-cli/src/graph_construct.rs index 3c2f849..12769c4 100644 --- a/crates/aura-cli/src/graph_construct.rs +++ b/crates/aura-cli/src/graph_construct.rs @@ -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; @@ -864,7 +865,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 { use std::fmt::Write as _; let text = resolve_blueprint_text(target, env)?; @@ -875,6 +880,9 @@ fn params_lines(target: &str, env: &aura_runner::project::Env) -> Result.` paths (#328: the wrapped `..` +/// 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)], +) { + let wrapped_open = blueprint_axis_probe(doc, env).param_space(); + let raw_bound: HashSet = 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 `..` 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)], env: &aura_runner::project::Env, ) -> Result<(String, Vec<(String, Vec)>), 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 = + 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 --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 --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)> = 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)); @@ -2532,8 +2592,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 +3592,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 +3604,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 +3614,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,25 +3622,27 @@ 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. `manifest.params` keys + /// are RAW (#328); `manifest.defaults` (a different field, `run`'s own + /// stamp of untouched bound params — untouched this cycle) stays WRAPPED. #[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!( diff --git a/crates/aura-cli/src/research_docs.rs b/crates/aura-cli/src/research_docs.rs index 900e538..12b7e58 100644 --- a/crates/aura-cli/src/research_docs.rs +++ b/crates/aura-cli/src/research_docs.rs @@ -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 diff --git a/crates/aura-cli/src/scaffold.rs b/crates/aura-cli/src/scaffold.rs index a621a67..b892ab4 100644 --- a/crates/aura-cli/src/scaffold.rs +++ b/crates/aura-cli/src/scaffold.rs @@ -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 --list-axes` lists the open + bound-overridable axes) +- Sweep: `aura sweep blueprints/signal.json --axis fast.length=2,4,8` + (`aura sweep --list-axes` lists the open + bound-overridable axes, RAW + `.` names — #328) - Native nodes: when the project needs its first project-specific node, `aura nodes new ` 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 diff --git a/crates/aura-cli/tests/cli_run.rs b/crates/aura-cli/tests/cli_run.rs index b1986bc..8f8c813 100644 --- a/crates/aura-cli/tests/cli_run.rs +++ b/crates/aura-cli/tests/cli_run.rs @@ -33,7 +33,7 @@ fn sweep_outside_project_refuses_and_leaves_no_store() { let cwd = temp_cwd("sweep-outside-project-refuses"); let bp = format!("{}/examples/r_sma.json", env!("CARGO_MANIFEST_DIR")); let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura")) - .args(["sweep", &bp, "--real", "GER40", "--axis", "sma_signal.fast.length=2,4"]) + .args(["sweep", &bp, "--real", "GER40", "--axis", "fast.length=2,4"]) .current_dir(&cwd) .output() .expect("spawn aura"); @@ -50,7 +50,7 @@ fn generalize_outside_project_refuses_and_leaves_no_store() { let cwd = temp_cwd("generalize-outside-project-refuses"); let bp = format!("{}/examples/r_sma.json", env!("CARGO_MANIFEST_DIR")); let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura")) - .args(["generalize", &bp, "--real", "GER40,USDJPY", "--axis", "sma_signal.fast.length=2"]) + .args(["generalize", &bp, "--real", "GER40,USDJPY", "--axis", "fast.length=2"]) .current_dir(&cwd) .output() .expect("spawn aura"); @@ -67,7 +67,7 @@ fn walkforward_outside_project_refuses_and_leaves_no_store() { let cwd = temp_cwd("walkforward-outside-project-refuses"); let bp = format!("{}/examples/r_sma.json", env!("CARGO_MANIFEST_DIR")); let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura")) - .args(["walkforward", &bp, "--real", "GER40", "--axis", "sma_signal.fast.length=2,4"]) + .args(["walkforward", &bp, "--real", "GER40", "--axis", "fast.length=2,4"]) .current_dir(&cwd) .output() .expect("spawn aura"); @@ -84,7 +84,7 @@ fn mc_outside_project_refuses_and_leaves_no_store() { let cwd = temp_cwd("mc-outside-project-refuses"); let bp = format!("{}/examples/r_sma.json", env!("CARGO_MANIFEST_DIR")); let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura")) - .args(["mc", &bp, "--real", "GER40", "--axis", "sma_signal.fast.length=2,4"]) + .args(["mc", &bp, "--real", "GER40", "--axis", "fast.length=2,4"]) .current_dir(&cwd) .output() .expect("spawn aura"); @@ -1083,8 +1083,8 @@ fn runs_families_list_and_per_family_rank_across_invocations() { let out = Command::new(BIN) .args([ "sweep", &fixture, - "--axis", "sma_signal.fast.length=2,4", - "--axis", "sma_signal.slow.length=8,16", + "--axis", "fast.length=2,4", + "--axis", "slow.length=8,16", ]) .current_dir(&cwd) .output() @@ -1162,8 +1162,8 @@ fn runs_family_rank_accepts_legacy_exposure_sign_flips_alias() { let sweep = Command::new(BIN) .args([ "sweep", &fixture, - "--axis", "sma_signal.fast.length=2,4", - "--axis", "sma_signal.slow.length=8,16", + "--axis", "fast.length=2,4", + "--axis", "slow.length=8,16", ]) .current_dir(&cwd) .output() @@ -1264,7 +1264,7 @@ fn sweep_real_no_geometry_symbol_refuses_with_exit_1() { let (dir, _g) = fresh_project(); let fixture = format!("{}/examples/r_sma.json", env!("CARGO_MANIFEST_DIR")); let out = std::process::Command::new(BIN) - .args(["sweep", &fixture, "--axis", "sma_signal.fast.length=2,4", "--real", "NONEXISTENT"]) + .args(["sweep", &fixture, "--axis", "fast.length=2,4", "--real", "NONEXISTENT"]) .current_dir(&dir).output().unwrap(); assert_eq!(out.status.code(), Some(1)); assert!(String::from_utf8_lossy(&out.stderr).contains("no recorded geometry")); @@ -1347,8 +1347,8 @@ fn sweep_real_blueprint_member_manifest_stamps_the_default_stop() { "--real", "GER40", "--from", GER40_SEPT2024_FROM_MS, "--to", GER40_SEPT2024_TO_MS, - "--axis", "sma_signal.fast.length=2,4", - "--axis", "sma_signal.slow.length=8", + "--axis", "fast.length=2,4", + "--axis", "slow.length=8", "--name", "brp_nostop", ]) .current_dir(&dir) @@ -1394,8 +1394,8 @@ fn sweep_real_blueprint_member_manifest_stamps_project_provenance() { "--real", "GER40", "--from", GER40_SEPT2024_FROM_MS, "--to", GER40_SEPT2024_TO_MS, - "--axis", "sma_signal.fast.length=2,4", - "--axis", "sma_signal.slow.length=8", + "--axis", "fast.length=2,4", + "--axis", "slow.length=8", "--name", "brp_provenance", ]) .current_dir(&dir) @@ -1515,8 +1515,8 @@ fn run_synthetic_refuses_a_multi_column_blueprint() { } /// The dissolved real-data sweep + reproduce over the OHLC example (#231 -/// acceptance): `--list-axes` pins the ganged channel knob's wrapped axis -/// name, the sweep runs one member per grid point over three real columns, +/// acceptance): `--list-axes` pins the ganged channel knob's RAW axis +/// name (#328), the sweep runs one member per grid point over three real columns, /// and every member re-derives bit-identically from the store (#229 real-data /// reproduce, now multi-column). Gated on the local GER40 archive. #[test] @@ -1534,9 +1534,9 @@ fn sweep_real_ohlc_channel_members_run_and_reproduce() { .unwrap(); assert_eq!( String::from_utf8_lossy(&axes.stdout), - "hl_channel.channel_length:I64\n\ - hl_channel.prev_high.lag:I64 default=1\n\ - hl_channel.prev_low.lag:I64 default=1\n", + "channel_length:I64\n\ + prev_high.lag:I64 default=1\n\ + prev_low.lag:I64 default=1\n", "the ganged channel knob is the one open axis, alongside the fixture's \ bound Delay lags as #246 defaults; stderr: {}", String::from_utf8_lossy(&axes.stderr) @@ -1547,7 +1547,7 @@ fn sweep_real_ohlc_channel_members_run_and_reproduce() { "--real", "GER40", "--from", GER40_SEPT2024_FROM_MS, "--to", GER40_SEPT2024_TO_MS, - "--axis", "hl_channel.channel_length=3,5", + "--axis", "channel_length=3,5", "--name", "chan", ]) .current_dir(&dir) @@ -1607,7 +1607,7 @@ fn sweep_real_ohlc_channel_members_run_and_reproduce_hostless() { "--real", "SYMA", "--from", "1707120000000", // 2024-02-05 08:00 UTC (Monday) "--to", "1707325200000", // 2024-02-07 17:00 UTC (Wednesday) - "--axis", "hl_channel.channel_length=3,5", + "--axis", "channel_length=3,5", "--name", "chan-hostless", ]) .current_dir(&dir) @@ -1674,8 +1674,8 @@ fn sweep_real_blueprint_member_lines_pin_the_dissolved_contract() { "--real", "GER40", "--from", GER40_SEPT2024_FROM_MS, "--to", GER40_SEPT2024_TO_MS, - "--axis", "sma_signal.fast.length=2,4", - "--axis", "sma_signal.slow.length=8,16", + "--axis", "fast.length=2,4", + "--axis", "slow.length=8,16", "--name", "brp", ]) .current_dir(&dir) @@ -1686,8 +1686,12 @@ fn sweep_real_blueprint_member_lines_pin_the_dissolved_contract() { let lines: Vec<&str> = stdout.lines().collect(); assert_eq!(lines.len(), 4, "one member line per 2x2 grid point: {stdout}"); - // Odometer order: `sma_signal.fast.length` (first `--axis`, outer/slowest) - // x `sma_signal.slow.length` (second `--axis`, inner) -> (2,8),(2,16),(4,8),(4,16). + // Odometer order: `fast.length` (first `--axis`, outer/slowest, typed RAW + // per #328) x `slow.length` (second `--axis`, inner) -> + // (2,8),(2,16),(4,8),(4,16). `report.manifest.params` itself is UNTOUCHED + // by #328 (out of this cycle's scope — only the synthetic sweep route's + // manifest moved to the raw frame) and still carries the WRAPPED + // `param_space()` name the real/campaign member actually bound against. let expected: [[(&str, i64); 2]; 4] = [ [("sma_signal.fast.length", 2), ("sma_signal.slow.length", 8)], [("sma_signal.fast.length", 2), ("sma_signal.slow.length", 16)], @@ -1767,15 +1771,16 @@ fn sweep_real_blueprint_member_lines_pin_the_dissolved_contract() { ); // Property (dispatch-rewire seam, docs/specs/sweep-dissolution.md Task 4 - // Step 3): the CLI's `--axis` names are typed against the WRAPPED probe - // namespace (`sma_signal.fast.length`), but a campaign document's - // `strategies[].axes` speak the RAW `param_space` namespace - // (`fast.length`) — `bind_axes`'s convention (#203). `dispatch_sweep` - // strips the wrapper's one leading node segment before generating the - // document; a regression that stopped stripping (or stripped one segment - // too many/few) would persist the wrong axis keys and - // `validate_campaign_refs` would refuse every axis as unknown before any - // member ran — this pin catches that at the artifact level, directly. + // Step 3, #328 update): the CLI's `--axis` names ARE the RAW + // campaign-document namespace directly (#328 — `--axis` accepts only the + // raw `.` form; there is no wrapped->raw strip in between + // any more), and a campaign document's `strategies[].axes` speak that + // same raw namespace — `bind_axes`'s convention (#203). A regression that + // reintroduced wrap-prefixing before generating the document (or wrote + // the raw name plus a stray extra segment) would persist the wrong axis + // keys and `validate_campaign_refs` would refuse every axis as unknown + // before any member ran — this pin catches that at the artifact level, + // directly. let campaign_doc: serde_json::Value = serde_json::from_str(&campaign_doc_json).expect("generated campaign doc parses as JSON"); let axes = campaign_doc["strategies"][0]["axes"] @@ -1825,8 +1830,8 @@ fn sweep_real_blueprint_member_lines_pin_the_dissolved_contract() { "--real", "GER40", "--from", GER40_SEPT2024_FROM_MS, "--to", GER40_SEPT2024_TO_MS, - "--axis", "sma_signal.fast.length=2,4", - "--axis", "sma_signal.slow.length=8,16", + "--axis", "fast.length=2,4", + "--axis", "slow.length=8,16", "--name", "brp", ]) .current_dir(&dir) @@ -1912,8 +1917,8 @@ fn sweep_real_with_trace_writes_tap_series_to_disk() { "--real", "GER40", "--from", GER40_SEPT2024_FROM_MS, "--to", GER40_SEPT2024_TO_MS, - "--axis", "sma_signal.fast.length=2,4", - "--axis", "sma_signal.slow.length=8", + "--axis", "fast.length=2,4", + "--axis", "slow.length=8", "--trace", "brp", ]) .current_dir(&dir) @@ -1980,8 +1985,8 @@ fn reproduce_real_sweep_family_re_derives_bit_identically() { "--real", "GER40", "--from", GER40_SEPT2024_FROM_MS, "--to", GER40_SEPT2024_TO_MS, - "--axis", "sma_signal.fast.length=2,4", - "--axis", "sma_signal.slow.length=8", + "--axis", "fast.length=2,4", + "--axis", "slow.length=8", "--name", "repro_sweep", ]) .current_dir(&dir) @@ -2014,7 +2019,14 @@ fn reproduce_real_sweep_family_re_derives_bit_identically() { /// `sma_signal.slow.length` bound), overriding only the `fast.length` axis and /// leaving `slow.length` bound — unlike the walk-forward reproduce sibling /// (`aura_walkforward_over_a_blueprint_reproduces_bit_identically`), which -/// overrides both axes over the same closed blueprint. +/// overrides both axes over the same closed blueprint. The walk-forward +/// synthetic route's own `--axis` intake is UNCHANGED by #328 (out of this +/// cycle's scope — batch 1 is the sweep verb's raw switch only), so this axis +/// is still typed WRAPPED here (`sma_signal.fast.length`); `override_paths`'s +/// #328 fuzzy matching tolerates either shape, but the `SweepBinder` call this +/// route drives (`blueprint_sweep_over`, untouched this cycle) still keys by +/// the exact wrapped `param_space()` name, so a RAW-typed axis here would +/// mismatch and misreport `UnknownKnob`. /// Modelled on that sibling (synthetic in-process walk-forward — no `--real`), /// NOT on `reproduce_real_sweep_family_re_derives_bit_identically`: a real-data /// mint routes through the campaign executor (`CliMemberRunner::run_member` + @@ -2024,7 +2036,7 @@ fn reproduce_real_sweep_family_re_derives_bit_identically() { /// it here would silently expand into unrelated, unreviewed surface. The /// synthetic walk-forward path exercises exactly the threaded functions: /// `blueprint_sweep_over` (via `blueprint_walkforward_family`) re-opens -/// `sma_signal.fast.length` for the per-window IS re-fit, and +/// `fast.length` for the per-window IS re-fit, and /// `reproduce_family_in` re-opens it again from the recorded manifest params /// so the re-derivation resolves against the same space the mint used. #[test] @@ -2078,7 +2090,7 @@ fn aura_reproduce_re_derives_a_sweep_family_minted_over_a_bound_param_override() let cwd = temp_cwd("sweep-reproduce-bound-override"); let bp = format!("{}/examples/r_sma.json", env!("CARGO_MANIFEST_DIR")); let sweep = Command::new(BIN) - .args(["sweep", &bp, "--axis", "sma_signal.fast.length=2,3", "--name", "swo"]) + .args(["sweep", &bp, "--axis", "fast.length=2,3", "--name", "swo"]) .current_dir(&cwd) .output() .expect("spawn aura sweep over a bound-param axis"); @@ -2135,8 +2147,8 @@ fn reproduce_real_walkforward_family_does_not_panic() { "--real", "GER40", "--from", WF_FROM_MS, "--to", WF_TO_MS, - "--axis", "sma_signal.fast.length=2,4", - "--axis", "sma_signal.slow.length=8", + "--axis", "fast.length=2,4", + "--axis", "slow.length=8", "--name", "repro_wf", ]) .current_dir(&dir) @@ -2221,8 +2233,8 @@ fn walkforward_synthetic_refuses_a_multi_column_blueprint_before_data_access() { let out = Command::new(BIN) .args([ "walkforward", fixture.to_str().unwrap(), - "--axis", "hl_signal.fast.length=2,4", - "--axis", "hl_signal.slow.length=8", + "--axis", "fast.length=2,4", + "--axis", "slow.length=8", ]) .current_dir(&dir) .output() @@ -2299,8 +2311,8 @@ fn sweep_real_multi_column_blueprint_opens_high_and_low_columns() { "--real", "GER40", "--from", GER40_SEPT2024_FROM_MS, "--to", GER40_SEPT2024_TO_MS, - "--axis", "hl_signal.fast.length=2,4", - "--axis", "hl_signal.slow.length=8", + "--axis", "fast.length=2,4", + "--axis", "slow.length=8", ]) .current_dir(&dir) .output() @@ -2319,19 +2331,20 @@ fn sweep_real_multi_column_blueprint_opens_high_and_low_columns() { let _ = std::fs::remove_dir_all(&fixture_dir); } -/// Property (#210 c0110 fieldtest, "the wrapped-name refusal mangles the axis -/// the user typed"): the dissolved real-data blueprint sweep types `--axis` -/// against the WRAPPED probe namespace (`sma_signal.fast.length`, what -/// `--list-axes` prints), not the raw campaign-document namespace -/// (`fast.length`). A user who types the raw form must be refused up front, -/// echoing exactly what they typed — never a stripped-then-mangled fragment -/// (the old failure mode: `fast.length` -> strip one segment -> the -/// nonsensical bare `"length"`). NOT gated: the preflight is data-free (it +/// Property (#328, new e2e — supersedes the pre-#328 "raw form is refused" +/// pin, inverted by the reconciliation): the dissolved real-data blueprint +/// sweep's `--axis` now accepts ONLY the RAW `.` namespace +/// (`fast.length`, what `--list-axes` prints, #328) — the WRAPPED +/// `..` form (`sma_signal.fast.length`, the old +/// `--list-axes` shape) is retired from the surface. A user who types the +/// wrapped form must be refused up front, echoing exactly what they typed and +/// naming the translated raw candidate — never a silent alias, never a +/// stripped-then-mangled fragment. NOT gated: the preflight is data-free (it /// only reloads the blueprint to probe its param space), so the refusal /// fires before the archive is ever touched — this test needs no local data. #[test] -fn aura_sweep_real_blueprint_refuses_a_raw_form_axis_name_before_data_access() { - let dir = temp_cwd("sweep_real_rawname_refusal"); +fn aura_sweep_real_blueprint_refuses_a_wrapped_form_axis_name_before_data_access() { + let dir = temp_cwd("sweep_real_wrappedname_refusal"); let fixture = format!("{}/examples/r_sma.json", env!("CARGO_MANIFEST_DIR")); let out = Command::new(BIN) .args([ @@ -2339,38 +2352,80 @@ fn aura_sweep_real_blueprint_refuses_a_raw_form_axis_name_before_data_access() { "--real", "GER40", "--from", GER40_SEPT2024_FROM_MS, "--to", GER40_SEPT2024_TO_MS, - "--axis", "fast.length=2,4", - "--name", "c0110-rawname", + "--axis", "sma_signal.fast.length=2,4", + "--name", "c0110-wrappedname", ]) .current_dir(&dir) .output() - .expect("spawn aura sweep (raw-form axis)"); - assert_eq!(out.status.code(), Some(2), "a raw-form axis name must be refused, not run: status={:?}", out.status); + .expect("spawn aura sweep (wrapped-form axis)"); + assert_eq!(out.status.code(), Some(2), "a wrapped-form axis name must be refused, not run: status={:?}", out.status); let stderr = String::from_utf8_lossy(&out.stderr).into_owned(); assert!( - stderr.contains("axis \"fast.length\""), + stderr.contains("axis \"sma_signal.fast.length\""), "the refusal echoes exactly the name the user typed: {stderr}" ); assert!( - !stderr.contains("axis \"length\""), - "must never mangle the typed axis into a stripped fragment: {stderr}" + stderr.contains("axis names are raw node.param paths") && stderr.contains("\"fast.length\""), + "translates the wrapped name to its raw candidate, naming the retirement: {stderr}" ); // Data-free: no archive/store access at all. assert!(!dir.join("runs").exists(), "a refused axis name must not start a real run"); let _ = std::fs::remove_dir_all(&dir); } -/// Property (#247): every sweep-verb boundary `BindError` renders as prose in -/// the same register as its unknown-axis sibling -/// (`aura: axis : names no param of this blueprint … — see `aura sweep -/// --list-axes``) — never a raw Rust Debug struct. A wrong-kind axis value -/// and an uncovered open knob are the two variants that today leak -/// `KindMismatch { knob: …, expected: …, got: … }` and -/// `MissingKnob("…")` verbatim to stderr; both must instead name the axis, carry -/// the specifics (expected vs supplied kind / the unbound knob), and point at -/// `--list-axes`, with the exit code staying 2. Data-free: both faults are -/// caught at the axis-resolution boundary before any member runs, so the test -/// needs no archive and no project (hostless synthetic sweep). +/// Property (#328, synthetic-route sibling of +/// `aura_sweep_real_blueprint_refuses_a_wrapped_form_axis_name_before_data_access`): +/// the SYNTHETIC (no `--real`) blueprint sweep bypasses `validate_and_register_axes` +/// entirely (no project/registry touched), so it needs its OWN wrapped-name +/// preflight (`refuse_wrapped_synthetic_axes`) — and that preflight must refuse +/// a wrapped `..` axis name exactly like the real +/// route does: echoing the typed name and naming the translated raw +/// candidate, never silently accepting it or running the sweep. +#[test] +fn aura_sweep_synthetic_blueprint_refuses_a_wrapped_form_axis_name() { + let dir = temp_cwd("sweep_synthetic_wrappedname_refusal"); + let fixture = format!("{}/examples/r_sma.json", env!("CARGO_MANIFEST_DIR")); + let out = Command::new(BIN) + .args(["sweep", &fixture, "--axis", "sma_signal.fast.length=2,4"]) + .current_dir(&dir) + .output() + .expect("spawn aura sweep (synthetic, wrapped-form axis)"); + assert_eq!( + out.status.code(), + Some(2), + "a wrapped-form axis name must be refused, not run: status={:?}", + out.status + ); + let stderr = String::from_utf8_lossy(&out.stderr).into_owned(); + assert!( + stderr.contains("axis \"sma_signal.fast.length\""), + "the refusal echoes exactly the name the user typed: {stderr}" + ); + assert!( + stderr.contains("axis names are raw node.param paths") && stderr.contains("\"fast.length\""), + "translates the wrapped name to its raw candidate, naming the retirement: {stderr}" + ); + // Data-free: no archive/store access at all — this route never touches a + // project store, so there is no `runs` dir to check for absence either + // way, but stdout must carry no member line (the sweep never ran). + assert!(out.stdout.is_empty(), "a refused axis name must not print a member line: {out:?}"); + let _ = std::fs::remove_dir_all(&dir); +} + +/// Property (#247, #328 render-seam echo): every sweep-verb boundary +/// `BindError` renders as prose in the same register as its unknown-axis +/// sibling (`aura: axis : names no param of this blueprint … — see +/// `aura sweep --list-axes``) — never a raw Rust Debug struct, and never +/// the WRAPPED `..` knob name `BindError` carries +/// internally (acceptance-criterion 2: no user-facing surface prints a +/// wrapped axis name). A wrong-kind axis value and an uncovered open knob are +/// the two variants that today leak `KindMismatch { knob: …, expected: …, +/// got: … }` and `MissingKnob("…")` verbatim to stderr; both must instead name +/// the RAW axis, carry the specifics (expected vs supplied kind / the unbound +/// knob), and point at `--list-axes`, with the exit code staying 2. Data-free: +/// both faults are caught at the axis-resolution boundary before any member +/// runs, so the test needs no archive and no project (hostless synthetic +/// sweep). #[test] fn sweep_boundary_bind_errors_render_as_prose_not_debug_structs() { let dir = temp_cwd("sweep-bind-error-prose"); @@ -2379,7 +2434,7 @@ fn sweep_boundary_bind_errors_render_as_prose_not_debug_structs() { // supplies F64 values: the specifics are the expected vs supplied kind. let closed = format!("{}/examples/r_sma.json", env!("CARGO_MANIFEST_DIR")); let km = Command::new(BIN) - .args(["sweep", &closed, "--axis", "sma_signal.fast.length=2.5,3.5"]) + .args(["sweep", &closed, "--axis", "fast.length=2.5,3.5"]) .current_dir(&dir) .output() .expect("spawn aura sweep (kind-mismatched axis)"); @@ -2395,9 +2450,14 @@ fn sweep_boundary_bind_errors_render_as_prose_not_debug_structs() { "must not leak the raw Rust Debug struct name: {km_err}" ); assert!( - km_err.contains("sma_signal.fast.length"), + km_err.contains("fast.length"), "the prose names the offending axis exactly as typed: {km_err}" ); + assert!( + km_err.contains("axis fast.length:") && !km_err.contains("sma_signal.fast.length"), + "the RAW axis echoes back verbatim — never the wrapped blueprint-\ + qualified knob name the BindError carries internally: {km_err}" + ); assert!( km_err.contains("I64") && km_err.contains("F64"), "the prose carries the expected vs supplied kind specifics: {km_err}" @@ -2411,7 +2471,7 @@ fn sweep_boundary_bind_errors_render_as_prose_not_debug_structs() { // `fast.length` is swept: the specific is the uncovered open knob. let open = format!("{}/tests/fixtures/r_sma_open.json", env!("CARGO_MANIFEST_DIR")); let mk = Command::new(BIN) - .args(["sweep", &open, "--axis", "sma_signal.fast.length=2,4"]) + .args(["sweep", &open, "--axis", "fast.length=2,4"]) .current_dir(&dir) .output() .expect("spawn aura sweep (uncovered open knob)"); @@ -2427,9 +2487,14 @@ fn sweep_boundary_bind_errors_render_as_prose_not_debug_structs() { "must not leak the raw Rust Debug struct name: {mk_err}" ); assert!( - mk_err.contains("sma_signal.slow.length"), + mk_err.contains("slow.length"), "the prose names the unbound knob no axis supplies: {mk_err}" ); + assert!( + mk_err.contains("axis slow.length:") && !mk_err.contains("sma_signal.slow.length"), + "the RAW knob echoes back verbatim — never the wrapped blueprint-\ + qualified knob name the BindError carries internally: {mk_err}" + ); assert!( mk_err.contains("--list-axes"), "the prose points at the discovery surface, like the unknown-axis sibling: {mk_err}" @@ -2462,7 +2527,7 @@ fn aura_sweep_real_blueprint_subset_axes_refuses_without_store_litter() { "--real", "GER40", "--from", GER40_SEPT2024_FROM_MS, "--to", GER40_SEPT2024_TO_MS, - "--axis", "sma_signal.fast.length=2,4", + "--axis", "fast.length=2,4", "--name", "c0110-subset", ]) .current_dir(&dir) @@ -2500,8 +2565,8 @@ fn aura_sweep_real_blueprint_subset_axes_refuses_without_store_litter() { /// (the dissolved-sweep e2e family `walkforward_dissolved_refuses_an_unknown_axis_name` /// belongs to): same store-reading assertions (`aura runs families`, one /// persisted `Sweep` family, member count), over the same `r_sma.json` -/// fixture every other real-data sweep pin uses (`sma_signal.fast.length`/ -/// `sma_signal.slow.length` both bound) — the distinguishing property here +/// fixture every other real-data sweep pin uses (`fast.length`/ +/// `slow.length` both bound) — the distinguishing property here /// is that the swept axis names an already-bound param, not that the /// fixture differs. #[test] @@ -2518,7 +2583,7 @@ fn sweep_dissolved_accepts_an_axis_over_a_bound_param() { "--real", "GER40", "--from", GER40_SEPT2024_FROM_MS, "--to", GER40_SEPT2024_TO_MS, - "--axis", "sma_signal.fast.length=2,3", + "--axis", "fast.length=2,3", "--name", "bound-override", ]) .current_dir(&dir) @@ -2562,8 +2627,8 @@ fn sweep_over_a_bound_override_matches_the_historical_open_bind() { let closed_out = Command::new(BIN) .args([ "sweep", &closed_bp, - "--axis", "sma_signal.fast.length=2,4", - "--axis", "sma_signal.slow.length=8,16", + "--axis", "fast.length=2,4", + "--axis", "slow.length=8,16", ]) .current_dir(&closed_dir) .output() @@ -2580,8 +2645,8 @@ fn sweep_over_a_bound_override_matches_the_historical_open_bind() { let open_out = Command::new(BIN) .args([ "sweep", &open_bp, - "--axis", "sma_signal.fast.length=2,4", - "--axis", "sma_signal.slow.length=8,16", + "--axis", "fast.length=2,4", + "--axis", "slow.length=8,16", ]) .current_dir(&open_dir) .output() @@ -2960,7 +3025,7 @@ fn generalize_grades_a_candidate_across_two_instruments() { let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura")) .args([ "generalize", &fixture, "--real", "GER40,USDJPY", - "--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12", + "--axis", "fast.length=3", "--axis", "slow.length=12", "--stop-length", "14", "--stop-k", "2.0", "--from", FROM_MS, "--to", TO_MS, ]) @@ -3006,7 +3071,7 @@ fn generalize_real_e2e_pins_the_exact_current_grade() { let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura")) .args([ "generalize", &fixture, "--real", "GER40,USDJPY", - "--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12", + "--axis", "fast.length=3", "--axis", "slow.length=12", "--stop-length", "14", "--stop-k", "2.0", "--from", FROM_MS, "--to", TO_MS, ]) @@ -3063,7 +3128,7 @@ fn generalize_grades_a_non_r_sma_blueprint() { let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura")) .args([ "generalize", &fixture, "--real", "GER40,USDJPY", - "--axis", "r_breakout_signal.channel_length=20", + "--axis", "channel_length=20", "--stop-length", "14", "--stop-k", "2.0", "--from", FROM_MS, "--to", TO_MS, ]) @@ -3111,7 +3176,7 @@ fn walkforward_real_e2e_pins_the_exact_current_grade() { let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura")) .args([ "walkforward", &fixture, "--real", "GER40", - "--axis", "sma_signal.fast.length=3,5", "--axis", "sma_signal.slow.length=12,20", + "--axis", "fast.length=3,5", "--axis", "slow.length=12,20", "--stop-length", "14", "--stop-k", "2.0", "--from", FROM_MS, "--to", TO_MS, ]) @@ -3173,7 +3238,7 @@ fn walkforward_dissolves_a_non_r_sma_blueprint() { let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura")) .args([ "walkforward", &fixture, "--real", "GER40", - "--axis", "r_breakout_signal.channel_length=10,20", + "--axis", "channel_length=10,20", "--stop-length", "14", "--stop-k", "2.0", "--from", FROM_MS, "--to", TO_MS, ]) @@ -3212,7 +3277,7 @@ fn walkforward_dissolved_refuses_a_multi_value_stop() { let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura")) .args([ "walkforward", &fixture, "--real", "GER40", - "--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12", + "--axis", "fast.length=3", "--axis", "slow.length=12", "--stop-length", "14,20", "--stop-k", "2.0", ]) .current_dir(&cwd) @@ -3237,7 +3302,7 @@ fn walkforward_dissolved_accepts_select_plateau() { let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura")) .args([ "walkforward", &fixture, "--real", "GER40", - "--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12", + "--axis", "fast.length=3", "--axis", "slow.length=12", "--stop-length", "14", "--stop-k", "2.0", "--select", "plateau:worst", "--from", "1735689600000", "--to", "1747353600000", @@ -3282,7 +3347,7 @@ fn walkforward_real_e2e_pins_the_exact_current_plateau_grade() { let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura")) .args([ "walkforward", &fixture, "--real", "GER40", - "--axis", "sma_signal.fast.length=3,5", "--axis", "sma_signal.slow.length=12,20", + "--axis", "fast.length=3,5", "--axis", "slow.length=12,20", "--stop-length", "14", "--stop-k", "2.0", "--from", FROM_MS, "--to", TO_MS, "--select", "plateau:worst", @@ -3338,7 +3403,7 @@ fn walkforward_dissolved_defaults_a_single_omitted_knob() { let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura")) .args([ "walkforward", &fixture, "--real", "GER40", - "--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12", + "--axis", "fast.length=3", "--axis", "slow.length=12", "--stop-length", "3", "--from", "1735689600000", "--to", "1747353600000", ]) @@ -3372,7 +3437,7 @@ fn walkforward_dissolved_refuses_an_empty_real_symbol() { let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura")) .args([ "walkforward", &fixture, "--real", "", - "--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12", + "--axis", "fast.length=3", "--axis", "slow.length=12", "--stop-length", "14", "--stop-k", "2.0", ]) .current_dir(&cwd) @@ -3426,7 +3491,7 @@ fn walkforward_dissolved_refuses_an_unknown_select_token() { let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura")) .args([ "walkforward", &fixture, "--real", "GER40", - "--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12", + "--axis", "fast.length=3", "--axis", "slow.length=12", "--stop-length", "14", "--stop-k", "2.0", "--select", "bogus", ]) @@ -3450,7 +3515,7 @@ fn walkforward_dissolved_refuses_name_and_trace_together() { let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura")) .args([ "walkforward", &fixture, "--real", "GER40", - "--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12", + "--axis", "fast.length=3", "--axis", "slow.length=12", "--stop-length", "14", "--stop-k", "2.0", "--name", "a", "--trace", "b", ]) @@ -3486,7 +3551,7 @@ fn walkforward_dissolves_through_the_campaign_path() { let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura")) .args([ "walkforward", &fixture, "--real", "GER40", - "--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12", + "--axis", "fast.length=3", "--axis", "slow.length=12", "--stop-length", "14", "--stop-k", "2.0", "--from", FROM_MS, "--to", TO_MS, ]) @@ -3581,7 +3646,7 @@ fn walkforward_stopless_defaults_the_regime_byte_identically_to_explicit_3_2() { let stopless = std::process::Command::new(env!("CARGO_BIN_EXE_aura")) .args([ "walkforward", &fixture, "--real", "GER40", - "--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12", + "--axis", "fast.length=3", "--axis", "slow.length=12", "--from", FROM_MS, "--to", TO_MS, ]) .current_dir(&cwd) @@ -3609,7 +3674,7 @@ fn walkforward_stopless_defaults_the_regime_byte_identically_to_explicit_3_2() { let explicit = std::process::Command::new(env!("CARGO_BIN_EXE_aura")) .args([ "walkforward", &fixture, "--real", "GER40", - "--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12", + "--axis", "fast.length=3", "--axis", "slow.length=12", "--stop-length", "3", "--stop-k", "2.0", "--from", FROM_MS, "--to", TO_MS, ]) @@ -3668,7 +3733,7 @@ fn walkforward_bare_plateau_generates_the_same_campaign_as_plateau_mean() { let bare = std::process::Command::new(env!("CARGO_BIN_EXE_aura")) .args([ "walkforward", &fixture, "--real", "GER40", - "--axis", "sma_signal.fast.length=3,5", "--axis", "sma_signal.slow.length=12,20", + "--axis", "fast.length=3,5", "--axis", "slow.length=12,20", "--stop-length", "14", "--stop-k", "2.0", "--select", "plateau", "--from", FROM_MS, "--to", TO_MS, @@ -3697,7 +3762,7 @@ fn walkforward_bare_plateau_generates_the_same_campaign_as_plateau_mean() { let explicit = std::process::Command::new(env!("CARGO_BIN_EXE_aura")) .args([ "walkforward", &fixture, "--real", "GER40", - "--axis", "sma_signal.fast.length=3,5", "--axis", "sma_signal.slow.length=12,20", + "--axis", "fast.length=3,5", "--axis", "slow.length=12,20", "--stop-length", "14", "--stop-k", "2.0", "--select", "plateau:mean", "--from", FROM_MS, "--to", TO_MS, @@ -3753,7 +3818,7 @@ fn generalize_dissolves_through_the_campaign_path() { let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura")) .args([ "generalize", &fixture, "--real", "GER40,USDJPY", - "--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12", + "--axis", "fast.length=3", "--axis", "slow.length=12", "--stop-length", "14", "--stop-k", "2.0", "--from", FROM_MS, "--to", TO_MS, ]) @@ -3845,7 +3910,7 @@ fn generalize_repeated_identical_invocation_does_not_litter_the_store() { std::process::Command::new(env!("CARGO_BIN_EXE_aura")) .args([ "generalize", &fixture, "--real", "GER40,USDJPY", - "--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12", + "--axis", "fast.length=3", "--axis", "slow.length=12", "--stop-length", "14", "--stop-k", "2.0", "--from", FROM_MS, "--to", TO_MS, ]) @@ -3897,11 +3962,11 @@ fn generalize_distinct_invocations_persist_distinct_campaign_documents() { let (cwd, _g) = fresh_project(); let fixture = format!("{}/examples/r_sma.json", env!("CARGO_MANIFEST_DIR")); let invoke = |fast: &str| { - let fast_axis = format!("sma_signal.fast.length={fast}"); + let fast_axis = format!("fast.length={fast}"); std::process::Command::new(env!("CARGO_BIN_EXE_aura")) .args([ "generalize", &fixture, "--real", "GER40,USDJPY", - "--axis", &fast_axis, "--axis", "sma_signal.slow.length=12", + "--axis", &fast_axis, "--axis", "slow.length=12", "--stop-length", "14", "--stop-k", "2.0", "--from", FROM_MS, "--to", TO_MS, ]) @@ -3952,7 +4017,7 @@ fn generalize_without_explicit_window_resolves_a_shared_window_and_completes() { let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura")) .args([ "generalize", &fixture, "--real", "SYMA,SYMB", - "--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12", + "--axis", "fast.length=3", "--axis", "slow.length=12", "--stop-length", "14", "--stop-k", "2.0", ]) .current_dir(&cwd) @@ -4018,7 +4083,7 @@ fn generalize_without_explicit_window_resolves_the_intersection_of_all_symbols() let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura")) .args([ "sweep", &fixture, "--real", symbol, - "--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12", + "--axis", "fast.length=3", "--axis", "slow.length=12", "--name", name, ]) .current_dir(&cwd) @@ -4050,7 +4115,7 @@ fn generalize_without_explicit_window_resolves_the_intersection_of_all_symbols() let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura")) .args([ "generalize", &fixture, "--real", "SYMA,SYMB", - "--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12", + "--axis", "fast.length=3", "--axis", "slow.length=12", "--name", "gen-both", ]) .current_dir(&cwd) @@ -4078,7 +4143,7 @@ fn generalize_refuses_a_single_instrument() { let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura")) .args([ "generalize", &fixture, "--real", "GER40", - "--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12", + "--axis", "fast.length=3", "--axis", "slow.length=12", "--stop-length", "14", "--stop-k", "2.0", ]) .output() @@ -4094,7 +4159,7 @@ fn generalize_refuses_a_non_r_metric() { let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura")) .args([ "generalize", &fixture, "--real", "GER40,USDJPY", - "--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12", + "--axis", "fast.length=3", "--axis", "slow.length=12", "--stop-length", "14", "--stop-k", "2.0", "--metric", "total_pips", ]) @@ -4119,7 +4184,7 @@ fn generalize_refuses_a_duplicate_instrument() { let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura")) .args([ "generalize", &fixture, "--real", "GER40,GER40", - "--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12", + "--axis", "fast.length=3", "--axis", "slow.length=12", "--stop-length", "14", "--stop-k", "2.0", ]) .output() @@ -4144,7 +4209,7 @@ fn generalize_refuses_a_multi_value_axis() { let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura")) .args([ "generalize", &fixture, "--real", "GER40,USDJPY", - "--axis", "sma_signal.fast.length=2,3", "--axis", "sma_signal.slow.length=12", + "--axis", "fast.length=2,3", "--axis", "slow.length=12", "--stop-length", "14", "--stop-k", "2.0", ]) .output() @@ -4229,7 +4294,7 @@ fn generalize_persists_a_discoverable_cross_instrument_family() { let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura")) .args([ "generalize", &fixture, "--real", "GER40,USDJPY", - "--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12", + "--axis", "fast.length=3", "--axis", "slow.length=12", "--stop-length", "14", "--stop-k", "2.0", "--from", FROM_MS, "--to", TO_MS, ]) @@ -4291,7 +4356,7 @@ fn generalize_family_members_are_attributed_to_the_right_instrument() { let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura")) .args([ "generalize", &fixture, "--real", "GER40,USDJPY", - "--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12", + "--axis", "fast.length=3", "--axis", "slow.length=12", "--stop-length", "14", "--stop-k", "2.0", "--from", FROM_MS, "--to", TO_MS, ]) @@ -4358,7 +4423,7 @@ fn generalize_family_members_stamp_project_provenance() { let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura")) .args([ "generalize", &fixture, "--real", "GER40,USDJPY", - "--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12", + "--axis", "fast.length=3", "--axis", "slow.length=12", "--stop-length", "14", "--stop-k", "2.0", "--from", FROM_MS, "--to", TO_MS, ]) @@ -4411,8 +4476,8 @@ fn aura_sweep_loads_a_blueprint_and_persists_a_sweep_family() { let out = Command::new(BIN) .args([ "sweep", &fixture, - "--axis", "sma_signal.fast.length=2,4", - "--axis", "sma_signal.slow.length=8,16", + "--axis", "fast.length=2,4", + "--axis", "slow.length=8,16", "--name", "f", ]) .current_dir(&cwd) @@ -4448,9 +4513,9 @@ fn sweep_and_walkforward_refuse_trace_with_a_forward_pointer() { verb, &fixture, "--axis", - "sma_signal.fast.length=2,4", + "fast.length=2,4", "--axis", - "sma_signal.slow.length=8,16", + "slow.length=8,16", "--trace", "fam", ]) @@ -4498,8 +4563,8 @@ fn aura_sweep_persists_the_canonical_blueprint_keyed_by_topology_hash() { let out = Command::new(BIN) .args([ "sweep", &fixture, - "--axis", "sma_signal.fast.length=2,4", - "--axis", "sma_signal.slow.length=8,16", + "--axis", "fast.length=2,4", + "--axis", "slow.length=8,16", "--name", "f", ]) .current_dir(&cwd) @@ -4576,8 +4641,8 @@ fn aura_reproduce_re_derives_a_persisted_sweep_family() { let sweep = Command::new(BIN) .args([ "sweep", &fixture, - "--axis", "sma_signal.fast.length=2,4", - "--axis", "sma_signal.slow.length=8,16", + "--axis", "fast.length=2,4", + "--axis", "slow.length=8,16", "--name", "f", ]) .current_dir(&cwd) @@ -4600,7 +4665,7 @@ fn aura_reproduce_re_derives_a_persisted_sweep_family() { 4, "every member re-derives bit-identically: {stdout}" ); - assert!(stdout.contains("sma_signal.fast.length=2"), "label renders values portably: {stdout}"); + assert!(stdout.contains("fast.length=2"), "label renders values portably: {stdout}"); assert!(!stdout.contains("I64("), "label must not leak the Scalar Debug form: {stdout}"); assert!(stdout.contains("reproduced 4/4 members bit-identically"), "summary line: {stdout}"); let _ = std::fs::remove_dir_all(&cwd); @@ -4617,8 +4682,8 @@ fn aura_reproduce_reports_a_diverged_member_and_exits_one() { let sweep = Command::new(BIN) .args([ "sweep", &fixture, - "--axis", "sma_signal.fast.length=2,4", - "--axis", "sma_signal.slow.length=8,16", + "--axis", "fast.length=2,4", + "--axis", "slow.length=8,16", "--name", "f", ]) .current_dir(&cwd) @@ -4659,9 +4724,9 @@ fn aura_reproduce_re_derives_a_persisted_sweep_bit_identically() { "sweep", &fixture, "--axis", - "sma_signal.fast.length=2", + "fast.length=2", "--axis", - "sma_signal.slow.length=4,6", + "slow.length=4,6", "--name", "smacross", ]) @@ -4733,6 +4798,8 @@ fn aura_mc_over_a_blueprint_reproduces_bit_identically() { /// E2E (#173): an IS-refit walk-forward over a loaded blueprint persists a /// content-addressed WalkForward family that `aura reproduce`s bit-identically — /// each OOS member's windowed slice + winner params are recovered from its manifest. +/// The synthetic walk-forward route's own `--axis` intake is unchanged by #328 +/// (batch 1 is the sweep verb's raw switch only), so these axes stay WRAPPED. #[test] fn aura_walkforward_over_a_blueprint_reproduces_bit_identically() { let cwd = temp_cwd("blueprint-walkforward-reproduce"); @@ -4889,7 +4956,7 @@ fn aura_sweep_synthetic_member_panic_is_contained() { let cwd = temp_cwd("blueprint-sweep-member-panic-contained"); let bp = format!("{}/examples/r_sma.json", env!("CARGO_MANIFEST_DIR")); let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura")) - .args(["sweep", &bp, "--axis", "sma_signal.fast.length=0", "--name", "swpanic"]) + .args(["sweep", &bp, "--axis", "fast.length=0", "--name", "swpanic"]) .current_dir(&cwd) .output() .expect("spawn aura sweep (poison member)"); @@ -4984,7 +5051,7 @@ fn walkforward_real_all_zero_trade_windows_emit_the_note() { let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura")) .args([ "walkforward", &fixture, "--real", "GER40", - "--axis", "sma_signal.fast.length=5", "--axis", "sma_signal.slow.length=5", + "--axis", "fast.length=5", "--axis", "slow.length=5", "--stop-length", "14", "--stop-k", "2.0", "--from", FROM_MS, "--to", TO_MS, ]) @@ -5032,7 +5099,7 @@ fn aura_walkforward_over_a_blueprint_rejects_no_axis() { fn aura_walkforward_over_a_blueprint_rejects_malformed() { let bp = format!("{}/tests/fixtures/unknown_node.json", env!("CARGO_MANIFEST_DIR")); let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura")) - .args(["walkforward", &bp, "--axis", "sma_signal.fast.length=2,3"]) + .args(["walkforward", &bp, "--axis", "fast.length=2,3"]) .output() .expect("spawn aura walkforward (malformed)"); assert_eq!(out.status.code(), Some(2), "malformed must be rejected, not panic"); @@ -5244,9 +5311,9 @@ fn aura_reproduce_rejects_a_missing_stored_blueprint() { "sweep", &fixture, "--axis", - "sma_signal.fast.length=2", + "fast.length=2", "--axis", - "sma_signal.slow.length=4", + "slow.length=4", "--name", "bp", ]) @@ -5269,16 +5336,17 @@ fn aura_reproduce_rejects_a_missing_stored_blueprint() { let _ = std::fs::remove_dir_all(&cwd); } -/// E2E (#169/#246): `aura sweep --list-axes` prints one +/// E2E (#169/#246/#328): `aura sweep --list-axes` prints one /// `:` line per open sweepable knob, in `param_space()` order, /// followed by one `: default=` line per BOUND param (the /// fixture's `bias.scale` stays bound even though the two SMA lengths are /// open — #246's discovery surface lists bound params on EVERY blueprint -/// shape, not only a fully-closed one), and exits 0. The names are prefixed -/// by the wrapping (sma_signal.*) — exactly the strings `--axis` binds — so a +/// shape, not only a fully-closed one), and exits 0. The names are RAW +/// `.` paths (#328: the wrapped `..` form +/// is retired from the surface) — exactly the strings `--axis` binds — so a /// user can discover then sweep or re-open without guessing. #[test] -fn aura_sweep_list_axes_prints_prefixed_names() { +fn aura_sweep_list_axes_prints_raw_names() { let bp = format!("{}/tests/fixtures/r_sma_open.json", env!("CARGO_MANIFEST_DIR")); let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura")) .args(["sweep", &bp, "--list-axes"]) @@ -5288,17 +5356,17 @@ fn aura_sweep_list_axes_prints_prefixed_names() { let stdout = String::from_utf8(out.stdout).expect("utf-8"); assert_eq!( stdout, - "sma_signal.fast.length:I64\n\ - sma_signal.slow.length:I64\n\ - sma_signal.bias.scale:F64 default=0.5\n" + "fast.length:I64\n\ + slow.length:I64\n\ + bias.scale:F64 default=0.5\n" ); } -/// E2E (#246): `--list-axes` on a CLOSED blueprint (all knobs bound) prints one -/// `..: default=` line per bound param — the -/// bound value IS the default, overridable by naming the same path as an +/// E2E (#246/#328): `--list-axes` on a CLOSED blueprint (all knobs bound) prints one +/// `.: default=` line per bound param, RAW (#328) — +/// the bound value IS the default, overridable by naming the same path as an /// `--axis` (#246's re-open contract) — and exits 0. The sibling pin above -/// (`aura_sweep_list_axes_prints_prefixed_names`) proves the same bound-param +/// (`aura_sweep_list_axes_prints_raw_names`) proves the same bound-param /// lines also appear on a PARTIALLY open blueprint, after its open lines. #[test] fn aura_sweep_list_axes_closed_prints_bound_defaults() { @@ -5311,9 +5379,9 @@ fn aura_sweep_list_axes_closed_prints_bound_defaults() { let stdout = String::from_utf8(out.stdout).expect("utf-8"); assert_eq!( stdout, - "sma_signal.fast.length:I64 default=2\n\ - sma_signal.slow.length:I64 default=4\n\ - sma_signal.bias.scale:F64 default=0.5\n" + "fast.length:I64 default=2\n\ + slow.length:I64 default=4\n\ + bias.scale:F64 default=0.5\n" ); } @@ -5324,7 +5392,7 @@ fn aura_sweep_list_axes_closed_prints_bound_defaults() { fn aura_sweep_list_axes_rejects_other_flags() { let bp = format!("{}/examples/r_sma.json", env!("CARGO_MANIFEST_DIR")); let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura")) - .args(["sweep", &bp, "--list-axes", "--axis", "sma_signal.fast.length=2,3"]) + .args(["sweep", &bp, "--list-axes", "--axis", "fast.length=2,3"]) .output() .expect("spawn aura sweep --list-axes + --axis"); assert_eq!(out.status.code(), Some(2)); @@ -5711,7 +5779,7 @@ fn mc_r_bootstrap_real_e2e_pins_the_exact_current_grade() { let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura")) .args([ "mc", &fixture, "--real", "GER40", - "--axis", "sma_signal.fast.length=3,5", "--axis", "sma_signal.slow.length=12,20", + "--axis", "fast.length=3,5", "--axis", "slow.length=12,20", "--stop-length", "14", "--stop-k", "2.0", "--block-len", "5", "--resamples", "1000", "--seed", "42", "--from", FROM_MS, "--to", TO_MS, @@ -5768,7 +5836,7 @@ fn mc_dissolves_a_non_r_sma_blueprint() { let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura")) .args([ "mc", &fixture, "--real", "GER40", - "--axis", "r_breakout_signal.channel_length=10,20", + "--axis", "channel_length=10,20", "--stop-length", "14", "--stop-k", "2.0", "--block-len", "5", "--resamples", "1000", "--seed", "42", "--from", FROM_MS, "--to", TO_MS, @@ -5824,7 +5892,7 @@ fn mc_dissolves_through_the_campaign_path() { let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura")) .args([ "mc", &fixture, "--real", "GER40", - "--axis", "sma_signal.fast.length=3,5", "--axis", "sma_signal.slow.length=12,20", + "--axis", "fast.length=3,5", "--axis", "slow.length=12,20", "--stop-length", "14", "--stop-k", "2.0", "--block-len", "5", "--resamples", "1000", "--seed", "42", "--from", FROM_MS, "--to", TO_MS, @@ -5934,7 +6002,7 @@ fn mc_real_fits_the_injected_roller_to_a_short_window() { let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura")) .args([ "mc", &fixture, "--real", "GER40", - "--axis", "sma_signal.fast.length=3,5", "--axis", "sma_signal.slow.length=12,20", + "--axis", "fast.length=3,5", "--axis", "slow.length=12,20", "--stop-length", "14", "--stop-k", "2.0", "--block-len", "5", "--resamples", "100", "--seed", "42", "--from", SUB_FROM_MS, "--to", SUB_TO_MS, @@ -5975,7 +6043,7 @@ fn mc_dissolved_refuses_a_multi_value_stop() { let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura")) .args([ "mc", &fixture, "--real", "GER40", - "--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12", + "--axis", "fast.length=3", "--axis", "slow.length=12", "--stop-length", "14,20", "--stop-k", "2.0", ]) .current_dir(&cwd) @@ -6043,7 +6111,7 @@ fn mc_dissolved_seed_changes_the_bootstrap_draw_not_the_pooled_series() { std::process::Command::new(env!("CARGO_BIN_EXE_aura")) .args([ "mc", &fixture, "--real", "GER40", - "--axis", "sma_signal.fast.length=3,5", "--axis", "sma_signal.slow.length=12,20", + "--axis", "fast.length=3,5", "--axis", "slow.length=12,20", "--stop-length", "14", "--stop-k", "2.0", "--block-len", "5", "--resamples", "1000", "--seed", seed, "--from", FROM_MS, "--to", TO_MS, @@ -6105,7 +6173,7 @@ fn walkforward_campaign_runs_an_arbitrary_blueprint() { let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura")) .args([ "walkforward", &fixture, "--real", "GER40", - "--axis", "r_breakout_signal.channel_length=10,20", + "--axis", "channel_length=10,20", "--stop-length", "3", "--stop-k", "2.0", "--from", FROM_MS, "--to", TO_MS, ]) @@ -6156,7 +6224,7 @@ fn mc_campaign_runs_an_arbitrary_blueprint() { let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura")) .args([ "mc", &fixture, "--real", "GER40", - "--axis", "r_breakout_signal.channel_length=10,20", + "--axis", "channel_length=10,20", "--stop-length", "3", "--stop-k", "2.0", "--block-len", "5", "--resamples", "100", "--seed", "7", "--from", FROM_MS, "--to", TO_MS, @@ -6205,8 +6273,8 @@ fn generalize_campaign_runs_an_arbitrary_blueprint() { let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura")) .args([ "generalize", &fixture, "--real", "GER40,USDJPY", - "--axis", "r_meanrev_signal.window=20", - "--axis", "r_meanrev_signal.band.factor=2.0", + "--axis", "window=20", + "--axis", "band.factor=2.0", "--stop-length", "3", "--stop-k", "2.0", "--from", GER40_SEPT2024_FROM_MS, "--to", GER40_SEPT2024_TO_MS, ]) @@ -6259,7 +6327,7 @@ fn mc_campaign_refuses_seeds_with_real() { let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura")) .args([ "mc", &fixture, "--real", "GER40", "--seeds", "3", - "--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12", + "--axis", "fast.length=3", "--axis", "slow.length=12", "--stop-length", "14", "--stop-k", "2.0", ]) .current_dir(&cwd) @@ -6270,25 +6338,32 @@ fn mc_campaign_refuses_seeds_with_real() { assert!(stderr.contains("Usage: aura mc"), "usage refusal: {stderr}"); } -/// #220: a raw-form axis name is refused before any data access on the migrated -/// verbs, exactly as sweep's shipped refusal — echoing what the user typed, with -/// no store litter (mirrors aura_sweep_real_blueprint_refuses_a_raw_form_axis_name_before_data_access). +/// #328 (supersedes the pre-#328 "raw form is refused" pin, inverted by the +/// reconciliation): a WRAPPED-form axis name is refused before any data +/// access on the migrated verbs, exactly as sweep's shipped refusal — +/// echoing what the user typed plus the translated raw candidate, with no +/// store litter (mirrors +/// aura_sweep_real_blueprint_refuses_a_wrapped_form_axis_name_before_data_access). #[test] -fn walkforward_campaign_refuses_a_raw_form_axis_name() { - let cwd = temp_cwd("walkforward-campaign-raw-axis"); +fn walkforward_campaign_refuses_a_wrapped_form_axis_name() { + let cwd = temp_cwd("walkforward-campaign-wrapped-axis"); let fixture = format!("{}/examples/r_sma.json", env!("CARGO_MANIFEST_DIR")); let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura")) .args([ "walkforward", &fixture, "--real", "GER40", - "--axis", "fast.length=3,5", "--axis", "sma_signal.slow.length=12,20", + "--axis", "sma_signal.fast.length=3,5", "--axis", "sma_signal.slow.length=12,20", "--stop-length", "14", "--stop-k", "2.0", ]) .current_dir(&cwd) .output() .expect("spawn aura"); - assert_eq!(out.status.code(), Some(2), "a raw-form axis name is refused before any data access"); + assert_eq!(out.status.code(), Some(2), "a wrapped-form axis name is refused before any data access"); let stderr = String::from_utf8_lossy(&out.stderr); - assert!(stderr.contains("axis \"fast.length\""), "echoes the raw name verbatim: {stderr}"); + assert!(stderr.contains("axis \"sma_signal.fast.length\""), "echoes the typed name verbatim: {stderr}"); + assert!( + stderr.contains("axis names are raw node.param paths") && stderr.contains("\"fast.length\""), + "translates to the raw candidate: {stderr}" + ); assert!(!cwd.join("runs").exists(), "no store litter on the refusal path"); } @@ -6407,7 +6482,7 @@ fn probe_window_resolves_the_pinned_span_before_the_252_swap() { let sweep = |symbol: &str, name: &str, from: Option<&str>, to: Option<&str>| { let mut args = vec![ "sweep", fixture.as_str(), "--real", symbol, - "--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12", + "--axis", "fast.length=3", "--axis", "slow.length=12", "--name", name, ]; if let Some(f) = from { @@ -6471,7 +6546,7 @@ fn probe_window_still_refuses_a_hostless_zero_bar_window() { let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura")) .args([ "sweep", &fixture, "--real", "SYMA", - "--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12", + "--axis", "fast.length=3", "--axis", "slow.length=12", "--name", "probe-252-zero-bars", "--from", "1709337600000", // 2024-03-02 00:00 UTC (Saturday) "--to", "1709424000000", // 2024-03-03 00:00 UTC (Sunday) @@ -6622,7 +6697,7 @@ fn campaign_show_round_trips_the_registered_document() { let run = std::process::Command::new(env!("CARGO_BIN_EXE_aura")) .args([ "walkforward", &fixture, "--real", "GER40", - "--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12", + "--axis", "fast.length=3", "--axis", "slow.length=12", "--select", "plateau:mean", "--from", FROM_MS, "--to", TO_MS, ]) @@ -6719,7 +6794,7 @@ fn process_show_round_trips_the_registered_document() { let run = std::process::Command::new(env!("CARGO_BIN_EXE_aura")) .args([ "walkforward", &fixture, "--real", "GER40", - "--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12", + "--axis", "fast.length=3", "--axis", "slow.length=12", "--select", "plateau:mean", "--from", FROM_MS, "--to", TO_MS, ]) diff --git a/crates/aura-cli/tests/graph_construct.rs b/crates/aura-cli/tests/graph_construct.rs index b11f0f9..66352d4 100644 --- a/crates/aura-cli/tests/graph_construct.rs +++ b/crates/aura-cli/tests/graph_construct.rs @@ -669,7 +669,10 @@ fn graph_params_lists_raw_axis_namespace() { 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 (#196, file-mode --content-id): a blueprint FILE's printed content @@ -719,7 +722,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 ` target may carry the display @@ -736,7 +742,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 ` with a well-shaped 64-hex id @@ -965,7 +974,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 +995,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 +1033,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 +1051,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 +1082,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 +1104,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 +1146,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 +1912,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)" ); } diff --git a/crates/aura-cli/tests/project_load.rs b/crates/aura-cli/tests/project_load.rs index a057893..6e98d16 100644 --- a/crates/aura-cli/tests/project_load.rs +++ b/crates/aura-cli/tests/project_load.rs @@ -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", ]) diff --git a/crates/aura-cli/tests/project_new.rs b/crates/aura-cli/tests/project_new.rs index a7f3da9..66f89e5 100644 --- a/crates/aura-cli/tests/project_new.rs +++ b/crates/aura-cli/tests/project_new.rs @@ -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)); diff --git a/crates/aura-cli/tests/project_sweep_campaign.rs b/crates/aura-cli/tests/project_sweep_campaign.rs index 5d55488..11a9b90 100644 --- a/crates/aura-cli/tests/project_sweep_campaign.rs +++ b/crates/aura-cli/tests/project_sweep_campaign.rs @@ -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 `..`. + // no data, so the discoverability half runs on every host. The name is RAW + // `.` (#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", ], @@ -213,8 +213,10 @@ fn project_node_open_param_sweeps_and_reproduces() { 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. + // its wrapped name (the real/campaign route's own `manifest.params` is + // unchanged by #328 — batch 1 is the synthetic sweep route's manifest + // only) — 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!( diff --git a/crates/aura-cli/tests/research_docs.rs b/crates/aura-cli/tests/research_docs.rs index ab1c2ef..fe6c06f 100644 --- a/crates/aura-cli/tests/research_docs.rs +++ b/crates/aura-cli/tests/research_docs.rs @@ -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, ], diff --git a/crates/aura-registry/src/lib.rs b/crates/aura-registry/src/lib.rs index a15b034..9245e82 100644 --- a/crates/aura-registry/src/lib.rs +++ b/crates/aura-registry/src/lib.rs @@ -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 `..` 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, + }, 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 diff --git a/crates/aura-runner/src/axes.rs b/crates/aura-runner/src/axes.rs index 6c76ec6..35df8f0 100644 --- a/crates/aura-runner/src/axes.rs +++ b/crates/aura-runner/src/axes.rs @@ -45,6 +45,57 @@ 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, +) -> 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 + } +} + /// 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 +190,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 = ["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 = ["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 = ["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 = 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) diff --git a/crates/aura-runner/src/family.rs b/crates/aura-runner/src/family.rs index adf3936..35d3c55 100644 --- a/crates/aura-runner/src/family.rs +++ b/crates/aura-runner/src/family.rs @@ -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}=` — see `aura sweep --list-axes`" - ), - BindError::KindMismatch { knob, expected, got } => format!( - "axis {knob}: expected {expected:?}, supplied {got:?} — \ - see `aura sweep --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}=` — see `aura sweep --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 --list-axes`" + ) + } BindError::UnknownKnob(msg) => msg.clone(), BindError::DuplicateBinding(name) => format!( "axis {name}: bound twice — each param takes exactly one axis — \ @@ -463,15 +477,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 = 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 +522,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) => { diff --git a/crates/aura-runner/src/member.rs b/crates/aura-runner/src/member.rs index cd7eee5..34cf124 100644 --- a/crates/aura-runner/src/member.rs +++ b/crates/aura-runner/src/member.rs @@ -706,55 +706,72 @@ pub fn wrapped_bound_names(signal: &Composite) -> HashSet { .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 -/// `.` 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 { - 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)], open_space: &[ParamSpec], signal: &Composite, ) -> Result, 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 --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 --list-axes`" + )); + } } } Ok(overrides) diff --git a/crates/aura-runner/src/reproduce.rs b/crates/aura-runner/src/reproduce.rs index b4b7fed..929b1f1 100644 --- a/crates/aura-runner/src/reproduce.rs +++ b/crates/aura-runner/src/reproduce.rs @@ -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,29 @@ 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` may be RAW (a `Sweep` family minted by + // `blueprint_sweep_family`, whose manifest now records the raw campaign + // namespace) or WRAPPED (every other route, unchanged this cycle) — + // `reproduce_family_in` reproduces either generically, so 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].`) 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