audit: cycle 0103 tidy (drift-clean after three doc-mirror alignments)
Architect review of b672a37..HEAD. What holds: emitted templates match the settled contracts (Aura.toml paths-only per C17, cdylib + empty [workspace] per C16, ::-namespaced ids per the charter, the C14 exit partition in dispatch_new); the load-bypass guard is narrow and sound (only the new-project verb exempted, the load arm byte-unchanged for every other verb); e2e coverage is real (scaffold -> build -> run twice byte-identical, namespace stamped, introspection, four refusals, the unbuilt-tree bypass, plus the additive namespace-override test). Regression: cargo build --workspace rc=0; cargo test --workspace 873 passed / 0 failed (project_new 6/6); clippy --workspace --all-targets -D warnings rc=0; cargo doc --workspace --no-deps 0 warnings. No baselines exist to move, so nothing to ratify. Drift resolved in this commit: three doc mirrors still framing the scaffolder as future — project-layout.md's 'the future aura new scaffolder will emit this line; until then, add it by hand', the C24 status paragraph's 'what remains open is the aura new scaffolder (the milestone's next cycle)', and the #109 open-thread bullet's 'the aura new scaffolder and the experiment-builder API remain open' — all rewritten to the landed reading (only the experiment-builder API stays open in that layer). Carried debt, filed forward: the scaffold templates and the demo-project fixture encode one project shape twice with no lockstep guard and deliberate cosmetic divergences (#181, idea — needs a design pass, not a mechanical edit). Process note: one mid-cycle plan-fix commit (derive(Debug) on a plan-prescribed struct whose own test code required it) — the implement loop's spec gate correctly blocked the necessary deviation; resolution followed the fix-plan/commit-subset/re-run path. Ephemeral cycle artifacts removed per convention: spec 0103, plan 0103. refs #180
This commit is contained in:
@@ -1986,10 +1986,11 @@ construction-arg builders (`LinComb` / `CostSum` / `SimBroker` / `Session` —
|
||||
structural-axis args, a C20 concern), additively addable later (#156). The
|
||||
**project-as-crate load boundary landed in cycle 0102** (`Aura.toml` discovery +
|
||||
`cdylib` loading + merged project ∪ std vocabulary — see the C13 realization
|
||||
note); of the two layers this paragraph used to name as sequencing-coupled,
|
||||
what remains open is the `aura new` scaffolder (the milestone's next cycle) and
|
||||
the **composable-orchestration** thread (#109): topology-as-data is the
|
||||
substrate both stand on.
|
||||
note; the `aura new` scaffolder followed in cycle 0103 — one command emits a
|
||||
buildable project whose blueprint runs through the merged vocabulary); of the
|
||||
two layers this paragraph used to name as sequencing-coupled, what remains
|
||||
open is the **composable-orchestration** thread (#109): topology-as-data is
|
||||
the substrate it stands on.
|
||||
|
||||
**Op-script grammar (`aura graph build`, the #157 wire surface).** The construction
|
||||
op-script is a JSON **array of ops**, each object internally tagged by `"op"`,
|
||||
@@ -2088,8 +2089,9 @@ artifact, and any run is deterministic once instantiated" (C1).
|
||||
a user wires into an analysis workflow, rather than today's hard-wired CLI verbs
|
||||
(`sweep_family` / `walkforward_family` in `aura-cli`) — still open; and (2) the
|
||||
project-as-crate authoring layer above — its **load boundary landed in cycle 0102**
|
||||
(`Aura.toml` discovery + cdylib loading + merged vocabulary, C13 realization note;
|
||||
the `aura new` scaffolder and the experiment-builder API remain open, C16/C17).
|
||||
(`Aura.toml` discovery + cdylib loading + merged vocabulary, C13 realization note)
|
||||
and the **`aura new` scaffolder in cycle 0103**; of that layer only the
|
||||
experiment-builder API remains open (C16/C17).
|
||||
On the meta-level, authoring a strategy is *wiring
|
||||
existing nodes* (C9 composition) + *defining the analysis framework* (C21), not
|
||||
writing new nodes; the user wants it driven via the CLI, later the interactive
|
||||
|
||||
@@ -1,667 +0,0 @@
|
||||
# The `aura new` scaffolder — Implementation Plan
|
||||
|
||||
> **Parent spec:** `docs/specs/0103-aura-new-scaffolder.md`
|
||||
>
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: use the `implement` skill to
|
||||
> run this plan. Steps use `- [ ]` checkboxes for tracking.
|
||||
|
||||
**Goal:** `aura new <name>` emits a complete project crate (the cycle-0102
|
||||
fixture shape, parameterized) that builds and runs a blueprint with zero
|
||||
hand-editing; the verb bypasses the eager project-load so it works inside
|
||||
unbuilt trees.
|
||||
|
||||
**Architecture:** one new `scaffold` module in aura-cli (validation +
|
||||
token-replace templates + emit + best-effort git init), one new `Command::New`
|
||||
variant with a `dispatch_new`, and a 3-line load-bypass guard in `main()`.
|
||||
Everything else is untouched — the verb is additive.
|
||||
|
||||
**Tech Stack:** aura-cli only. No new dependencies (`git init` via
|
||||
`std::process::Command`, engine root via `env!("CARGO_MANIFEST_DIR")`).
|
||||
|
||||
**Files this plan creates or modifies:**
|
||||
|
||||
- Create: `crates/aura-cli/src/scaffold.rs` — validation, templates, emit
|
||||
- Create: `crates/aura-cli/tests/project_new.rs` — authoring-loop e2e + refusals
|
||||
- Modify: `crates/aura-cli/src/main.rs:14-16` (mod block), `:3742-3762`
|
||||
(Command enum + NewCmd beside the other `*Cmd` structs), `:4638-4662`
|
||||
(load-bypass guard + dispatch arm + `dispatch_new`)
|
||||
- Test: unit tests inside `scaffold.rs`; e2e in `tests/project_new.rs`
|
||||
|
||||
---
|
||||
|
||||
### Task 1: the scaffold module (validation + templates + emit)
|
||||
|
||||
**Files:**
|
||||
- Create: `crates/aura-cli/src/scaffold.rs`
|
||||
- Modify: `crates/aura-cli/src/main.rs:14-16` (add `mod scaffold;`)
|
||||
|
||||
- [ ] **Step 1: Create `crates/aura-cli/src/scaffold.rs`**
|
||||
|
||||
```rust
|
||||
//! `aura new` — scaffold a research project crate (cycle 0103, C16/C17).
|
||||
//!
|
||||
//! Emits the cycle-0102 fixture shape parameterized by name/namespace: a
|
||||
//! cdylib crate with path-deps on the engine checkout (the baked default or
|
||||
//! `--engine-path`), a paths-only `Aura.toml`, a sample node + runnable
|
||||
//! blueprint, and a project `CLAUDE.md`. The scaffolder never builds and
|
||||
//! never loads (the author builds; `aura new` bypasses the project-load
|
||||
//! phase in `main()`).
|
||||
//!
|
||||
//! 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` 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()
|
||||
}
|
||||
|
||||
/// 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"
|
||||
"#;
|
||||
|
||||
const GITIGNORE: &str = "/target\nCargo.lock\n/runs\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, PortSpec, PrimitiveBuilder,
|
||||
ScalarKind,
|
||||
};
|
||||
|
||||
/// One-input f64 pass-through. Emits `None` until its input has a value.
|
||||
pub struct Identity {
|
||||
out: [Cell; 1],
|
||||
}
|
||||
|
||||
impl Identity {
|
||||
pub fn new() -> Self {
|
||||
Self { out: [Cell::from_f64(0.0)] }
|
||||
}
|
||||
pub fn builder() -> PrimitiveBuilder {
|
||||
PrimitiveBuilder::new(
|
||||
"__NS__::Identity",
|
||||
NodeSchema {
|
||||
inputs: vec![PortSpec {
|
||||
kind: ScalarKind::F64,
|
||||
firing: Firing::Any,
|
||||
name: "value".into(),
|
||||
}],
|
||||
output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }],
|
||||
params: vec![],
|
||||
},
|
||||
|_| Box::new(Identity::new()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Identity {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Node for Identity {
|
||||
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]);
|
||||
Some(&self.out)
|
||||
}
|
||||
fn label(&self) -> String {
|
||||
"__NS__::Identity".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn vocabulary(type_id: &str) -> Option<PrimitiveBuilder> {
|
||||
match type_id {
|
||||
"__NS__::Identity" => Some(Identity::builder()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn type_ids() -> &'static [&'static str] {
|
||||
&["__NS__::Identity"]
|
||||
}
|
||||
|
||||
aura_core::aura_project! {
|
||||
namespace: "__NS__",
|
||||
vocabulary: vocabulary,
|
||||
type_ids: type_ids,
|
||||
}
|
||||
"#;
|
||||
|
||||
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 CLAUDE_MD: &str = r#"# __NAME__ — an aura research project
|
||||
|
||||
This crate is an aura project: a cdylib of node/strategy blueprints the
|
||||
`aura` host loads during research (see the engine's docs/project-layout.md).
|
||||
|
||||
- Build: `cargo build` (the next `aura` invocation loads the fresh dylib)
|
||||
- Run: `aura run blueprints/signal.json` (from anywhere inside this dir)
|
||||
- Nodes live in `src/`; each is registered in `vocabulary()`/`type_ids()`
|
||||
under the `__NS__::` namespace prefix.
|
||||
- Topology is data (`blueprints/*.json`), node logic is Rust.
|
||||
"#;
|
||||
|
||||
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 the project. `Err` is a runtime refusal — the caller exits 1 (C14
|
||||
/// partition). All refusals fire before the first write.
|
||||
pub fn scaffold(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("Aura.toml", render(AURA_TOML, spec))?;
|
||||
write(".gitignore", render(GITIGNORE, spec))?;
|
||||
write("src/lib.rs", render(LIB_RS, spec))?;
|
||||
write("blueprints/signal.json", render(SIGNAL_JSON, spec))?;
|
||||
write("CLAUDE.md", render(CLAUDE_MD, spec))?;
|
||||
// Best-effort git init: a warning, never an error (cargo-new mirror).
|
||||
match std::process::Command::new("git")
|
||||
.arg("init")
|
||||
.current_dir(&spec.target_dir)
|
||||
.output()
|
||||
{
|
||||
Ok(o) if o.status.success() => {}
|
||||
_ => eprintln!("aura: warning: git init failed (project created without a repo)"),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[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() {
|
||||
let spec = spec_for("demo-lab", None);
|
||||
for t in [CARGO_TOML, AURA_TOML, GITIGNORE, LIB_RS, SIGNAL_JSON, CLAUDE_MD] {
|
||||
let out = render(t, &spec);
|
||||
assert!(!out.contains("__"), "unsubstituted token in: {out}");
|
||||
}
|
||||
let lib = render(LIB_RS, &spec);
|
||||
assert!(lib.contains("\"demo_lab::Identity\""));
|
||||
assert!(lib.contains("namespace: \"demo_lab\""));
|
||||
let json = render(SIGNAL_JSON, &spec);
|
||||
assert!(json.contains("\"type\":\"demo_lab::Identity\""));
|
||||
assert!(json.contains("\"name\":\"demo_lab_signal\""));
|
||||
let cargo = render(CARGO_TOML, &spec);
|
||||
assert!(cargo.contains("name = \"demo-lab\""));
|
||||
assert!(cargo.contains("/crates/aura-core"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scaffold_refuses_existing_dir_and_missing_engine() {
|
||||
let tmp = std::env::temp_dir().join(format!("aura-scaf-{}", std::process::id()));
|
||||
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(&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(&spec).unwrap_err();
|
||||
assert!(e.contains("--engine-path"), "{e}");
|
||||
std::fs::remove_dir_all(&tmp).ok();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Register the module**
|
||||
|
||||
In `crates/aura-cli/src/main.rs`, in the mod block (lines 14-16, currently
|
||||
`mod render;` / `mod graph_construct;` / `mod project;`), add:
|
||||
|
||||
```rust
|
||||
mod scaffold;
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Run the unit tests**
|
||||
|
||||
Run: `cargo test -p aura-cli scaffold::`
|
||||
Expected: PASS — `name_and_namespace_validation_table`,
|
||||
`namespace_defaults_to_snake_name`, `default_engine_root_is_this_checkout`,
|
||||
`templates_render_without_leftover_tokens`,
|
||||
`scaffold_refuses_existing_dir_and_missing_engine`. (Interim dead-code
|
||||
warnings are expected until Task 2 wires the verb; the plain build stays
|
||||
warning-tolerant and Task 4 runs the clippy gate.)
|
||||
|
||||
---
|
||||
|
||||
### Task 2: CLI wiring (`Command::New`, dispatch, load-bypass guard)
|
||||
|
||||
**Files:**
|
||||
- Modify: `crates/aura-cli/src/main.rs:3742-3762` (Command enum), beside the
|
||||
other `*Cmd` structs (~3764), `:4638-4662` (main), beside the other
|
||||
`dispatch_*` fns (~4236)
|
||||
|
||||
- [ ] **Step 1: Add the variant and the args struct**
|
||||
|
||||
In `enum Command` (main.rs:3742-3762), after the `Reproduce(...)` variant add:
|
||||
|
||||
```rust
|
||||
/// Scaffold a new research project crate.
|
||||
New(NewCmd),
|
||||
```
|
||||
|
||||
Beside the other `*Cmd` structs (e.g. after `ChartCmd`), add:
|
||||
|
||||
```rust
|
||||
#[derive(clap::Args)]
|
||||
struct NewCmd {
|
||||
/// Directory / crate name to create.
|
||||
name: String,
|
||||
/// Engine checkout the project depends on (path deps). Default: the
|
||||
/// engine root this binary was built from.
|
||||
#[arg(long)]
|
||||
engine_path: Option<std::path::PathBuf>,
|
||||
/// Vocabulary namespace (default: the name with dashes as underscores).
|
||||
#[arg(long)]
|
||||
namespace: Option<String>,
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: The load-bypass guard in `main()`**
|
||||
|
||||
At main.rs:4639-4651, the current shape is:
|
||||
|
||||
```rust
|
||||
let env = match std::env::current_dir()
|
||||
.ok()
|
||||
.and_then(|d| project::discover_from(&d))
|
||||
{
|
||||
Some(root) => match project::load(&root, cli.release) {
|
||||
Ok(p) => project::Env::with_project(p),
|
||||
Err(e) => {
|
||||
eprintln!("aura: {e}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
},
|
||||
None => project::Env::std(),
|
||||
};
|
||||
```
|
||||
|
||||
Wrap it (the inner load arm stays byte-identical):
|
||||
|
||||
```rust
|
||||
// `aura new` scaffolds; it must not require a loadable project even
|
||||
// when invoked inside one (e.g. an unbuilt tree).
|
||||
let env = if matches!(cli.command, Command::New(_)) {
|
||||
project::Env::std()
|
||||
} else {
|
||||
match std::env::current_dir()
|
||||
.ok()
|
||||
.and_then(|d| project::discover_from(&d))
|
||||
{
|
||||
Some(root) => match project::load(&root, cli.release) {
|
||||
Ok(p) => project::Env::with_project(p),
|
||||
Err(e) => {
|
||||
eprintln!("aura: {e}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
},
|
||||
None => project::Env::std(),
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
- [ ] **Step 3: The dispatch arm and `dispatch_new`**
|
||||
|
||||
In the `match cli.command` block (4652-4662), add the arm:
|
||||
|
||||
```rust
|
||||
Command::New(a) => dispatch_new(a, &env),
|
||||
```
|
||||
|
||||
Beside the other `dispatch_*` fns, add:
|
||||
|
||||
```rust
|
||||
fn dispatch_new(a: NewCmd, _env: &project::Env) {
|
||||
let cwd = std::env::current_dir().unwrap_or_else(|e| {
|
||||
eprintln!("aura: {e}");
|
||||
std::process::exit(1);
|
||||
});
|
||||
let spec = scaffold::scaffold_spec(
|
||||
&a.name,
|
||||
a.engine_path.as_deref(),
|
||||
a.namespace.as_deref(),
|
||||
&cwd,
|
||||
)
|
||||
.unwrap_or_else(|m| {
|
||||
eprintln!("aura: {m}");
|
||||
std::process::exit(2);
|
||||
});
|
||||
scaffold::scaffold(&spec).unwrap_or_else(|m| {
|
||||
eprintln!("aura: {m}");
|
||||
std::process::exit(1);
|
||||
});
|
||||
println!(
|
||||
"created project `{}` (namespace `{}`)",
|
||||
spec.name, spec.namespace
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Compile gate + smoke**
|
||||
|
||||
Run: `cargo build --workspace`
|
||||
Expected: 0 errors (the dispatch match is the one exhaustiveness site).
|
||||
|
||||
Run: `cargo test -p aura-cli --test cli_run`
|
||||
Expected: PASS unchanged (the verb is additive; no existing pin moves).
|
||||
|
||||
---
|
||||
|
||||
### Task 3: the authoring-loop e2e
|
||||
|
||||
**Files:**
|
||||
- Create: `crates/aura-cli/tests/project_new.rs`
|
||||
|
||||
- [ ] **Step 1: Create `crates/aura-cli/tests/project_new.rs`**
|
||||
|
||||
```rust
|
||||
//! E2E: the `aura new` scaffolder (cycle 0103). Proves the full authoring
|
||||
//! loop — scaffold → cargo build → aura run — with zero hand-editing, plus
|
||||
//! the refusal contract. Scaffolded projects live in per-test temp dirs;
|
||||
//! their path-deps resolve into this checkout (the baked engine root).
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::{Command, Output};
|
||||
|
||||
fn aura(args: &[&str], cwd: &Path) -> Output {
|
||||
Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||
.args(args)
|
||||
.current_dir(cwd)
|
||||
.output()
|
||||
.expect("run aura")
|
||||
}
|
||||
|
||||
fn tmp(tag: &str) -> PathBuf {
|
||||
let d = std::env::temp_dir().join(format!("aura-new-{tag}-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&d);
|
||||
std::fs::create_dir_all(&d).unwrap();
|
||||
d
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_scaffold_builds_and_runs_the_authoring_loop() {
|
||||
let base = tmp("loop");
|
||||
let out = aura(&["new", "demo-lab"], &base);
|
||||
assert!(
|
||||
out.status.success(),
|
||||
"aura new failed: {}",
|
||||
String::from_utf8_lossy(&out.stderr)
|
||||
);
|
||||
let proj = base.join("demo-lab");
|
||||
for f in [
|
||||
"Cargo.toml",
|
||||
"Aura.toml",
|
||||
".gitignore",
|
||||
"src/lib.rs",
|
||||
"blueprints/signal.json",
|
||||
"CLAUDE.md",
|
||||
] {
|
||||
assert!(proj.join(f).is_file(), "missing scaffolded file {f}");
|
||||
}
|
||||
let build = Command::new("cargo")
|
||||
.arg("build")
|
||||
.current_dir(&proj)
|
||||
.output()
|
||||
.expect("cargo build scaffolded project");
|
||||
assert!(
|
||||
build.status.success(),
|
||||
"scaffolded build failed:\n{}",
|
||||
String::from_utf8_lossy(&build.stderr)
|
||||
);
|
||||
let a = aura(&["run", "blueprints/signal.json"], &proj);
|
||||
assert!(
|
||||
a.status.success(),
|
||||
"run failed: {}",
|
||||
String::from_utf8_lossy(&a.stderr)
|
||||
);
|
||||
let b = aura(&["run", "blueprints/signal.json"], &proj);
|
||||
assert_eq!(a.stdout, b.stdout, "two runs must be byte-identical (C1)");
|
||||
let v: serde_json::Value = serde_json::from_slice(&a.stdout).expect("report is JSON");
|
||||
assert_eq!(v["manifest"]["project"]["namespace"], "demo_lab");
|
||||
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");
|
||||
let _ = std::fs::remove_dir_all(&base);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_refuses_an_existing_destination() {
|
||||
let base = tmp("exists");
|
||||
std::fs::create_dir_all(base.join("taken")).unwrap();
|
||||
let out = aura(&["new", "taken"], &base);
|
||||
assert_eq!(out.status.code(), Some(1), "runtime refusal");
|
||||
let err = String::from_utf8_lossy(&out.stderr);
|
||||
assert!(err.contains("already exists"), "stderr: {err}");
|
||||
let _ = std::fs::remove_dir_all(&base);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_rejects_invalid_names_as_usage_faults() {
|
||||
let base = tmp("badname");
|
||||
for bad in ["bad/name", "9lives"] {
|
||||
let out = aura(&["new", bad], &base);
|
||||
assert_eq!(out.status.code(), Some(2), "usage fault for {bad}");
|
||||
let err = String::from_utf8_lossy(&out.stderr);
|
||||
assert!(err.contains("invalid project name"), "stderr: {err}");
|
||||
}
|
||||
let _ = std::fs::remove_dir_all(&base);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_refuses_a_missing_engine_path_with_hint() {
|
||||
let base = tmp("noengine");
|
||||
let out = aura(&["new", "x", "--engine-path", "/nonexistent-engine"], &base);
|
||||
assert_eq!(out.status.code(), Some(1), "runtime refusal");
|
||||
let err = String::from_utf8_lossy(&out.stderr);
|
||||
assert!(err.contains("--engine-path"), "stderr: {err}");
|
||||
let _ = std::fs::remove_dir_all(&base);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_works_inside_an_unbuilt_project_tree() {
|
||||
// Scaffold once (unbuilt), then scaffold AGAIN from inside that tree:
|
||||
// the load-bypass guard must keep aura new from trying to load the
|
||||
// enclosing project's (absent) dylib.
|
||||
let base = tmp("nested");
|
||||
let out = aura(&["new", "outer"], &base);
|
||||
assert!(out.status.success());
|
||||
let inner_cwd = base.join("outer");
|
||||
let out = aura(&["new", "inner"], &inner_cwd);
|
||||
assert!(
|
||||
out.status.success(),
|
||||
"aura new inside an unbuilt project tree must succeed: {}",
|
||||
String::from_utf8_lossy(&out.stderr)
|
||||
);
|
||||
assert!(inner_cwd.join("inner/Cargo.toml").is_file());
|
||||
let _ = std::fs::remove_dir_all(&base);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the e2e suite**
|
||||
|
||||
Run: `cargo test -p aura-cli --test project_new`
|
||||
Expected: PASS, all five tests (the first pays the scaffolded project's
|
||||
cargo build).
|
||||
|
||||
---
|
||||
|
||||
### Task 4: full regression gate
|
||||
|
||||
**Files:** none (verification only)
|
||||
|
||||
- [ ] **Step 1: workspace build + suite**
|
||||
|
||||
Run: `cargo build --workspace`
|
||||
Expected: 0 errors.
|
||||
|
||||
Run: `cargo test --workspace`
|
||||
Expected: PASS — every pre-existing test green unchanged plus the new
|
||||
scaffold unit tests and the five project_new e2e tests.
|
||||
|
||||
- [ ] **Step 2: lint + docs**
|
||||
|
||||
Run: `cargo clippy --workspace --all-targets -- -D warnings`
|
||||
Expected: clean.
|
||||
|
||||
Run: `cargo doc --workspace --no-deps 2>&1 | grep -c warning`
|
||||
Expected: `0`.
|
||||
@@ -57,8 +57,9 @@ ger40-lab/ # your research project — a separate Rust crate (cd
|
||||
>
|
||||
> The fix is cargo's own hint: an empty `[workspace]` table in the project's
|
||||
> `Cargo.toml`. It pins the crate as a standalone workspace root and costs
|
||||
> nothing when the project later sits in its own repo. The future `aura new`
|
||||
> scaffolder will emit this line; until then, add it by hand.
|
||||
> nothing when the project later sits in its own repo. The `aura new`
|
||||
> scaffolder emits this line (cycle 0103); a hand-rolled project adds it
|
||||
> itself.
|
||||
|
||||
## Where reusable nodes live (three tiers)
|
||||
|
||||
|
||||
@@ -1,281 +0,0 @@
|
||||
# The `aura new` scaffolder — Design Spec
|
||||
|
||||
**Date:** 2026-07-02
|
||||
**Status:** Draft — awaiting sign-off (boss run: grounding-check gate)
|
||||
**Authors:** orchestrator + Claude
|
||||
**Cycle:** 0103 — second implementation cycle of milestone "Project environment
|
||||
— the project-as-crate authoring loop" (#180; fork decisions for this cycle are
|
||||
the cycle-0103 reconciliation comment on #180)
|
||||
|
||||
## Goal
|
||||
|
||||
`aura new <name>` scaffolds a complete, immediately usable research project:
|
||||
after the one command, `cargo build` succeeds and `aura run
|
||||
blueprints/signal.json` runs a real blueprint through the project's own
|
||||
loaded vocabulary — the full authoring loop (edit → build → run) with zero
|
||||
hand-editing. The emitted files are the cycle-0102 fixture
|
||||
(`crates/aura-cli/tests/fixtures/demo-project/`) parameterized by project
|
||||
name and namespace.
|
||||
|
||||
## Architecture
|
||||
|
||||
One new subcommand in `aura-cli` (`Command::New` + `dispatch_new`), a
|
||||
`scaffold` module that renders the fixed file set from templates
|
||||
(format-string parameterization; no template engine), and one load-order
|
||||
carve-out in `main()`: **`aura new` bypasses the project-load phase** —
|
||||
scaffolding must work inside an unbuilt or foreign project tree, so the
|
||||
eager `discover→load` step added in cycle 0102 is skipped for exactly this
|
||||
verb (an `aura new` invocation never needs a vocabulary).
|
||||
|
||||
Decisions carried from the #180 reconciliation comment (cycle 0103):
|
||||
|
||||
- **Engine dependency = path deps on the engine checkout.** Default root:
|
||||
the engine root baked into the binary at compile time
|
||||
(`env!("CARGO_MANIFEST_DIR")` of aura-cli, two ancestors up), overridable
|
||||
via `--engine-path`; a default that no longer exists on disk refuses with
|
||||
the `--engine-path` hint (never a silently broken scaffold). Rationale
|
||||
(recorded on #180): unpublished proprietary engine; a git-URL dep embeds a
|
||||
host and can pin a revision whose aura-core diverges from the host binary
|
||||
(provoking the C13 stamp refusal); the path dep makes the stamp match hold
|
||||
by construction.
|
||||
- **Namespace default = project name, dashes → underscores** (`ger40-lab` →
|
||||
`ger40_lab`), overridable via `--namespace`. The load-time charter
|
||||
validates it anyway.
|
||||
- **cargo-new-mirror conventions:** refuse an existing target directory;
|
||||
best-effort `git init`; no `cargo build` inside the scaffolder (the author
|
||||
builds — the cycle-0102 "aura only loads" decision).
|
||||
|
||||
## Concrete code shapes
|
||||
|
||||
### The user-facing loop (the acceptance-criterion evidence)
|
||||
|
||||
```console
|
||||
$ aura new ger40-lab
|
||||
created project `ger40-lab` (namespace `ger40_lab`)
|
||||
$ cd ger40-lab && cargo build
|
||||
$ aura run blueprints/signal.json # resolves ger40_lab::Identity
|
||||
$ $EDITOR src/lib.rs # author a real node
|
||||
$ cargo build && aura run blueprints/signal.json # the authoring loop
|
||||
```
|
||||
|
||||
Emitted tree:
|
||||
|
||||
```
|
||||
ger40-lab/
|
||||
├── Aura.toml # [paths] runs = "runs"
|
||||
├── Cargo.toml # cdylib; path-deps on the engine; empty [workspace]
|
||||
├── CLAUDE.md # minimal project authoring notes
|
||||
├── .gitignore # /target, Cargo.lock, /runs
|
||||
├── blueprints/
|
||||
│ └── signal.json # runnable demo blueprint using <ns>::Identity
|
||||
└── src/
|
||||
└── lib.rs # Identity node + vocabulary + aura_project! export
|
||||
```
|
||||
|
||||
### Emitted file contents (templates, parameterized by `{name}`, `{ns}`, `{engine}`)
|
||||
|
||||
`Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[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]
|
||||
```
|
||||
|
||||
`Aura.toml`:
|
||||
|
||||
```toml
|
||||
# Static project context only (C17); paths only (cycle 0102).
|
||||
[paths]
|
||||
runs = "runs"
|
||||
```
|
||||
|
||||
`.gitignore`:
|
||||
|
||||
```gitignore
|
||||
/target
|
||||
Cargo.lock
|
||||
/runs
|
||||
```
|
||||
|
||||
`src/lib.rs` — the cycle-0102 fixture's `lib.rs` verbatim with
|
||||
`demo::Identity` → `{ns}::Identity` and namespace `"demo"` → `"{ns}"`
|
||||
(same `Identity` node, `vocabulary` fn, `type_ids` fn, `aura_project!`
|
||||
invocation; the fixture is a green, tested source — not aspirational code).
|
||||
|
||||
`blueprints/signal.json` — the fixture's `demo_signal.json` verbatim with
|
||||
`"demo::Identity"` → `"{ns}::Identity"` and the blueprint name
|
||||
`"demo_signal"` → `"{name_snake}_signal"` (`{name_snake}` = the project name
|
||||
with dashes mapped to underscores — the same mapping as the namespace
|
||||
default).
|
||||
|
||||
`CLAUDE.md`:
|
||||
|
||||
```markdown
|
||||
# {name} — an aura research project
|
||||
|
||||
This crate is an aura project: a cdylib of node/strategy blueprints the
|
||||
`aura` host loads during research (see the engine's docs/project-layout.md).
|
||||
|
||||
- Build: `cargo build` (the next `aura` invocation loads the fresh dylib)
|
||||
- Run: `aura run blueprints/signal.json` (from anywhere inside this dir)
|
||||
- Nodes live in `src/`; each is registered in `vocabulary()`/`type_ids()`
|
||||
under the `{ns}::` namespace prefix.
|
||||
- Topology is data (`blueprints/*.json`), node logic is Rust.
|
||||
```
|
||||
|
||||
### CLI shape (before → after, secondary)
|
||||
|
||||
`Command` enum (aura-cli `main.rs`) gains:
|
||||
|
||||
```rust
|
||||
/// Scaffold a new research project crate.
|
||||
New(NewCmd),
|
||||
```
|
||||
|
||||
```rust
|
||||
#[derive(clap::Args)]
|
||||
struct NewCmd {
|
||||
/// Directory / crate name to create.
|
||||
name: String,
|
||||
/// Engine checkout the project depends on (path deps).
|
||||
#[arg(long)]
|
||||
engine_path: Option<PathBuf>,
|
||||
/// Vocabulary namespace (default: name with dashes as underscores).
|
||||
#[arg(long)]
|
||||
namespace: Option<String>,
|
||||
}
|
||||
```
|
||||
|
||||
`main()` load-order carve-out — before:
|
||||
|
||||
```rust
|
||||
let env = match std::env::current_dir().ok().and_then(|d| project::discover_from(&d)) {
|
||||
Some(root) => match project::load(&root, cli.release) { ... },
|
||||
None => project::Env::std(),
|
||||
};
|
||||
```
|
||||
|
||||
After (only the guard is new; the load arm is byte-unchanged):
|
||||
|
||||
```rust
|
||||
// `aura new` scaffolds; it must not require a loadable project even
|
||||
// when invoked inside one (e.g. an unbuilt tree).
|
||||
let env = if matches!(cli.command, Command::New(_)) {
|
||||
project::Env::std()
|
||||
} else {
|
||||
match std::env::current_dir().ok().and_then(|d| project::discover_from(&d)) {
|
||||
Some(root) => match project::load(&root, cli.release) { ... },
|
||||
None => project::Env::std(),
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
New module `aura-cli/src/scaffold.rs`:
|
||||
|
||||
```rust
|
||||
pub struct ScaffoldSpec {
|
||||
pub name: String, // validated: [A-Za-z0-9_-]+, no leading digit
|
||||
pub namespace: String, // validated: [A-Za-z0-9_]+, no leading digit
|
||||
pub engine_root: PathBuf,
|
||||
pub target_dir: PathBuf, // cwd/<name>
|
||||
}
|
||||
|
||||
/// Validate argv-level inputs (bad name/namespace = usage fault, exit 2).
|
||||
pub fn scaffold_spec(cmd: &NewCmd) -> Result<ScaffoldSpec, String>;
|
||||
|
||||
/// Emit the file set; every failure is a runtime refusal (exit 1):
|
||||
/// target dir exists, engine root missing (with --engine-path hint),
|
||||
/// io errors. Ends with a best-effort `git init` (a warning on failure,
|
||||
/// never an error).
|
||||
pub fn scaffold(spec: &ScaffoldSpec) -> Result<(), String>;
|
||||
|
||||
/// The compile-time default engine root: two ancestors above aura-cli's
|
||||
/// manifest dir. A binary running on the box it was built on resolves to
|
||||
/// the live checkout; anywhere else the existence check refuses.
|
||||
pub fn default_engine_root() -> PathBuf; // env!("CARGO_MANIFEST_DIR")/../..
|
||||
```
|
||||
|
||||
## Components
|
||||
|
||||
| Component | Crate | New/changed |
|
||||
|---|---|---|
|
||||
| `Command::New` + `NewCmd` + `dispatch_new` | aura-cli | new variant + fn |
|
||||
| `scaffold` module (templates + validation + emit) | aura-cli | new module |
|
||||
| `main()` New-bypass guard | aura-cli | 3-line edit |
|
||||
| e2e `tests/project_new.rs` | aura-cli | new test file |
|
||||
|
||||
No engine/registry/analysis crate is touched. No new dependencies (`git init`
|
||||
via subprocess, matching the build.rs git-subprocess precedent).
|
||||
|
||||
## Data flow
|
||||
|
||||
```
|
||||
aura new <name> [--engine-path P] [--namespace NS]
|
||||
→ validate name/ns (usage fault → exit 2)
|
||||
→ engine root = P | baked default; must exist (else exit 1 + hint)
|
||||
→ target dir cwd/<name>; must not exist (else exit 1)
|
||||
→ emit Cargo.toml/Aura.toml/.gitignore/src/lib.rs/blueprints/signal.json/CLAUDE.md
|
||||
→ best-effort git init
|
||||
→ print "created project `<name>` (namespace `<ns>`)"
|
||||
```
|
||||
|
||||
## Error handling
|
||||
|
||||
- Invalid `name` / `--namespace` (empty, path separators, leading digit,
|
||||
chars outside `[A-Za-z0-9_-]` / `[A-Za-z0-9_]`) → **exit 2** (argv fault,
|
||||
C14 partition), message naming the offending value.
|
||||
- Target directory already exists → **exit 1** (environment state), message
|
||||
naming the path.
|
||||
- Engine root absent (baked default on a foreign box, or a wrong
|
||||
`--engine-path`) → **exit 1**, message naming the missing path and the
|
||||
`--engine-path` flag.
|
||||
- `git init` failure → a `warning:` line on stderr, exit unaffected.
|
||||
- No partial-tree cleanup promise: refusals happen before the first write
|
||||
(all validation precedes emission); an io error mid-emit leaves the
|
||||
partial dir for inspection, named in the message.
|
||||
|
||||
## Testing strategy
|
||||
|
||||
1. **Unit (scaffold module):** name/namespace validation table (valid,
|
||||
empty, `a/b`, leading digit, dash-vs-underscore rules); namespace
|
||||
derivation (`ger40-lab` → `ger40_lab`); `default_engine_root` ends in
|
||||
the checkout root (its `crates/aura-core` exists in this workspace);
|
||||
template rendering contains the parameterized ids (`{ns}::Identity` in
|
||||
lib.rs and signal.json bytes).
|
||||
2. **E2E (`tests/project_new.rs`, the authoring-loop proof):** in a temp
|
||||
dir, `aura new demo-lab`; assert the six files exist; `cargo build` in
|
||||
the scaffolded dir (path-deps resolve into this checkout); `aura run
|
||||
blueprints/signal.json` from inside it exits 0, twice byte-identically
|
||||
(C1), with `manifest.project.namespace == "demo_lab"`; `aura graph
|
||||
introspect --vocabulary` lists `demo_lab::Identity`.
|
||||
3. **Refusals (e2e):** second `aura new demo-lab` in the same cwd → exit 1
|
||||
naming the dir; `aura new "bad/name"` → exit 2; `aura new x
|
||||
--engine-path /nonexistent` → exit 1 with the hint; `aura new` inside an
|
||||
UNBUILT scaffolded project dir succeeds (the load-bypass guard).
|
||||
4. **Regression:** full workspace suite green unchanged (the verb is
|
||||
additive; no existing path is touched beyond the 3-line main() guard).
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
1. `aura new <name>` → `cargo build` → `aura run blueprints/signal.json`
|
||||
succeeds end-to-end with zero manual edits, bit-deterministically, and
|
||||
the run's manifest records the project provenance under the derived
|
||||
namespace.
|
||||
2. Every refusal in Error handling fires with the documented exit class and
|
||||
a message naming the cause; `aura new` works inside an unbuilt project
|
||||
tree (no project load precedes scaffolding).
|
||||
3. The emitted file set byte-matches the templates (fixture-derived), with
|
||||
name/namespace/engine-path parameterization and nothing else varying.
|
||||
4. The full existing suite stays green unchanged.
|
||||
Reference in New Issue
Block a user