feat(cli): swap the scaffold's starter node for a param-bearing Scale (#183)
The `aura new` scaffold shipped one node, a param-less Identity, so a downstream author had no public example of a build closure consuming a bound param — the highest-value gap the project-environment fieldtest found (F2): a newcomer could copy a param-less node but had to read engine source to write their first *tunable* one, which the consumer contract forbids. Swap that single starter node for a Scale node (mirroring the std gain operator): its schema declares a `factor` ParamSpec and its build closure reads the bound value (`|p| Box::new(Scale::new(p[0].f64()))`), and the starter blueprint binds `factor` so `aura run` exercises the bound-param path with zero hand-editing. factor is bound to 1.0 — a neutral identity default the newcomer then tunes, not the only C7-safe value (only |factor|>1 would push the terminal `bias` output past the bounded [-1,+1] contract; the knob is genuinely read and multiplied each cycle). The param-reading closure is the copyable template content #183 delivers; a hypothetical `|_|` regression is caught by the scaffold unit test's source guard, and tests/project_new.rs re-validates the swap by building and running the scaffold end to end. Template-side only (#181): the frozen 0102 demo-project fixture is untouched. closes #183
This commit is contained in:
@@ -115,22 +115,24 @@ const LIB_RS: &str = r#"//! __NAME__ — an aura research project: node logic +
|
||||
//! this cdylib per invocation and merges it with the std vocabulary.
|
||||
|
||||
use aura_core::{
|
||||
Cell, Ctx, FieldSpec, Firing, Node, NodeSchema, PortSpec, PrimitiveBuilder,
|
||||
Cell, Ctx, FieldSpec, Firing, Node, NodeSchema, ParamSpec, PortSpec, PrimitiveBuilder,
|
||||
ScalarKind,
|
||||
};
|
||||
|
||||
/// One-input f64 pass-through. Emits `None` until its input has a value.
|
||||
pub struct Identity {
|
||||
/// One-input scalar gain: emits `input * factor`. Emits `None` until its
|
||||
/// input has a value (warm-up filter, C8).
|
||||
pub struct Scale {
|
||||
factor: f64,
|
||||
out: [Cell; 1],
|
||||
}
|
||||
|
||||
impl Identity {
|
||||
pub fn new() -> Self {
|
||||
Self { out: [Cell::from_f64(0.0)] }
|
||||
impl Scale {
|
||||
pub fn new(factor: f64) -> Self {
|
||||
Self { factor, out: [Cell::from_f64(0.0)] }
|
||||
}
|
||||
pub fn builder() -> PrimitiveBuilder {
|
||||
PrimitiveBuilder::new(
|
||||
"__NS__::Identity",
|
||||
"__NS__::Scale",
|
||||
NodeSchema {
|
||||
inputs: vec![PortSpec {
|
||||
kind: ScalarKind::F64,
|
||||
@@ -138,20 +140,14 @@ impl Identity {
|
||||
name: "value".into(),
|
||||
}],
|
||||
output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }],
|
||||
params: vec![],
|
||||
params: vec![ParamSpec { name: "factor".into(), kind: ScalarKind::F64 }],
|
||||
},
|
||||
|_| Box::new(Identity::new()),
|
||||
|p| Box::new(Scale::new(p[0].f64())),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Identity {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Node for Identity {
|
||||
impl Node for Scale {
|
||||
fn lookbacks(&self) -> Vec<usize> {
|
||||
vec![1]
|
||||
}
|
||||
@@ -160,23 +156,23 @@ impl Node for Identity {
|
||||
if w.is_empty() {
|
||||
return None;
|
||||
}
|
||||
self.out[0] = Cell::from_f64(w[0]);
|
||||
self.out[0] = Cell::from_f64(w[0] * self.factor);
|
||||
Some(&self.out)
|
||||
}
|
||||
fn label(&self) -> String {
|
||||
"__NS__::Identity".to_string()
|
||||
format!("__NS__::Scale({})", self.factor)
|
||||
}
|
||||
}
|
||||
|
||||
fn vocabulary(type_id: &str) -> Option<PrimitiveBuilder> {
|
||||
match type_id {
|
||||
"__NS__::Identity" => Some(Identity::builder()),
|
||||
"__NS__::Scale" => Some(Scale::builder()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn type_ids() -> &'static [&'static str] {
|
||||
&["__NS__::Identity"]
|
||||
&["__NS__::Scale"]
|
||||
}
|
||||
|
||||
aura_core::aura_project! {
|
||||
@@ -186,7 +182,7 @@ aura_core::aura_project! {
|
||||
}
|
||||
"#;
|
||||
|
||||
const SIGNAL_JSON: &str = r#"{"format_version":1,"blueprint":{"name":"__NAME_SNAKE___signal","nodes":[{"primitive":{"type":"SMA","name":"fast","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":2}}]}},{"primitive":{"type":"SMA","name":"slow","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":4}}]}},{"primitive":{"type":"Sub"}},{"primitive":{"type":"Bias","name":"bias","bound":[{"pos":0,"name":"scale","kind":"F64","value":{"F64":0.5}}]}},{"primitive":{"type":"__NS__::Identity"}}],"edges":[{"from":0,"to":2,"slot":0,"from_field":0},{"from":1,"to":2,"slot":1,"from_field":0},{"from":2,"to":3,"slot":0,"from_field":0},{"from":3,"to":4,"slot":0,"from_field":0}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0},{"node":1,"slot":0}],"source":"F64"}],"output":[{"node":4,"field":0,"name":"bias"}]}}"#;
|
||||
const SIGNAL_JSON: &str = r#"{"format_version":1,"blueprint":{"name":"__NAME_SNAKE___signal","nodes":[{"primitive":{"type":"SMA","name":"fast","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":2}}]}},{"primitive":{"type":"SMA","name":"slow","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":4}}]}},{"primitive":{"type":"Sub"}},{"primitive":{"type":"Bias","name":"bias","bound":[{"pos":0,"name":"scale","kind":"F64","value":{"F64":0.5}}]}},{"primitive":{"type":"__NS__::Scale","bound":[{"pos":0,"name":"factor","kind":"F64","value":{"F64":1.0}}]}}],"edges":[{"from":0,"to":2,"slot":0,"from_field":0},{"from":1,"to":2,"slot":1,"from_field":0},{"from":2,"to":3,"slot":0,"from_field":0},{"from":3,"to":4,"slot":0,"from_field":0}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0},{"node":1,"slot":0}],"source":"F64"}],"output":[{"node":4,"field":0,"name":"bias"}]}}"#;
|
||||
|
||||
const CLAUDE_MD: &str = r#"# __NAME__ — an aura research project
|
||||
|
||||
@@ -311,10 +307,10 @@ mod tests {
|
||||
assert!(!out.contains("__"), "unsubstituted token in: {out}");
|
||||
}
|
||||
let lib = render(LIB_RS, &spec);
|
||||
assert!(lib.contains("\"demo_lab::Identity\""));
|
||||
assert!(lib.contains("\"demo_lab::Scale\""));
|
||||
assert!(lib.contains("namespace: \"demo_lab\""));
|
||||
let json = render(SIGNAL_JSON, &spec);
|
||||
assert!(json.contains("\"type\":\"demo_lab::Identity\""));
|
||||
assert!(json.contains("\"type\":\"demo_lab::Scale\""));
|
||||
assert!(json.contains("\"name\":\"demo_lab_signal\""));
|
||||
let cargo = render(CARGO_TOML, &spec);
|
||||
assert!(cargo.contains("name = \"demo-lab\""));
|
||||
|
||||
@@ -64,7 +64,7 @@ fn new_scaffold_builds_and_runs_the_authoring_loop() {
|
||||
let introspect = aura(&["graph", "introspect", "--vocabulary"], &proj);
|
||||
assert!(introspect.status.success());
|
||||
let text = String::from_utf8_lossy(&introspect.stdout);
|
||||
assert!(text.contains("demo_lab::Identity"), "project id listed");
|
||||
assert!(text.contains("demo_lab::Scale"), "project id listed");
|
||||
let _ = std::fs::remove_dir_all(&base);
|
||||
}
|
||||
|
||||
@@ -136,8 +136,8 @@ fn new_namespace_override_reaches_the_built_vocabulary() {
|
||||
let introspect = aura(&["graph", "introspect", "--vocabulary"], &proj);
|
||||
assert!(introspect.status.success());
|
||||
let text = String::from_utf8_lossy(&introspect.stdout);
|
||||
assert!(text.contains("custom_ns::Identity"), "overridden namespace listed: {text}");
|
||||
assert!(!text.contains("demo_lab::Identity"), "default-derived namespace must not leak");
|
||||
assert!(text.contains("custom_ns::Scale"), "overridden namespace listed: {text}");
|
||||
assert!(!text.contains("demo_lab::Scale"), "default-derived namespace must not leak");
|
||||
let _ = std::fs::remove_dir_all(&base);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user