24782caaec
Slice 5 of the #319 retirement. The scaffolder's project CLAUDE.md and the CLI concepts help now teach only the surviving surface (exec over both document classes, --override, graph introspect --params for discovery; traces via exec --tap / presentation.persist_taps). Sweep vehicles moved onto campaign documents driven by exec (project_new's starter quickstarts, cli_broken_pipe — whose bare-sweep EPIPE probe was latently vacuous and now streams a real 4-member campaign, project_sweep_campaign's real-data leg, graph_construct's gang-axis parity pair as paired one-cell campaigns); pure discovery sites became graph introspect --params, and the line-identity test kept its literal --params expectations minus the sweep half. Help pins rewritten; the scaffold template's in-src pin follows the new bytes. refs #319
571 lines
24 KiB
Rust
571 lines
24 KiB
Rust
//! `aura new` — scaffold a data-only research project (#241).
|
|
//!
|
|
//! `aura new` emits the data-only tier: a paths-only `Aura.toml`, a runnable
|
|
//! starter blueprint over the std vocabulary, `.gitignore`, and `CLAUDE.md` —
|
|
//! no crate, no build step. The scaffolder never builds and never loads (the
|
|
//! project-load phase in `main()` sees a plain data-only project just like
|
|
//! any other).
|
|
//!
|
|
//! The crate-scaffold templates (`ScaffoldSpec`/`scaffold_node_crate`/
|
|
//! `scaffold_spec`, cycle 0103, C16/C17) serve the native-node tier: `aura
|
|
//! nodes new` attaches a node crate beside a project.
|
|
//!
|
|
//! Templates are raw strings with `__NS__` / `__NAME__` / `__NAME_SNAKE__` /
|
|
//! `__ENGINE__` tokens substituted by plain `.replace()` — no format-string
|
|
//! brace escaping in Rust template bodies.
|
|
|
|
use std::path::{Path, PathBuf};
|
|
|
|
#[derive(Debug)]
|
|
pub struct ScaffoldSpec {
|
|
pub name: String,
|
|
pub namespace: String,
|
|
pub engine_root: PathBuf,
|
|
pub target_dir: PathBuf,
|
|
}
|
|
|
|
fn valid_name(s: &str) -> bool {
|
|
!s.is_empty()
|
|
&& !s.starts_with(|c: char| c.is_ascii_digit())
|
|
&& s.chars().all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
|
|
}
|
|
|
|
fn valid_namespace(s: &str) -> bool {
|
|
!s.is_empty()
|
|
&& !s.starts_with(|c: char| c.is_ascii_digit())
|
|
&& s.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
|
|
}
|
|
|
|
/// The name→namespace / name→blueprint-name mapping: dashes become
|
|
/// underscores (mirrors cargo's crate-name normalization).
|
|
pub fn snake(name: &str) -> String {
|
|
name.replace('-', "_")
|
|
}
|
|
|
|
/// The compile-time default engine root: two ancestors above aura-cli's
|
|
/// manifest dir (`<root>/crates/aura-cli` → `<root>`). Valid on the box the
|
|
/// binary was built on; `scaffold_node_crate` refuses when it no longer
|
|
/// exists.
|
|
pub fn default_engine_root() -> PathBuf {
|
|
Path::new(env!("CARGO_MANIFEST_DIR"))
|
|
.ancestors()
|
|
.nth(2)
|
|
.expect("aura-cli manifest dir has two ancestors")
|
|
.to_path_buf()
|
|
}
|
|
|
|
/// The data-only project scaffold (#241): no crate, no build step.
|
|
#[derive(Debug)]
|
|
pub struct ProjectScaffoldSpec {
|
|
pub name: String,
|
|
pub target_dir: PathBuf,
|
|
}
|
|
|
|
/// Validate the argv-level inputs for the data-only project scaffold. `Err`
|
|
/// is a usage fault — the caller exits 2 (C14 partition).
|
|
pub fn project_scaffold_spec(name: &str, cwd: &Path) -> Result<ProjectScaffoldSpec, String> {
|
|
if !valid_name(name) {
|
|
return Err(format!(
|
|
"invalid project name `{name}` (allowed: letters, digits, `_`, `-`; no leading digit)"
|
|
));
|
|
}
|
|
Ok(ProjectScaffoldSpec { name: name.to_string(), target_dir: cwd.join(name) })
|
|
}
|
|
|
|
/// Validate the argv-level inputs. `Err` is a usage fault — the caller
|
|
/// exits 2 (C14 partition).
|
|
pub fn scaffold_spec(
|
|
name: &str,
|
|
engine_path: Option<&Path>,
|
|
namespace: Option<&str>,
|
|
cwd: &Path,
|
|
) -> Result<ScaffoldSpec, String> {
|
|
if !valid_name(name) {
|
|
return Err(format!(
|
|
"invalid project name `{name}` (allowed: letters, digits, `_`, `-`; no leading digit)"
|
|
));
|
|
}
|
|
let namespace = match namespace {
|
|
Some(ns) if !valid_namespace(ns) => {
|
|
return Err(format!(
|
|
"invalid namespace `{ns}` (allowed: letters, digits, `_`; no leading digit)"
|
|
));
|
|
}
|
|
Some(ns) => ns.to_string(),
|
|
None => snake(name),
|
|
};
|
|
Ok(ScaffoldSpec {
|
|
name: name.to_string(),
|
|
namespace,
|
|
engine_root: engine_path
|
|
.map(Path::to_path_buf)
|
|
.unwrap_or_else(default_engine_root),
|
|
target_dir: cwd.join(name),
|
|
})
|
|
}
|
|
|
|
// ---------------------------------------------------------------- templates
|
|
|
|
const CARGO_TOML: &str = r#"[package]
|
|
name = "__NAME__"
|
|
version = "0.1.0"
|
|
edition = "2024"
|
|
publish = false
|
|
|
|
[lib]
|
|
crate-type = ["cdylib"]
|
|
|
|
[dependencies]
|
|
aura-core = { path = "__ENGINE__/crates/aura-core" }
|
|
|
|
# Standalone workspace root: never a member of an enclosing workspace.
|
|
[workspace]
|
|
"#;
|
|
|
|
const AURA_TOML: &str = r#"# Static project context only (C17); paths only.
|
|
[paths]
|
|
runs = "runs"
|
|
# data = "/path/to/archive" # the recorded-data root; defaults to the built-in path
|
|
"#;
|
|
|
|
const GITIGNORE_CRATE: &str = "/target\nCargo.lock\n";
|
|
|
|
const LIB_RS: &str = r#"//! __NAME__ — an aura research project: node logic + the exported vocabulary.
|
|
//!
|
|
//! Every node type this crate provides is registered in `vocabulary()` /
|
|
//! `type_ids()` under the `__NS__::` namespace prefix; the `aura` host loads
|
|
//! this cdylib per invocation and merges it with the std vocabulary.
|
|
|
|
use aura_core::{
|
|
Cell, Ctx, FieldSpec, Firing, Node, NodeSchema, ParamSpec, PortSpec, PrimitiveBuilder,
|
|
ScalarKind,
|
|
};
|
|
|
|
/// 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 Scale {
|
|
pub fn new(factor: f64) -> Self {
|
|
Self { factor, out: [Cell::from_f64(0.0)] }
|
|
}
|
|
// Replace `doc` below when renaming this sample node — it must keep
|
|
// describing the node's own behaviour, not this scaffolding step.
|
|
pub fn builder() -> PrimitiveBuilder {
|
|
PrimitiveBuilder::new(
|
|
"__NS__::Scale",
|
|
NodeSchema {
|
|
inputs: vec![PortSpec {
|
|
kind: ScalarKind::F64,
|
|
firing: Firing::Any,
|
|
name: "value".into(),
|
|
}],
|
|
output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }],
|
|
params: vec![ParamSpec { name: "factor".into(), kind: ScalarKind::F64 }],
|
|
// C29: every vocabulary entry ships a one-line meaning — the
|
|
// doc field is required (E0063 without it) and gated at load.
|
|
doc: "scalar gain: emits the input times the factor param",
|
|
},
|
|
|p| Box::new(Scale::new(p[0].f64())),
|
|
)
|
|
}
|
|
}
|
|
|
|
impl Node for Scale {
|
|
fn lookbacks(&self) -> Vec<usize> {
|
|
vec![1]
|
|
}
|
|
fn eval(&mut self, ctx: Ctx<'_>) -> Option<&[Cell]> {
|
|
let w = ctx.f64_in(0);
|
|
if w.is_empty() {
|
|
return None;
|
|
}
|
|
self.out[0] = Cell::from_f64(w[0] * self.factor);
|
|
Some(&self.out)
|
|
}
|
|
fn label(&self) -> String {
|
|
format!("__NS__::Scale({})", self.factor)
|
|
}
|
|
}
|
|
|
|
fn vocabulary(type_id: &str) -> Option<PrimitiveBuilder> {
|
|
match type_id {
|
|
"__NS__::Scale" => Some(Scale::builder()),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn type_ids() -> &'static [&'static str] {
|
|
&["__NS__::Scale"]
|
|
}
|
|
|
|
aura_core::aura_project! {
|
|
namespace: "__NS__",
|
|
vocabulary: vocabulary,
|
|
type_ids: type_ids,
|
|
}
|
|
"#;
|
|
|
|
const CLAUDE_MD_CRATE: &str = r#"# __NAME__ — an aura node crate
|
|
|
|
Native node logic for an aura research project (extension-dev role). This
|
|
crate is attached to its project via `[nodes]` in the project's `Aura.toml`.
|
|
|
|
- Build: `cargo build` (the next `aura` invocation in the project loads the
|
|
fresh dylib)
|
|
- Each node is registered in `vocabulary()`/`type_ids()` under the
|
|
`__NS__::` namespace prefix; blueprints reference nodes by that id.
|
|
- This crate has no data-plane dependencies — nodes are pure stream
|
|
transformers over read-only input windows.
|
|
"#;
|
|
|
|
const GITIGNORE_PROJECT: &str = "/runs\n";
|
|
|
|
const SIGNAL_JSON_STD: &str = r#"{"format_version":1,"blueprint":{"name":"__NAME_SNAKE___signal","doc":"fast/slow SMA difference clamped into a directional bias","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"}]}}"#;
|
|
|
|
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 exec blueprints/signal.json` (single smoke run, synthetic stream
|
|
— the starter is closed; all params bound; bound values are defaults —
|
|
`--override NODE.PARAM=VALUE` may override one for this run (#246))
|
|
- Campaign: `aura exec <campaign.json>` executes a registered campaign
|
|
document (file or content id) — the multi-cell/axis surface
|
|
- Axes: `aura graph introspect --params blueprints/signal.json` 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`).
|
|
- Topology is data (`blueprints/*.json`); results land in `runs/`.
|
|
- Execution model: a strategy emits a bias in [-1,+1] per cycle, held as the
|
|
continuously-tracked target position; a protective stop defines the risk
|
|
unit R, and quality metrics are R-based. Entry signals become held state
|
|
via the signal-side latch/edge-pulse idiom (see
|
|
`aura graph introspect --vocabulary`).
|
|
- Data plane: the research verbs are sugar over registered process/campaign
|
|
documents — author them directly with `aura process` / `aura campaign`,
|
|
growing one from a bare `{}` via `aura campaign introspect --unwired`.
|
|
"#;
|
|
|
|
/// Render a data-only project template: only `__NAME__`/`__NAME_SNAKE__`
|
|
/// tokens exist in this template set (no namespace, no engine path).
|
|
fn render_project(template: &str, name: &str) -> String {
|
|
template
|
|
.replace("__NAME_SNAKE__", &snake(name))
|
|
.replace("__NAME__", name)
|
|
}
|
|
|
|
fn render(template: &str, spec: &ScaffoldSpec) -> String {
|
|
template
|
|
.replace("__NAME_SNAKE__", &snake(&spec.name))
|
|
.replace("__NAME__", &spec.name)
|
|
.replace("__NS__", &spec.namespace)
|
|
.replace("__ENGINE__", &spec.engine_root.display().to_string())
|
|
}
|
|
|
|
// --------------------------------------------------------------------- emit
|
|
|
|
/// Emit a data-only project. `Err` is a runtime refusal (exit 1). All
|
|
/// refusals fire before the first write.
|
|
pub fn scaffold_project(spec: &ProjectScaffoldSpec) -> Result<(), String> {
|
|
if spec.target_dir.exists() {
|
|
return Err(format!("destination `{}` already exists", spec.target_dir.display()));
|
|
}
|
|
let write = |rel: &str, body: String| -> Result<(), String> {
|
|
let path = spec.target_dir.join(rel);
|
|
if let Some(dir) = path.parent() {
|
|
std::fs::create_dir_all(dir)
|
|
.map_err(|e| format!("creating `{}`: {e}", dir.display()))?;
|
|
}
|
|
std::fs::write(&path, body).map_err(|e| format!("writing `{}`: {e}", path.display()))
|
|
};
|
|
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("CLAUDE.md", render_project(CLAUDE_MD_PROJECT, &spec.name))?;
|
|
if inside_git_work_tree(&spec.target_dir) {
|
|
return Ok(());
|
|
}
|
|
match std::process::Command::new("git").arg("init").current_dir(&spec.target_dir).output() {
|
|
Ok(o) if o.status.success() => {
|
|
commit_all(&spec.target_dir, "aura new scaffold");
|
|
}
|
|
_ => crate::diag::warning!("git init failed (project created without a repo)"),
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Emit a node crate (#241): the native half of the old single-tier scaffold
|
|
/// — Cargo.toml + src/lib.rs + crate-scoped .gitignore + CLAUDE.md, no
|
|
/// Aura.toml and no blueprints/ (those belong to the research project).
|
|
pub fn scaffold_node_crate(spec: &ScaffoldSpec) -> Result<(), String> {
|
|
if spec.target_dir.exists() {
|
|
return Err(format!("destination `{}` already exists", spec.target_dir.display()));
|
|
}
|
|
if !spec.engine_root.join("crates/aura-core").is_dir() {
|
|
return Err(format!(
|
|
"engine checkout not found at `{}` — pass --engine-path <engine checkout>",
|
|
spec.engine_root.display()
|
|
));
|
|
}
|
|
let write = |rel: &str, body: String| -> Result<(), String> {
|
|
let path = spec.target_dir.join(rel);
|
|
if let Some(dir) = path.parent() {
|
|
std::fs::create_dir_all(dir)
|
|
.map_err(|e| format!("creating `{}`: {e}", dir.display()))?;
|
|
}
|
|
std::fs::write(&path, body).map_err(|e| format!("writing `{}`: {e}", path.display()))
|
|
};
|
|
write("Cargo.toml", render(CARGO_TOML, spec))?;
|
|
write(".gitignore", GITIGNORE_CRATE.to_string())?;
|
|
write("src/lib.rs", render(LIB_RS, spec))?;
|
|
write("CLAUDE.md", render(CLAUDE_MD_CRATE, spec))?;
|
|
// Best-effort git init: a warning, never an error (cargo-new mirror).
|
|
// Also the cargo-new mirror: inside an existing work tree the init is
|
|
// SKIPPED silently — a nested .git is a gitlink/submodule hazard when the
|
|
// scaffold lands in a tracked tree (#204).
|
|
if inside_git_work_tree(&spec.target_dir) {
|
|
return Ok(());
|
|
}
|
|
match std::process::Command::new("git").arg("init").current_dir(&spec.target_dir).output() {
|
|
Ok(o) if o.status.success() => {
|
|
commit_all(&spec.target_dir, "aura nodes new scaffold");
|
|
}
|
|
_ => crate::diag::warning!("git init failed (node crate created without a repo)"),
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Append the `[nodes]` pointer to a project's Aura.toml. Refuses when a
|
|
/// `[nodes]` section already exists (single-crate iteration).
|
|
pub fn append_nodes_pointer(project_root: &Path, pointer: &str) -> Result<(), String> {
|
|
let path = project_root.join("Aura.toml");
|
|
let text = std::fs::read_to_string(&path)
|
|
.map_err(|e| format!("reading `{}`: {e}", path.display()))?;
|
|
if text.contains("[nodes]") {
|
|
return Err(
|
|
"Aura.toml already has a [nodes] section (multi-crate loading is not yet supported)"
|
|
.to_string(),
|
|
);
|
|
}
|
|
std::fs::write(&path, format!("{text}\n[nodes]\ncrates = [\"{pointer}\"]\n"))
|
|
.map_err(|e| format!("writing `{}`: {e}", path.display()))
|
|
}
|
|
|
|
/// Stage everything and make the initial commit in a freshly-`git init`ed
|
|
/// scaffold, so `git rev-parse HEAD` resolves from the very first run
|
|
/// (otherwise HEAD stays unborn and provenance stamping sees no commit). A
|
|
/// committer identity is passed inline via `-c` so this works on a box with
|
|
/// no global git config; best-effort — a failure is a warning, never fatal,
|
|
/// and never runs when `git init` itself was skipped or failed.
|
|
fn commit_all(dir: &std::path::Path, message: &str) {
|
|
let add = std::process::Command::new("git")
|
|
.args(["add", "-A"])
|
|
.current_dir(dir)
|
|
.output();
|
|
if !matches!(add, Ok(o) if o.status.success()) {
|
|
crate::diag::warning!("git add failed (scaffold created without an initial commit)");
|
|
return;
|
|
}
|
|
let commit = std::process::Command::new("git")
|
|
.args([
|
|
"-c",
|
|
"user.email=aura@localhost",
|
|
"-c",
|
|
"user.name=aura",
|
|
"commit",
|
|
"-q",
|
|
"-m",
|
|
message,
|
|
])
|
|
.current_dir(dir)
|
|
.output();
|
|
if !matches!(commit, Ok(o) if o.status.success()) {
|
|
crate::diag::warning!(
|
|
"git commit failed (scaffold created without an initial commit)"
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Whether `dir` already sits inside a git work tree (`git rev-parse`, the
|
|
/// same oracle cargo-new consults). A missing git binary or any failure reads
|
|
/// as "not in a work tree" — the init stays best-effort either way.
|
|
fn inside_git_work_tree(dir: &std::path::Path) -> bool {
|
|
std::process::Command::new("git")
|
|
.args(["rev-parse", "--is-inside-work-tree"])
|
|
.current_dir(dir)
|
|
.output()
|
|
.map(|o| o.status.success() && String::from_utf8_lossy(&o.stdout).trim() == "true")
|
|
.unwrap_or(false)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn spec_for(name: &str, ns: Option<&str>) -> ScaffoldSpec {
|
|
scaffold_spec(name, None, ns, Path::new("/tmp/x")).expect("valid spec")
|
|
}
|
|
|
|
#[test]
|
|
fn name_and_namespace_validation_table() {
|
|
assert!(scaffold_spec("ger40-lab", None, None, Path::new("/t")).is_ok());
|
|
assert!(scaffold_spec("x_1", None, None, Path::new("/t")).is_ok());
|
|
for bad in ["", "bad/name", "9lives", "a b", "ä"] {
|
|
let e = scaffold_spec(bad, None, None, Path::new("/t")).unwrap_err();
|
|
assert!(e.contains("invalid project name"), "{bad}: {e}");
|
|
}
|
|
for bad_ns in ["", "with-dash", "9x", "a::b"] {
|
|
let e = scaffold_spec("ok", None, Some(bad_ns), Path::new("/t")).unwrap_err();
|
|
assert!(e.contains("invalid namespace"), "{bad_ns}: {e}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn namespace_defaults_to_snake_name() {
|
|
assert_eq!(spec_for("ger40-lab", None).namespace, "ger40_lab");
|
|
assert_eq!(spec_for("ger40-lab", Some("ger40")).namespace, "ger40");
|
|
assert_eq!(snake("a-b-c"), "a_b_c");
|
|
}
|
|
|
|
#[test]
|
|
fn default_engine_root_is_this_checkout() {
|
|
assert!(default_engine_root().join("crates/aura-core").is_dir());
|
|
}
|
|
|
|
#[test]
|
|
fn templates_render_without_leftover_tokens() {
|
|
// Crate-template set (native tier, via `ScaffoldSpec`).
|
|
let spec = spec_for("demo-lab", None);
|
|
for t in [CARGO_TOML, GITIGNORE_CRATE, LIB_RS, CLAUDE_MD_CRATE] {
|
|
let out = render(t, &spec);
|
|
assert!(!out.contains("__"), "unsubstituted token in: {out}");
|
|
}
|
|
let lib = render(LIB_RS, &spec);
|
|
assert!(lib.contains("\"demo_lab::Scale\""));
|
|
assert!(lib.contains("namespace: \"demo_lab\""));
|
|
let cargo = render(CARGO_TOML, &spec);
|
|
assert!(cargo.contains("name = \"demo-lab\""));
|
|
assert!(cargo.contains("/crates/aura-core"));
|
|
|
|
// Project-template set (data-only tier, via `render_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 quickstart targets the closed starter itself through
|
|
// the surviving surface (#319): exec for both document classes, the
|
|
// #246 override residue, and raw-namespace axis discovery (#328).
|
|
assert!(claude.contains("blueprints/signal.json"));
|
|
assert!(claude.contains("--override NODE.PARAM=VALUE"));
|
|
assert!(claude.contains("graph introspect --params"));
|
|
}
|
|
|
|
/// #315: the scaffolded project CLAUDE.md teaches the execution semantics
|
|
/// (bias as held target position, protective stop, R) and the document
|
|
/// data plane — the two things the 2026-07-22 field agent had to discover
|
|
/// forensically.
|
|
#[test]
|
|
fn project_claude_md_teaches_execution_semantics_and_the_data_plane() {
|
|
let claude = render_project(CLAUDE_MD_PROJECT, "demo-lab");
|
|
assert!(claude.contains("target position"), "names the held-target model: {claude}");
|
|
assert!(claude.contains("protective stop"), "names the stop that defines R: {claude}");
|
|
assert!(claude.contains("aura process"), "points at the document layer: {claude}");
|
|
assert!(claude.contains("introspect --unwired"), "names the bare-{{}} ramp: {claude}");
|
|
}
|
|
|
|
/// #323: the authoring guide's S0 worked literal is byte-identical to the
|
|
/// scaffold's `LIB_RS` template (rendered with the guide's `my_lab`
|
|
/// namespace). The scaffold e2e tests compile that template for real, so
|
|
/// this equality pin is a transitive compile guard — the guide literal
|
|
/// can no longer drift silently from the schema (it broke against the
|
|
/// required `NodeSchema.doc` once).
|
|
#[test]
|
|
fn guide_worked_example_matches_the_scaffold_template() {
|
|
let guide = include_str!("../../../docs/authoring-guide.md");
|
|
let anchor = "### Worked example: `Scale`";
|
|
let after = &guide[guide.find(anchor).expect("guide keeps the S0 worked example")..];
|
|
let start = after.find("```rust\n").expect("worked example is a rust fence") + 8;
|
|
let fence = &after[start..];
|
|
let fence = &fence[..fence.find("\n```").expect("fence closes") + 1];
|
|
let body = &LIB_RS[LIB_RS.find("use aura_core::{").expect("template body starts at the import")..];
|
|
let expected = body.replace("__NS__", "my_lab");
|
|
assert_eq!(
|
|
fence, expected,
|
|
"docs/authoring-guide.md S0 literal drifted from the compiled scaffold template"
|
|
);
|
|
}
|
|
|
|
/// Property (#183, fieldtest F2 gap): the scaffold's own starter node teaches
|
|
/// the bound-param build closure. Its rendered `src/lib.rs` declares a param in
|
|
/// the node schema and reads a bound value in the build closure (not the
|
|
/// param-less `|_|` Identity the template shipped before) — the copyable
|
|
/// template content a newcomer learns from directly.
|
|
#[test]
|
|
fn starter_node_declares_and_reads_a_bound_param() {
|
|
let spec = spec_for("demo-lab", None);
|
|
let lib = render(LIB_RS, &spec);
|
|
// the node schema declares a bindable param...
|
|
assert!(
|
|
lib.contains("ParamSpec"),
|
|
"starter node must declare a param in its schema, not `params: vec![]`: {lib}"
|
|
);
|
|
// ...and the build closure reads that bound param (p[0]), not `|_|`.
|
|
assert!(
|
|
lib.contains("p[0].f64()"),
|
|
"starter build closure must consume a bound param (p[0].f64()), not `|_|`: {lib}"
|
|
);
|
|
}
|
|
|
|
/// #241: the data-only starter blueprint references std vocabulary only —
|
|
/// no project-namespaced (`::`) type id survives in the rendered JSON.
|
|
/// 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\""));
|
|
}
|
|
|
|
#[test]
|
|
fn scaffold_project_refuses_existing_destination() {
|
|
let tmp = std::path::Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/../../target/tmp"))
|
|
.join("aura-pscaf");
|
|
let _ = std::fs::remove_dir_all(&tmp);
|
|
std::fs::create_dir_all(tmp.join("x")).unwrap();
|
|
let spec = project_scaffold_spec("x", &tmp).unwrap();
|
|
let e = scaffold_project(&spec).unwrap_err();
|
|
assert!(e.contains("already exists"), "{e}");
|
|
std::fs::remove_dir_all(&tmp).ok();
|
|
}
|
|
|
|
#[test]
|
|
fn scaffold_node_crate_refuses_existing_dir_and_missing_engine() {
|
|
let tmp = std::path::Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/../../target/tmp"))
|
|
.join("aura-scaf");
|
|
let _ = std::fs::remove_dir_all(&tmp);
|
|
std::fs::create_dir_all(&tmp).unwrap();
|
|
// existing destination
|
|
let mut spec = scaffold_spec("x", None, None, &tmp).unwrap();
|
|
std::fs::create_dir_all(tmp.join("x")).unwrap();
|
|
let e = scaffold_node_crate(&spec).unwrap_err();
|
|
assert!(e.contains("already exists"), "{e}");
|
|
// missing engine root
|
|
std::fs::remove_dir_all(tmp.join("x")).unwrap();
|
|
spec.engine_root = PathBuf::from("/nonexistent-engine");
|
|
let e = scaffold_node_crate(&spec).unwrap_err();
|
|
assert!(e.contains("--engine-path"), "{e}");
|
|
std::fs::remove_dir_all(&tmp).ok();
|
|
}
|
|
}
|