feat(cli): the scaffold ships one starter blueprint

Task 6 of the bound-override cycle: SIGNAL_OPEN_JSON_STD and its write
retire — aura new scaffolds signal.json alone, and the project CLAUDE.md
quickstart advertises run AND sweep against that one file (the sweep line
overrides the bound fast.length; --list-axes shows open + bound axes).
The data-only e2e loop proves it end to end: scaffold, run the closed
starter, sweep it via an override axis, and assert signal.json is the
only starter blueprint. The authoring-guide's starter-pair sentence is
retargeted; the last signal_open reference outside the unrelated #196
fixture string is gone.

refs #246
This commit is contained in:
2026-07-13 04:11:04 +02:00
parent e902a0f31d
commit 024d878ac3
3 changed files with 64 additions and 40 deletions
+15 -26
View File
@@ -221,21 +221,16 @@ const GITIGNORE_PROJECT: &str = "/runs\n";
const SIGNAL_JSON_STD: &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}}]}}],"edges":[{"from":0,"to":2,"slot":0,"from_field":0},{"from":1,"to":2,"slot":1,"from_field":0},{"from":2,"to":3,"slot":0,"from_field":0}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0},{"node":1,"slot":0}],"source":"F64"}],"output":[{"node":3,"field":0,"name":"bias"}]}}"#;
/// The open sibling of `SIGNAL_JSON_STD`: `fast.length` is a free knob, so
/// the scaffolded sweep quickstart works out of the box (`run` requires a
/// closed blueprint; `sweep` requires open knobs — one file cannot serve
/// both, #241).
const SIGNAL_OPEN_JSON_STD: &str = r#"{"format_version":1,"blueprint":{"name":"__NAME_SNAKE___signal_open","nodes":[{"primitive":{"type":"SMA","name":"fast"}},{"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}}]}}],"edges":[{"from":0,"to":2,"slot":0,"from_field":0},{"from":1,"to":2,"slot":1,"from_field":0},{"from":2,"to":3,"slot":0,"from_field":0}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0},{"node":1,"slot":0}],"source":"F64"}],"output":[{"node":3,"field":0,"name":"bias"}]}}"#;
const CLAUDE_MD_PROJECT: &str = r#"# __NAME__ — an aura research project (data-only)
This directory is an aura project: blueprints + research documents over the
std vocabulary, anchored by `Aura.toml`. There is no crate and no build step.
- Run: `aura run blueprints/signal.json` (a *closed* blueprint — every
param bound)
- Sweep: `aura sweep blueprints/signal_open.json --axis __NAME_SNAKE___signal_open.fast.length=2,4,8`
(a sweep varies *open* knobs; `aura sweep <bp> --list-axes` lists them)
- 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)
- 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`).
@@ -277,7 +272,6 @@ pub fn scaffold_project(spec: &ProjectScaffoldSpec) -> Result<(), String> {
write("Aura.toml", AURA_TOML.to_string())?;
write(".gitignore", GITIGNORE_PROJECT.to_string())?;
write("blueprints/signal.json", render_project(SIGNAL_JSON_STD, &spec.name))?;
write("blueprints/signal_open.json", render_project(SIGNAL_OPEN_JSON_STD, &spec.name))?;
write("CLAUDE.md", render_project(CLAUDE_MD_PROJECT, &spec.name))?;
if inside_git_work_tree(&spec.target_dir) {
return Ok(());
@@ -445,16 +439,17 @@ mod tests {
assert!(cargo.contains("/crates/aura-core"));
// Project-template set (data-only tier, via `render_project`).
for t in [GITIGNORE_PROJECT, SIGNAL_JSON_STD, SIGNAL_OPEN_JSON_STD, CLAUDE_MD_PROJECT] {
for t in [GITIGNORE_PROJECT, SIGNAL_JSON_STD, CLAUDE_MD_PROJECT] {
let out = render_project(t, "demo-lab");
assert!(!out.contains("__"), "unsubstituted token in: {out}");
}
let claude = render_project(CLAUDE_MD_PROJECT, "demo-lab");
assert!(claude.contains("demo-lab"));
// The CLAUDE.md sweep quickstart targets the open sibling with the
// rendered blueprint-name axis prefix (a bound starter cannot sweep).
assert!(claude.contains("blueprints/signal_open.json"));
assert!(claude.contains("demo_lab_signal_open.fast.length"));
// 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).
assert!(claude.contains("blueprints/signal.json"));
assert!(claude.contains("demo_lab_signal.fast.length"));
}
/// Property (#183, fieldtest F2 gap): the scaffold's own starter node teaches
@@ -478,23 +473,17 @@ mod tests {
);
}
/// #241: the data-only starter blueprints reference std vocabulary only —
/// #241: the data-only starter blueprint references std vocabulary only —
/// no project-namespaced (`::`) type id survives in the rendered JSON.
/// The closed one binds every param (runnable); the open sibling leaves
/// `fast.length` free (sweepable).
/// It binds every param (runnable, #246: bound values are defaults an
/// `--axis` may override, so this closed starter is also the sweep
/// target).
#[test]
fn data_only_starter_blueprint_is_std_only() {
let json = render_project(SIGNAL_JSON_STD, "demo-lab");
assert!(!json.contains("__"), "unsubstituted token: {json}");
assert!(!json.contains("::"), "project-namespaced id in std-only blueprint: {json}");
assert!(json.contains("\"name\":\"demo_lab_signal\""));
let open = render_project(SIGNAL_OPEN_JSON_STD, "demo-lab");
assert!(!open.contains("::"), "project-namespaced id in std-only blueprint: {open}");
assert!(open.contains("\"name\":\"demo_lab_signal_open\""));
assert!(
open.contains(r#"{"primitive":{"type":"SMA","name":"fast"}}"#),
"the open sibling must leave fast.length unbound: {open}"
);
}
#[test]