Files
Aura/docs/specs/0103-aura-new-scaffolder.md
T
Brummel c707250912 spec: 0103 aura new scaffolder (boss-signed)
Grounding-check PASS as the autonomous signature: 8 load-bearing
current-behaviour assumptions each ratified by a named green test
(the templates are the cycle-0102 fixture verbatim, itself e2e-green);
greenfield scaffolder surfaces excluded per contract. Fork decisions:
the cycle-0103 reconciliation comment on the milestone reference issue.

refs #180
2026-07-02 18:27:11 +02:00

11 KiB

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-labger40_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)

$ 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:

[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:

# Static project context only (C17); paths only (cycle 0102).
[paths]
runs = "runs"

.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:

# {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:

    /// Scaffold a new research project crate.
    New(NewCmd),
#[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:

    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):

    // `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:

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-labger40_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 buildaura 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.