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
This commit is contained in:
@@ -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 <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