feat(aura-runner, aura-cli, aura-registry): one raw axis namespace end to end
Reconciles the sweep-axis namespace (closes #328): the raw form <node>.<param> (splice paths keep their interior path) is now the only user-facing axis name — printed by --list-axes, accepted by --axis on BOTH sweep routes, recorded in the synthetic run manifest, discovered by graph introspect --params (bound params included, default= lexicon), and validated by campaign documents. The wrapped <blueprint>.<...> form is retired with translation refusals on both seams. Mechanics: - classify_axis_intake (aura-runner axes.rs): explicit raw-first acceptance predicate shared by both --axis intake routes. Deliberately not built on raw_matches_wrapped, whose equality branch would have accepted the retired form silently (skeptic finding, minuted on #328). Wrapped hit -> translation refusal naming the raw candidate (shared prose builder wrapped_axis_refusal, one literal for both routes). - The synthetic route resolution moves to the raw frame the real route (bind_axes) already uses: override_paths/wrapped_bound_overrides_of match via raw_matches_wrapped (also removing a latent slicing panic), blueprint_sweep_family translates raw names onto wrapped SweepBinder slots, manifest.params records raw keys (name-only reshaping, zip is positional). The two sweep routes now share one convention. - reproduce translates recorded manifest names raw-or-wrapped onto the wrapped space: old registered artifacts stay replayable (C29), new raw manifests round-trip. - introspect --params gains the bound default= lines via the same render_value the --list-axes bound pass uses - the two discovery surfaces are byte-identical by construction. - AxisNotInParamSpace carries raw_candidate; ref_fault_prose renders the did-you-mean iff present. Sweep-terminal bind errors (MissingKnob/KindMismatch) render the raw knob name (render seam only; acceptance criterion: no user-facing surface prints a wrapped axis name). - Downstream consequence fixes: scaffold quickstart example spoke wrapped; ~90 wrapped test literals converted to raw across six test files (deliberately-bogus refusal names and the untouched internal walkforward wrapped route kept, with comments). Fork decisions and the skeptic correction are minuted on #328 (comments 2026-07-24/25). Grounding-check PASS first attempt (12 assumptions ratified). Review (opus): translation ambiguity cleared (uniform single prefix per blueprint), reproduce both-shapes traced; repair pass added the synthetic-route refusal e2e + the shared prose builder. Verified: cargo test --workspace green (99 targets, 0 failures), clippy -D warnings clean. refs #246, refs #319, refs #331
This commit is contained in:
@@ -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<String, String> {
|
||||
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<String,
|
||||
for p in composite.param_space() {
|
||||
let _ = writeln!(out, "{}:{:?}", p.name, p.kind);
|
||||
}
|
||||
for b in composite.bound_param_space() {
|
||||
let _ = writeln!(out, "{}:{:?} default={}", b.name, b.kind, render_value(&b.value));
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
|
||||
+109
-47
@@ -45,7 +45,7 @@ use aura_measurement::information_coefficient;
|
||||
// `blueprint_axis_probe_reopened` is production code only from `verb_sugar.rs`
|
||||
// (a sibling module reaching it via `crate::blueprint_axis_probe_reopened`,
|
||||
// the crate-root re-export this `use` gives it), not from this module itself.
|
||||
use aura_runner::member::{blueprint_axis_probe, blueprint_axis_probe_reopened, run_signal_r, wrapped_bound_names, RunData};
|
||||
use aura_runner::member::{blueprint_axis_probe, blueprint_axis_probe_reopened, run_signal_r, RunData};
|
||||
use aura_runner::{TapPlan, TapSubscription};
|
||||
#[cfg(test)]
|
||||
use aura_runner::member::{run_blueprint_member, wrap_r, SYNTHETIC_PIP_SIZE};
|
||||
@@ -813,17 +813,67 @@ impl IcReport {
|
||||
/// fully closed) — not only the fully-closed case: `--axis`/`override_paths`
|
||||
/// accepts a bound name regardless of how many knobs happen to be open
|
||||
/// alongside it, so the discovery surface must list it regardless too. Names
|
||||
/// are exactly what `--axis` binds.
|
||||
/// are RAW `<node>.<param>` paths (#328: the wrapped `<blueprint>.<node>.<param>`
|
||||
/// form is retired from the surface) — exactly what `--axis` binds.
|
||||
fn list_blueprint_axes(doc: &str, env: &aura_runner::project::Env) {
|
||||
let space = blueprint_axis_probe(doc, env).param_space();
|
||||
for p in &space {
|
||||
println!("{}:{:?}", p.name, p.kind); // ScalarKind Debug -> I64/F64/Bool/Timestamp
|
||||
// Strip the wrapper's one leading node segment (#328): open names print
|
||||
// RAW, matching `--axis`'s own accepted namespace.
|
||||
println!("{}:{:?}", aura_runner::axes::wrapped_to_raw_axis(&p.name), p.kind); // ScalarKind Debug -> I64/F64/Bool/Timestamp
|
||||
}
|
||||
let signal = blueprint_from_json(doc, &|t| env.resolve(t))
|
||||
.expect("doc parse-validated at the dispatch boundary");
|
||||
let bp = signal.name().to_string();
|
||||
for b in signal.bound_param_space() {
|
||||
println!("{bp}.{}:{:?} default={}", b.name, b.kind, render_value(&b.value));
|
||||
// `bound_param_space()`'s `.name` is already RAW (#203) — no blueprint-name
|
||||
// concatenation (#328: the blueprint name stays out of axis paths, C23).
|
||||
println!("{}:{:?} default={}", b.name, b.kind, render_value(&b.value));
|
||||
}
|
||||
}
|
||||
|
||||
/// The one-sentence prose for a refused WRAPPED `--axis` name (#328), shared
|
||||
/// by both intake routes so the wording carried by `refuse_wrapped_synthetic_axes`
|
||||
/// (the synthetic-sweep preflight) and `validate_and_register_axes` (the
|
||||
/// real-route/campaign preflight) cannot drift into two copies. Returns the
|
||||
/// message WITHOUT the leading `aura: ` — each caller prepends that itself
|
||||
/// (the synthetic route via its own `eprintln!`, the real route via
|
||||
/// `exit_axis_register_error`'s shared `eprintln!("aura: {m}")`), so the
|
||||
/// rendered bytes on both routes stay byte-identical.
|
||||
fn wrapped_axis_refusal(n: &str, raw: &str) -> String {
|
||||
format!(
|
||||
"axis \"{n}\": axis names are raw node.param paths — \
|
||||
use \"{raw}\" (the wrapped --list-axes form was retired, #328)"
|
||||
)
|
||||
}
|
||||
|
||||
/// `--axis` intake preflight for the SYNTHETIC (no `--real`) blueprint-sweep
|
||||
/// route (#328): this route bypasses `validate_and_register_axes` entirely (no
|
||||
/// project/registry touched), so it needs its own copy of the same WRAPPED-name
|
||||
/// refusal — the shared [`aura_runner::axes::classify_axis_intake`] predicate,
|
||||
/// intercepting only a `WrappedRetired` hit (echoing the translation pointer,
|
||||
/// exit 2, before the sweep ever runs). A name matching neither namespace is
|
||||
/// deliberately left unrefused HERE: `blueprint_sweep_family`'s own
|
||||
/// `override_paths` rejects it downstream with today's unchanged prose, so this
|
||||
/// preflight does not duplicate that message under a second wording.
|
||||
fn refuse_wrapped_synthetic_axes(
|
||||
doc: &str,
|
||||
env: &aura_runner::project::Env,
|
||||
axes: &[(String, Vec<Scalar>)],
|
||||
) {
|
||||
let wrapped_open = blueprint_axis_probe(doc, env).param_space();
|
||||
let raw_bound: HashSet<String> = blueprint_from_json(doc, &|t| env.resolve(t))
|
||||
.expect("doc parse-validated at the dispatch boundary")
|
||||
.bound_param_space()
|
||||
.into_iter()
|
||||
.map(|b| b.name)
|
||||
.collect();
|
||||
for (n, _) in axes {
|
||||
if let aura_runner::axes::AxisIntake::WrappedRetired(raw) =
|
||||
aura_runner::axes::classify_axis_intake(n, &wrapped_open, &raw_bound)
|
||||
{
|
||||
eprintln!("aura: {}", wrapped_axis_refusal(n, &raw));
|
||||
std::process::exit(2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1661,14 +1711,17 @@ enum AxisRegisterError {
|
||||
NoProject(String),
|
||||
}
|
||||
|
||||
/// Validate every `--axis` name against the blueprint's WRAPPED probe namespace
|
||||
/// (`blueprint_axis_probe`/`--list-axes`) — an axis naming a BOUND param (#246:
|
||||
/// re-openable, same as every already-open knob) passes exactly like an open
|
||||
/// one; only a name matching NEITHER space is refused — then canonicalize +
|
||||
/// register the blueprint by topology hash and strip every axis name to the RAW
|
||||
/// campaign namespace (the sweep sequence, #210 c0110). A raw-form or
|
||||
/// fat-fingered axis name is refused here, echoing exactly what the user typed,
|
||||
/// before the archive is touched or the blueprint is registered — shared by
|
||||
/// Validate every `--axis` name against the RAW campaign-axis namespace only
|
||||
/// (#328: `--list-axes`'s own namespace, open-or-bound) — a WRAPPED name (the
|
||||
/// retired `<blueprint>.<node>.<param>` form) is refused with a translation
|
||||
/// pointer to its raw candidate; a name in neither namespace gets today's
|
||||
/// unmatched-axis prose, unchanged. [`aura_runner::axes::classify_axis_intake`]
|
||||
/// is the shared predicate (deliberately not `raw_matches_wrapped`, whose own
|
||||
/// equality branch would silently accept a wrapped name — see its doc
|
||||
/// comment). Then canonicalize + register the blueprint by topology hash — no
|
||||
/// further strip: the axes are already raw by the time this returns them (the
|
||||
/// old wrapped->raw strip is gone, since a splice path's own dots, e.g.
|
||||
/// `anchor.sess.period_minutes`, must not be mangled a second time). Shared by
|
||||
/// every campaign-path dispatcher (sweep/generalize/walkforward/mc all landed
|
||||
/// the identical block; #220 slice-1 deferred this dedup to "once wf/mc land
|
||||
/// the same block", rule-of-three now exceeded 4x). The override set itself is
|
||||
@@ -1682,17 +1735,24 @@ fn validate_and_register_axes(
|
||||
axes: &[(String, Vec<Scalar>)],
|
||||
env: &aura_runner::project::Env,
|
||||
) -> Result<(String, Vec<(String, Vec<Scalar>)>), AxisRegisterError> {
|
||||
let space = blueprint_axis_probe(doc, env).param_space();
|
||||
let wrapped_open = blueprint_axis_probe(doc, env).param_space();
|
||||
let blueprint = blueprint_from_json(doc, &|t| env.resolve(t))
|
||||
.expect("doc parse-validated at the dispatch boundary");
|
||||
let bound = wrapped_bound_names(&blueprint);
|
||||
let raw_bound: HashSet<String> =
|
||||
blueprint.bound_param_space().into_iter().map(|b| b.name).collect();
|
||||
for (n, _) in axes {
|
||||
if !space.iter().any(|p| &p.name == n) && !bound.contains(n) {
|
||||
return Err(AxisRegisterError::UnknownAxis(format!(
|
||||
"axis \"{n}\" is not one of this blueprint's \
|
||||
sweepable axes — run 'aura sweep <bp> --list-axes' \
|
||||
to see them"
|
||||
)));
|
||||
match aura_runner::axes::classify_axis_intake(n, &wrapped_open, &raw_bound) {
|
||||
aura_runner::axes::AxisIntake::Raw => {}
|
||||
aura_runner::axes::AxisIntake::WrappedRetired(raw) => {
|
||||
return Err(AxisRegisterError::UnknownAxis(wrapped_axis_refusal(n, &raw)));
|
||||
}
|
||||
aura_runner::axes::AxisIntake::Unknown => {
|
||||
return Err(AxisRegisterError::UnknownAxis(format!(
|
||||
"axis \"{n}\" is not one of this blueprint's \
|
||||
sweepable axes — run 'aura sweep <bp> --list-axes' \
|
||||
to see them"
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
if env.provenance().is_none() {
|
||||
@@ -1709,11 +1769,7 @@ fn validate_and_register_axes(
|
||||
let topo = topology_hash(&blueprint);
|
||||
reg.put_blueprint(&topo, &canonical)
|
||||
.map_err(|e| AxisRegisterError::Registry(e.to_string()))?;
|
||||
let raw_axes: Vec<(String, Vec<Scalar>)> = axes
|
||||
.iter()
|
||||
.map(|(n, v)| (aura_runner::axes::wrapped_to_raw_axis(n).to_string(), v.clone()))
|
||||
.collect();
|
||||
Ok((canonical, raw_axes))
|
||||
Ok((canonical, axes.to_vec()))
|
||||
}
|
||||
|
||||
/// Exit-map for a `validate_and_register_axes` failure — the stderr line and
|
||||
@@ -2326,8 +2382,8 @@ fn dispatch_sweep(a: SweepCmd, env: &aura_runner::project::Env) {
|
||||
// strategy ref resolves against this one write; a second
|
||||
// put under `content_id_of(&canonical)` would target the
|
||||
// identical key and is not needed.
|
||||
// Axis validation + canonicalize/register + the wrapped->raw
|
||||
// strip are single-sourced in `validate_and_register_axes`
|
||||
// Axis validation (RAW-only, #328) + canonicalize/register are
|
||||
// single-sourced in `validate_and_register_axes`
|
||||
// (data-free, so this fires before the archive is touched).
|
||||
let (canonical, raw_axes) = validate_and_register_axes("sweep", &doc, &axes, env)
|
||||
.unwrap_or_else(|e| exit_axis_register_error(e));
|
||||
@@ -2361,6 +2417,10 @@ fn dispatch_sweep(a: SweepCmd, env: &aura_runner::project::Env) {
|
||||
);
|
||||
std::process::exit(2);
|
||||
}
|
||||
// #328: this route bypasses `validate_and_register_axes` (no
|
||||
// project/registry involved), so it needs its own WRAPPED-name
|
||||
// refusal ahead of the sweep.
|
||||
refuse_wrapped_synthetic_axes(&doc, env, &axes);
|
||||
run_blueprint_sweep(
|
||||
&doc, &axes, &name, persist,
|
||||
DataSource::from_choice(data, env), env,
|
||||
@@ -2418,8 +2478,8 @@ fn dispatch_walkforward(a: WalkforwardCmd, env: &aura_runner::project::Env) {
|
||||
eprintln!("aura: {m}");
|
||||
std::process::exit(2);
|
||||
});
|
||||
// Axis validation + canonicalize/register + the wrapped->raw
|
||||
// strip are single-sourced in `validate_and_register_axes`
|
||||
// Axis validation (RAW-only, #328) + canonicalize/register are
|
||||
// single-sourced in `validate_and_register_axes`
|
||||
// (data-free, so this fires before the archive is touched).
|
||||
let (canonical, raw_axes) = validate_and_register_axes("walkforward", &doc, &axes, env)
|
||||
.unwrap_or_else(|e| exit_axis_register_error(e));
|
||||
@@ -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!(
|
||||
|
||||
@@ -462,8 +462,14 @@ pub(crate) fn ref_fault_prose(f: &RefFault) -> String {
|
||||
RefFault::StrategyUnloadable { id, error } => {
|
||||
format!("strategy {id} cannot be loaded: {error}")
|
||||
}
|
||||
RefFault::AxisNotInParamSpace { strategy, axis } => {
|
||||
format!("strategy {strategy}: axis \"{axis}\" is not in the param space")
|
||||
RefFault::AxisNotInParamSpace { strategy, axis, raw_candidate } => {
|
||||
let mut msg = format!("strategy {strategy}: axis \"{axis}\" is not in the param space");
|
||||
if let Some(candidate) = raw_candidate {
|
||||
msg.push_str(&format!(
|
||||
"; axis names are raw node.param paths — did you mean \"{candidate}\"?"
|
||||
));
|
||||
}
|
||||
msg
|
||||
}
|
||||
RefFault::AxisKindMismatch { strategy, axis } => {
|
||||
format!("strategy {strategy}: axis \"{axis}\" declares a kind that is not the param's kind")
|
||||
@@ -761,6 +767,7 @@ mod tests {
|
||||
ref_fault_prose(&RefFault::AxisNotInParamSpace {
|
||||
strategy: "9f3a".into(),
|
||||
axis: "nope".into(),
|
||||
raw_candidate: None,
|
||||
}),
|
||||
ref_fault_prose(&RefFault::AxisKindMismatch {
|
||||
strategy: "9f3a".into(),
|
||||
@@ -781,6 +788,39 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// #328: `ref_fault_prose` appends the did-you-mean clause, contiguous
|
||||
/// and verbatim, exactly when `raw_candidate` is present — the document-
|
||||
/// side refusal seam's mirror of the sweep `--axis` intake translation
|
||||
/// (`aura-runner`'s `axes.rs` classify predicate), sharing the same
|
||||
/// `axis names are raw node.param paths` clause pinned there.
|
||||
#[test]
|
||||
fn ref_fault_prose_renders_the_did_you_mean_when_a_raw_candidate_is_present() {
|
||||
let msg = ref_fault_prose(&RefFault::AxisNotInParamSpace {
|
||||
strategy: "9f3a".into(),
|
||||
axis: "graph.fast.length".into(),
|
||||
raw_candidate: Some("fast.length".into()),
|
||||
});
|
||||
assert_eq!(
|
||||
msg,
|
||||
"strategy 9f3a: axis \"graph.fast.length\" is not in the param space; \
|
||||
axis names are raw node.param paths — did you mean \"fast.length\"?"
|
||||
);
|
||||
}
|
||||
|
||||
/// #328 (negative): a rejected axis with no `raw_candidate` (stripping one
|
||||
/// leading segment yields no param-space hit, or the axis has no leading
|
||||
/// segment to strip at all) renders today's prose unchanged — no
|
||||
/// speculative suggestion.
|
||||
#[test]
|
||||
fn ref_fault_prose_omits_the_did_you_mean_when_no_raw_candidate() {
|
||||
let msg = ref_fault_prose(&RefFault::AxisNotInParamSpace {
|
||||
strategy: "9f3a".into(),
|
||||
axis: "nope".into(),
|
||||
raw_candidate: None,
|
||||
});
|
||||
assert_eq!(msg, "strategy 9f3a: axis \"nope\" is not in the param space");
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// #194 (the prefix trap): a doc ref that carries the display `content:`
|
||||
/// prefix (a copy-paste from register/introspect output) is bare-only in
|
||||
|
||||
@@ -234,8 +234,9 @@ std vocabulary, anchored by `Aura.toml`. There is no crate and no build step.
|
||||
- Run: `aura run blueprints/signal.json` (the starter is closed — all
|
||||
params bound; bound values are defaults — any `--axis` may override them
|
||||
(#246))
|
||||
- Sweep: `aura sweep blueprints/signal.json --axis __NAME_SNAKE___signal.fast.length=2,4,8`
|
||||
(`aura sweep <bp> --list-axes` lists the open + bound-overridable axes)
|
||||
- Sweep: `aura sweep blueprints/signal.json --axis fast.length=2,4,8`
|
||||
(`aura sweep <bp> --list-axes` lists the open + bound-overridable axes, RAW
|
||||
`<node>.<param>` names — #328)
|
||||
- Native nodes: when the project needs its first project-specific node,
|
||||
`aura nodes new <name>` scaffolds a node crate beside this project and
|
||||
attaches it via `[nodes]` in `Aura.toml` (build it with `cargo build`).
|
||||
@@ -459,10 +460,11 @@ mod tests {
|
||||
let claude = render_project(CLAUDE_MD_PROJECT, "demo-lab");
|
||||
assert!(claude.contains("demo-lab"));
|
||||
// The CLAUDE.md sweep quickstart targets the closed starter itself
|
||||
// with the rendered blueprint-name axis prefix (#246: a bound param
|
||||
// is a default an axis overrides).
|
||||
// with the RAW axis name (#246: a bound param is a default an axis
|
||||
// overrides; #328: the axis namespace is raw, no blueprint-name
|
||||
// prefix).
|
||||
assert!(claude.contains("blueprints/signal.json"));
|
||||
assert!(claude.contains("demo_lab_signal.fast.length"));
|
||||
assert!(claude.contains("--axis fast.length"));
|
||||
}
|
||||
|
||||
/// #315: the scaffolded project CLAUDE.md teaches the execution semantics
|
||||
|
||||
+250
-175
File diff suppressed because it is too large
Load Diff
@@ -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 <ID>` 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 <ID>` 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)"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -117,9 +117,9 @@ fn project_registry_anchors_at_discovered_root_not_invocation_cwd() {
|
||||
"sweep",
|
||||
&closed_bp,
|
||||
"--axis",
|
||||
"sma_signal.fast.length=2,4",
|
||||
"fast.length=2,4",
|
||||
"--axis",
|
||||
"sma_signal.slow.length=8,16",
|
||||
"slow.length=8,16",
|
||||
"--name",
|
||||
"proj-anchor",
|
||||
])
|
||||
|
||||
@@ -135,7 +135,7 @@ fn data_only_project_sweeps_without_any_build() {
|
||||
assert!(new.status.success(), "{}", String::from_utf8_lossy(&new.stderr));
|
||||
let proj = base.join("scratch");
|
||||
let out = aura(
|
||||
&["sweep", "blueprints/signal.json", "--axis", "scratch_signal.fast.length=2,4"],
|
||||
&["sweep", "blueprints/signal.json", "--axis", "fast.length=2,4"],
|
||||
&proj,
|
||||
);
|
||||
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
|
||||
@@ -157,7 +157,7 @@ fn data_only_project_sweep_over_the_starter_opens_one_member_per_axis_value() {
|
||||
assert!(new.status.success(), "{}", String::from_utf8_lossy(&new.stderr));
|
||||
let proj = base.join("scratch");
|
||||
let out = aura(
|
||||
&["sweep", "blueprints/signal.json", "--axis", "scratch_signal.fast.length=2,4,8"],
|
||||
&["sweep", "blueprints/signal.json", "--axis", "fast.length=2,4,8"],
|
||||
&proj,
|
||||
);
|
||||
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
|
||||
|
||||
@@ -160,8 +160,8 @@ fn project_node_open_param_sweeps_and_reproduces() {
|
||||
// the fixture's three BOUND params as `default=`-lines (#246: every bound
|
||||
// param is an equally re-openable `--axis`, listed regardless of how many
|
||||
// knobs happen to be open alongside it). NOT gated: enumerating axes needs
|
||||
// no data, so the discoverability half runs on every host. The wrapped
|
||||
// name is `<blueprint>.<node>.<param>`.
|
||||
// no data, so the discoverability half runs on every host. The name is RAW
|
||||
// `<node>.<param>` (#328: the blueprint name stays out of axis paths).
|
||||
let axes = aura_in(dir, &["sweep", "blueprints/scaled_open.json", "--list-axes"]);
|
||||
assert!(
|
||||
axes.status.success(),
|
||||
@@ -170,10 +170,10 @@ fn project_node_open_param_sweeps_and_reproduces() {
|
||||
);
|
||||
assert_eq!(
|
||||
String::from_utf8_lossy(&axes.stdout),
|
||||
"scaled_signal.gain.factor:F64\n\
|
||||
scaled_signal.fast.length:I64 default=2\n\
|
||||
scaled_signal.slow.length:I64 default=4\n\
|
||||
scaled_signal.bias.scale:F64 default=0.5\n",
|
||||
"gain.factor:F64\n\
|
||||
fast.length:I64 default=2\n\
|
||||
slow.length:I64 default=4\n\
|
||||
bias.scale:F64 default=0.5\n",
|
||||
"the project node's own OPEN param is the one open axis, alongside the \
|
||||
fixture's bound defaults; stderr: {}",
|
||||
String::from_utf8_lossy(&axes.stderr)
|
||||
@@ -198,7 +198,7 @@ fn project_node_open_param_sweeps_and_reproduces() {
|
||||
"--to",
|
||||
GER40_TO_MS,
|
||||
"--axis",
|
||||
"scaled_signal.gain.factor=0.5,1.0",
|
||||
"gain.factor=0.5,1.0",
|
||||
"--name",
|
||||
"knob",
|
||||
],
|
||||
@@ -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!(
|
||||
|
||||
@@ -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,
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user