From 024d878ac3f467268c601dd615fd370b9ed5caf2 Mon Sep 17 00:00:00 2001 From: claude Date: Mon, 13 Jul 2026 04:11:04 +0200 Subject: [PATCH] feat(cli): the scaffold ships one starter blueprint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- crates/aura-cli/src/scaffold.rs | 41 ++++++++------------ crates/aura-cli/tests/project_new.rs | 56 ++++++++++++++++++++++------ docs/authoring-guide.md | 7 ++-- 3 files changed, 64 insertions(+), 40 deletions(-) diff --git a/crates/aura-cli/src/scaffold.rs b/crates/aura-cli/src/scaffold.rs index a3dedaf..4ad85de 100644 --- a/crates/aura-cli/src/scaffold.rs +++ b/crates/aura-cli/src/scaffold.rs @@ -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 --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 --list-axes` lists the open + bound-overridable axes) - 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`). @@ -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] diff --git a/crates/aura-cli/tests/project_new.rs b/crates/aura-cli/tests/project_new.rs index f289d8b..6fbae50 100644 --- a/crates/aura-cli/tests/project_new.rs +++ b/crates/aura-cli/tests/project_new.rs @@ -79,11 +79,15 @@ fn new_scaffolds_and_runs_the_data_only_loop() { "Aura.toml", ".gitignore", "blueprints/signal.json", - "blueprints/signal_open.json", "CLAUDE.md", ] { assert!(proj.join(f).is_file(), "missing scaffolded file {f}"); } + assert_eq!( + std::fs::read_dir(proj.join("blueprints")).unwrap().count(), + 1, + "blueprints/signal.json must be the ONLY starter blueprint (#246)" + ); assert!(!proj.join("Cargo.toml").exists(), "data-only project must have no Cargo.toml"); assert!(!proj.join("src/lib.rs").exists(), "data-only project must have no src/lib.rs"); @@ -108,15 +112,11 @@ fn new_scaffolds_and_runs_the_data_only_loop() { } /// #241 headline: a fresh data-only project sweeps a dissolved verb (#218's -/// gate) with zero build step — using the scaffolded open starter blueprint -/// and the exact axis the scaffolded CLAUDE.md advertises. +/// gate) with zero build step — using the scaffolded (closed) starter +/// blueprint and the exact axis the scaffolded CLAUDE.md advertises. /// -/// Why a second starter blueprint exists at all: `aura run` requires a -/// CLOSED blueprint (every param bound) while `aura sweep` requires OPEN -/// knobs ("this blueprint is fully bound; nothing to sweep"), so one file -/// cannot serve both quickstart lines. `blueprints/signal.json` stays closed -/// for the run headline above; `blueprints/signal_open.json` leaves -/// `fast.length` free for this one. +/// #246: the closed starter IS the sweep target — a bound param is a default +/// an axis overrides. #[test] fn data_only_project_sweeps_without_any_build() { let base = tmp("sweep"); @@ -124,7 +124,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_open.json", "--axis", "scratch_signal_open.fast.length=2,4"], + &["sweep", "blueprints/signal.json", "--axis", "scratch_signal.fast.length=2,4"], &proj, ); assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr)); @@ -132,6 +132,36 @@ fn data_only_project_sweeps_without_any_build() { let _ = std::fs::remove_dir_all(&base); } +/// Property (#246): the scaffold's sweep quickstart genuinely re-opens the +/// bound `fast.length` param — the persisted family has exactly one member +/// per axis value, not a single collapsed member. A silent regression where +/// the bound-param override is dropped (member always built from the +/// blueprint's own default) would still exit 0 and still create a `runs` +/// store, so `data_only_project_sweeps_without_any_build` above cannot catch +/// it; only the member count can. +#[test] +fn data_only_project_sweep_over_the_starter_opens_one_member_per_axis_value() { + let base = tmp("sweep-members"); + let new = aura(&["new", "scratch"], &base); + 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"], + &proj, + ); + assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr)); + let fams = aura(&["runs", "families"], &proj); + assert!(fams.status.success(), "runs families exit: {:?}", fams.status); + let fams_out = String::from_utf8_lossy(&fams.stdout).into_owned(); + assert_eq!(fams_out.lines().count(), 1, "exactly one family persisted: {fams_out}"); + assert!( + fams_out.contains("\"members\":3"), + "the three-value bound-param axis grid must yield three members, one per \ + value (2,4,8) — not a collapsed default-only sweep: {fams_out}" + ); + let _ = std::fs::remove_dir_all(&base); +} + #[test] fn new_refuses_an_existing_destination() { let base = tmp("exists"); @@ -186,11 +216,15 @@ fn new_inside_a_work_tree_skips_the_nested_git() { "Aura.toml", ".gitignore", "blueprints/signal.json", - "blueprints/signal_open.json", "CLAUDE.md", ] { assert!(proj.join(f).is_file(), "missing scaffolded file {f}"); } + assert_eq!( + std::fs::read_dir(proj.join("blueprints")).unwrap().count(), + 1, + "blueprints/signal.json must be the ONLY starter blueprint (#246)" + ); // ...but without a nested repo of its own. assert!( !proj.join(".git").exists(), diff --git a/docs/authoring-guide.md b/docs/authoring-guide.md index a932b0e..548b14f 100644 --- a/docs/authoring-guide.md +++ b/docs/authoring-guide.md @@ -38,9 +38,10 @@ the same shape every node in `aura-std` already follows. - **The project's own signal doesn't need a crate by default.** `aura new` scaffolds a **data-only** project (`docs/project-layout.md`, "A project repo (two tiers)") — a strategy over the std vocabulary is a - blueprint/campaign document, not Rust: the scaffold ships a closed `signal.json` run target - and an open `signal_open.json` sweep target, both runnable immediately, no - build step. + blueprint/campaign document, not Rust: the scaffold ships a single closed + `signal.json` starter, runnable immediately with no build step, and also + the sweep target itself (a bound param is a default any `--axis` may + override, #246). - **A project-specific *native* node type** — the moment §0's three-part pattern is actually needed — lives in an attached **node crate**: `aura nodes new ` scaffolds a sibling cdylib crate (`Cargo.toml` +