diff --git a/docs/specs/0103-aura-new-scaffolder.md b/docs/specs/0103-aura-new-scaffolder.md new file mode 100644 index 0000000..7c4d48e --- /dev/null +++ b/docs/specs/0103-aura-new-scaffolder.md @@ -0,0 +1,281 @@ +# 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 ` 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 ::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, + /// Vocabulary namespace (default: name with dashes as underscores). + #[arg(long)] + namespace: Option, +} +``` + +`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/ +} + +/// Validate argv-level inputs (bad name/namespace = usage fault, exit 2). +pub fn scaffold_spec(cmd: &NewCmd) -> Result; + +/// 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 [--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/; 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 `` (namespace ``)" +``` + +## 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 ` → `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.