Nine tasks: aura-core descriptor contract (two-tier repr(C) + build.rs stamps), node_name namespace strip, RunManifest.project Tier-1 field (22-literal sweep), aura-cli project module (discover/load/charter/Env), the Env threading fan-out, provenance stamping, demo-project fixture + five e2e tests, docs/ledger alignment, full regression gate. refs #180
51 KiB
The project-as-crate load boundary — Implementation Plan
Parent spec:
docs/specs/0102-project-load-boundary.mdFor agentic workers: REQUIRED SUB-SKILL: use the
implementskill to run this plan. Steps use- [ ]checkboxes for tracking.
Goal: an aura invocation inside a project directory loads the project's
cdylib through a two-tier ABI-checked descriptor, merges its vocabulary with
std_vocabulary, anchors registry/data paths at the project root, and stamps
provenance into the manifest — while outside a project every byte of behaviour
is unchanged.
Architecture: new aura-core::project (descriptor contract, both sides
compile it), new aura-cli::project (discovery/load/charter/Env), an
&Env-threading fan-out through the enumerated dispatch/helper chains, one
Tier-1 manifest field, one fixture project crate proving e2e.
Tech Stack: aura-core (repr(C) descriptor, build.rs), aura-cli (libloading, toml, cargo-metadata subprocess), aura-engine (RunManifest), docs/ledger.
Files this plan creates or modifies:
- Create:
crates/aura-core/build.rs— bakeAURA_RUSTC_VERSION - Create:
crates/aura-core/src/project.rs— descriptor + stamps + macro - Create:
crates/aura-cli/src/project.rs— discover/load/charter/Env - Create:
crates/aura-cli/tests/fixtures/demo-project/{Cargo.toml,Aura.toml,.gitignore,src/lib.rs,demo_signal.json}— fixture project - Create:
crates/aura-cli/tests/project_load.rs— e2e + refusal tests - Modify:
crates/aura-core/src/lib.rs:31-38—pub mod project; - Modify:
crates/aura-core/src/node.rs:143-147— namespace strip - Modify:
crates/aura-engine/src/report.rs:58-60,270-332—projectfield + struct + legacy test - Modify: 21 further
RunManifest {literals (enumerated in Task 3) - Modify:
crates/aura-cli/Cargo.toml— addlibloading,toml - Modify:
crates/aura-cli/src/main.rs— threading fan-out (sites enumerated per task) - Modify:
crates/aura-cli/src/graph_construct.rs:105-200— resolver threading - Modify:
docs/project-layout.md:33-34,docs/glossary.md:20-21,docs/design/INDEX.md(C13 ~1055, C16 1148-1149, C17 1209-1210)
Task 1: aura-core descriptor contract
Files:
-
Create:
crates/aura-core/build.rs -
Create:
crates/aura-core/src/project.rs -
Modify:
crates/aura-core/src/lib.rs:31-38 -
Step 1: Create
crates/aura-core/build.rs
//! Bakes the compiling toolchain's version into `AURA_RUSTC_VERSION` so the
//! project descriptor (src/project.rs) can stamp it. Runs per consuming build:
//! the host binary and a project cdylib each recompile aura-core under their
//! own toolchain, which is exactly what makes the two stamps comparable.
fn main() {
let rustc = std::env::var("RUSTC").unwrap_or_else(|_| "rustc".into());
let out = std::process::Command::new(&rustc)
.arg("--version")
.output()
.expect("rustc --version");
println!(
"cargo:rustc-env=AURA_RUSTC_VERSION={}",
String::from_utf8_lossy(&out.stdout).trim()
);
}
- Step 2: Create
crates/aura-core/src/project.rs
//! The project-cdylib descriptor contract (C13/C16, cycle 0102).
//!
//! A research project compiles to a cdylib exporting ONE symbol,
//! `AURA_PROJECT`, a [`ProjectDescriptor`]. The descriptor has two ABI tiers:
//! a **C tier** (`magic`, `descriptor_version`, the version stamps, the
//! namespace — all C-compatible field types) the host validates BEFORE
//! trusting anything, and a **Rust tier** (the vocabulary resolver and the
//! enumerable type-id list) the host touches only after both stamps match.
//! The stamp cannot certify the channel it rides on, so it must not itself
//! depend on the Rust ABI it certifies.
//!
//! The stamps are baked at *aura-core compile time* — per consuming build —
//! so host and dylib each carry their own side's truth.
use crate::node::PrimitiveBuilder;
/// First 8 bytes of the C tier; ASCII "AURAPROJ".
pub const AURA_DESCRIPTOR_MAGIC: u64 = 0x4155_5241_5052_4F4A;
/// Bumped on any layout change of [`ProjectDescriptor`].
pub const AURA_DESCRIPTOR_VERSION: u32 = 1;
/// The one exported symbol name (NUL-terminated for the symbol lookup).
pub const AURA_PROJECT_SYMBOL: &[u8] = b"AURA_PROJECT\0";
/// The compiling rustc's `--version` line (via build.rs).
pub const RUSTC_VERSION: &str = env!("AURA_RUSTC_VERSION");
/// This aura-core's crate version.
pub const CORE_VERSION: &str = env!("CARGO_PKG_VERSION");
/// C-ABI string slice: pointer + length over `'static` UTF-8 bytes.
/// No NUL convention needed; readable without trusting the Rust ABI.
#[repr(C)]
#[derive(Clone, Copy)]
pub struct StrSlice {
pub ptr: *const u8,
pub len: usize,
}
impl StrSlice {
pub const fn new(s: &'static str) -> Self {
Self { ptr: s.as_ptr(), len: s.len() }
}
/// Read the slice back as UTF-8.
///
/// # Safety
/// `ptr`/`len` must describe live UTF-8 bytes — true for any descriptor
/// built by [`aura_project!`], whose strings are `'static` and whose
/// library is loaded-and-held (never unloaded).
pub unsafe fn as_str(&self) -> Option<&str> {
if self.ptr.is_null() {
return None;
}
let bytes = unsafe { core::slice::from_raw_parts(self.ptr, self.len) };
core::str::from_utf8(bytes).ok()
}
}
/// The exported project descriptor. Field ORDER is ABI: C tier first,
/// Rust tier last; validated front-to-back.
#[repr(C)]
pub struct ProjectDescriptor {
// ---- C tier: validated BEFORE any Rust-ABI field is touched ----
pub magic: u64,
pub descriptor_version: u32,
pub rustc_version: StrSlice,
pub aura_core_version: StrSlice,
pub namespace: StrSlice,
// ---- Rust tier: only after both stamps match ----
pub vocabulary: fn(&str) -> Option<PrimitiveBuilder>,
pub type_ids: fn() -> &'static [&'static str],
}
// The raw pointers are to 'static data; the fn pointers are Sync by nature.
unsafe impl Sync for ProjectDescriptor {}
/// Declare a project crate's export in one place. Emits the `AURA_PROJECT`
/// static with this build's stamps baked in.
#[macro_export]
macro_rules! aura_project {
(namespace: $ns:literal, vocabulary: $vocab:path, type_ids: $ids:path $(,)?) => {
#[unsafe(no_mangle)]
pub static AURA_PROJECT: $crate::project::ProjectDescriptor =
$crate::project::ProjectDescriptor {
magic: $crate::project::AURA_DESCRIPTOR_MAGIC,
descriptor_version: $crate::project::AURA_DESCRIPTOR_VERSION,
rustc_version: $crate::project::StrSlice::new(
$crate::project::RUSTC_VERSION,
),
aura_core_version: $crate::project::StrSlice::new(
$crate::project::CORE_VERSION,
),
namespace: $crate::project::StrSlice::new($ns),
vocabulary: $vocab,
type_ids: $ids,
};
};
}
#[cfg(test)]
mod tests {
use super::*;
fn fake_vocab(type_id: &str) -> Option<PrimitiveBuilder> {
let _ = type_id;
None
}
fn fake_ids() -> &'static [&'static str] {
&["t::A"]
}
aura_project! {
namespace: "t",
vocabulary: fake_vocab,
type_ids: fake_ids,
}
#[test]
fn stamps_are_baked_and_nonempty() {
assert!(RUSTC_VERSION.starts_with("rustc "));
assert!(!CORE_VERSION.is_empty());
}
#[test]
fn str_slice_round_trips() {
let s = StrSlice::new("hello");
assert_eq!(unsafe { s.as_str() }, Some("hello"));
}
#[test]
fn macro_emits_a_valid_descriptor() {
let d = &AURA_PROJECT;
assert_eq!(d.magic, AURA_DESCRIPTOR_MAGIC);
assert_eq!(d.descriptor_version, AURA_DESCRIPTOR_VERSION);
assert_eq!(unsafe { d.namespace.as_str() }, Some("t"));
assert_eq!(unsafe { d.rustc_version.as_str() }, Some(RUSTC_VERSION));
assert_eq!(unsafe { d.aura_core_version.as_str() }, Some(CORE_VERSION));
assert!((d.vocabulary)("nope").is_none());
assert_eq!((d.type_ids)(), &["t::A"]);
}
}
- Step 3: Register the module in
crates/aura-core/src/lib.rs
In the module list (lines 31-38), after the existing pub mod node; line add:
pub mod project;
(No new crate-root re-exports: the descriptor surface is addressed as
aura_core::project::...; the macro is exported at crate root by
#[macro_export] automatically.)
- Step 4: Build and test aura-core
Run: cargo test -p aura-core
Expected: PASS, including stamps_are_baked_and_nonempty,
str_slice_round_trips, macro_emits_a_valid_descriptor.
Task 2: node_name() strips the namespace prefix
Files:
-
Modify:
crates/aura-core/src/node.rs:143-147 -
Test:
crates/aura-core/src/node.rs(tests module, near line 414) -
Step 1: Write the failing test
In the existing #[cfg(test)] tests module of node.rs (beside
node_name_defaults_to_lowercased_type_label, line 414), add:
#[test]
fn node_name_strips_namespace_prefix() {
let b = PrimitiveBuilder::new(
"demo::Identity",
NodeSchema { inputs: vec![], output: vec![], params: vec![] },
|_| unreachable!("never built in this test"),
);
assert_eq!(b.node_name(), "identity");
}
- Step 2: Run test to verify it fails
Run: cargo test -p aura-core node_name_strips_namespace_prefix
Expected: FAIL with assertion ... left: "demo::identity" ... right: "identity".
- Step 3: Implement the strip
crates/aura-core/src/node.rs:143-147, before:
pub fn node_name(&self) -> String {
self.instance_name
.clone()
.unwrap_or_else(|| self.name.to_ascii_lowercase())
}
After (byte-identical for ::-free std names):
pub fn node_name(&self) -> String {
self.instance_name.clone().unwrap_or_else(|| {
let bare = self.name.rsplit("::").next().unwrap_or(self.name);
bare.to_ascii_lowercase()
})
}
- Step 4: Run tests to verify green + unchanged pins
Run: cargo test -p aura-core
Expected: PASS — the new test plus the existing
node_name_defaults_to_lowercased_type_label (still "simbroker"/"fast").
Task 3: RunManifest.project provenance field (Tier-1 additive)
Files:
-
Modify:
crates/aura-engine/src/report.rs(struct at 29-60, tests at 270-332) -
Modify: the 21 other
RunManifest {struct literals (enumerated in Step 4) -
Step 1: Write the failing legacy-load test
In crates/aura-engine/src/report.rs, beside the existing
runmanifest_instrument_round_trips_and_omits_when_none (lines 288-314), add:
#[test]
fn runmanifest_project_field_round_trips_and_omits_when_none() {
// A pre-0102 line (no "project" key) must load with project: None.
let legacy = r#"{"commit":"c","params":[],"window":[0,1],"seed":7,"broker":"b"}"#;
let m: RunManifest = serde_json::from_str(legacy).expect("legacy line loads");
assert_eq!(m.project, None);
// None must serialize with the key absent (byte-stability of old paths).
let out = serde_json::to_string(&m).expect("serializes");
assert!(!out.contains("\"project\""));
// Some(..) must round-trip.
let p = ProjectProvenance {
namespace: "demo".into(),
dylib_sha256: "ab".repeat(32),
commit: None,
};
let m2 = RunManifest { project: Some(p.clone()), ..m };
let s = serde_json::to_string(&m2).expect("serializes");
let back: RunManifest = serde_json::from_str(&s).expect("round-trips");
assert_eq!(back.project, Some(p));
}
- Step 2: Run test to verify it fails to compile
Run: cargo test -p aura-engine runmanifest_project_field
Expected: COMPILE FAIL — no field 'project' on type RunManifest /
cannot find type ProjectProvenance.
- Step 3: Add the struct and the field
In crates/aura-engine/src/report.rs, directly after the topology_hash
field (lines 58-59), add:
/// Identity of the loaded project dylib, when the run used one (cycle
/// 0102). Pre-0102 records read as `None`; `None` serializes as an absent
/// key (the same one-directional widening as `instrument`).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub project: Option<ProjectProvenance>,
Below the RunManifest struct (after its closing brace), add:
/// Provenance of the project cdylib a run was resolved through (C16/C18).
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct ProjectProvenance {
/// The project's vocabulary namespace (from the descriptor).
pub namespace: String,
/// SHA-256 over the loaded dylib file's bytes.
pub dylib_sha256: String,
/// The project repo's HEAD (+ `-dirty`), when derivable.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub commit: Option<String>,
}
Re-export it beside the other report types: in
crates/aura-engine/src/lib.rs, extend the existing report:: re-export
list (line 74 area) with ProjectProvenance.
- Step 4: Thread
project: Nonethrough every struct literal
Run: cargo build --workspace 2>&1 | grep -c 'missing field .project'
Expected: a nonzero count naming exactly these literals (E0063). Add
project: None, after the topology_hash: None, (or last) field in each:
-
crates/aura-cli/src/main.rs:174(sim_optimal_manifest— covers all 20 CLI call sites) -
crates/aura-engine/src/blueprint.rs:997 -
crates/aura-engine/src/mc.rs:239, 265 -
crates/aura-engine/src/report.rs:279, 297, 307, 325, 456, 561, 592, 615 -
crates/aura-engine/src/sweep.rs:664 -
crates/aura-engine/src/walkforward.rs:288 -
crates/aura-ingest/examples/ger40_breakout_sweep.rs:63 -
crates/aura-ingest/examples/ger40_breakout_walkforward.rs:73 -
crates/aura-ingest/examples/shared/breakout_real.rs:390 -
crates/aura-ingest/tests/ger40_breakout_world.rs:65 -
crates/aura-ingest/tests/real_bars.rs:82 -
crates/aura-ingest/tests/streaming_seam.rs:82 -
crates/aura-registry/src/compat.rs:74 -
crates/aura-registry/src/lib.rs:583, 935 -
crates/aura-registry/src/trace_store.rs:250 -
Step 5: Verify green, workspace-wide
Run: cargo test --workspace
Expected: PASS — the new test green; every pre-existing test (incl. the
manifest-shape pins in cli_run.rs:34-39 and the goldens) unchanged, since
None serializes as an absent key.
Task 4: aura-cli project module (discover / load / charter / Env)
Files:
-
Modify:
crates/aura-cli/Cargo.toml -
Create:
crates/aura-cli/src/project.rs -
Modify:
crates/aura-cli/src/main.rs(only themod project;line this task) -
Step 1: Add the two dependencies
In crates/aura-cli/Cargo.toml [dependencies] (near sha2, line 42), add:
libloading = "0.8"
toml = "0.8"
(Per-case admission recorded in the spec: research-side leaf binary, the clap precedent; the frozen deploy artifact cannot pick either up.)
- Step 2: Create
crates/aura-cli/src/project.rs
//! Project discovery + the cdylib load boundary (cycle 0102, C13/C16).
//!
//! `discover_from` walks up to the nearest `Aura.toml` (the way cargo finds
//! `Cargo.toml`); `load` locates the project's cdylib via `cargo metadata`,
//! loads it **load-and-hold** (leaked, never unloaded), validates the C tier
//! of the descriptor BEFORE touching any Rust-ABI field, then applies the
//! vocabulary charter. `Env` is the per-invocation context every verb reads:
//! merged resolver, runs root, data path, provenance.
use aura_core::PrimitiveBuilder;
use aura_core::project::{
AURA_DESCRIPTOR_MAGIC, AURA_DESCRIPTOR_VERSION, AURA_PROJECT_SYMBOL,
CORE_VERSION, ProjectDescriptor, RUSTC_VERSION,
};
use aura_engine::ProjectProvenance;
use aura_registry::{Registry, TraceStore};
use aura_std::{std_vocabulary, std_vocabulary_types};
use sha2::{Digest, Sha256};
use std::fmt;
use std::path::{Path, PathBuf};
/// Parsed `Aura.toml` — static project context only (C17): paths, nothing else.
#[derive(Debug, Default, PartialEq, serde::Deserialize)]
pub struct AuraToml {
#[serde(default)]
pub paths: AuraPaths,
}
#[derive(Debug, Default, PartialEq, serde::Deserialize)]
pub struct AuraPaths {
/// Data archive root; default: the data-server default.
#[serde(default)]
pub data: Option<String>,
/// Registry root, relative to the project root; default `"runs"`.
#[serde(default)]
pub runs: Option<PathBuf>,
}
#[derive(Debug)]
pub enum ProjectError {
Toml(PathBuf, String),
CargoMetadata(String),
ArtifactMissing(PathBuf),
Load(PathBuf, String),
NotAProjectDylib(PathBuf),
Incompatible { what: &'static str, dylib: String, host: String },
Charter(String),
}
impl fmt::Display for ProjectError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Toml(p, e) => write!(f, "failed to parse {}: {e}", p.display()),
Self::CargoMetadata(e) => write!(f, "cargo metadata failed: {e}"),
Self::ArtifactMissing(p) => write!(
f,
"project dylib not found at {} — run `cargo build` in the \
project first (or pass --release to load the release build)",
p.display()
),
Self::Load(p, e) => write!(f, "failed to load {}: {e}", p.display()),
Self::NotAProjectDylib(p) => write!(
f,
"{} exports no valid AURA_PROJECT descriptor (not an aura \
project dylib, or an incompatible descriptor layout)",
p.display()
),
Self::Incompatible { what, dylib, host } => write!(
f,
"project dylib is ABI-incompatible: {what} mismatch \
(dylib: {dylib}, host: {host}) — rebuild the project with \
the host's toolchain/aura-core"
),
Self::Charter(e) => write!(f, "project vocabulary rejected: {e}"),
}
}
}
/// A successfully loaded project. The `Library` behind `resolver`/`type_ids`
/// is leaked (load-and-hold): the fn pointers are valid for 'static.
pub struct ProjectEnv {
pub root: PathBuf,
pub toml: AuraToml,
pub namespace: String,
pub dylib_sha256: String,
pub commit: Option<String>,
resolver: fn(&str) -> Option<PrimitiveBuilder>,
type_id_list: &'static [&'static str],
}
impl ProjectEnv {
fn resolve(&self, type_id: &str) -> Option<PrimitiveBuilder> {
(self.resolver)(type_id)
}
}
/// The per-invocation context every verb reads. Outside a project every
/// accessor collapses to today's literal defaults (byte-identical behaviour).
pub struct Env {
project: Option<ProjectEnv>,
}
impl Env {
pub fn std() -> Self {
Self { project: None }
}
pub fn with_project(p: ProjectEnv) -> Self {
Self { project: Some(p) }
}
/// The one resolver every verb consumes: project first, then std. The
/// charter makes overlap impossible, so order is not load-bearing.
pub fn resolve(&self, type_id: &str) -> Option<PrimitiveBuilder> {
match &self.project {
Some(p) => p.resolve(type_id).or_else(|| std_vocabulary(type_id)),
None => std_vocabulary(type_id),
}
}
/// project ∪ std type ids, project first (for `introspect --vocabulary`).
pub fn type_ids(&self) -> Vec<&'static str> {
let mut out: Vec<&'static str> = Vec::new();
if let Some(p) = &self.project {
out.extend_from_slice(p.type_id_list);
}
out.extend_from_slice(std_vocabulary_types());
out
}
/// The registry root: `<project>/<paths.runs|"runs">` inside a project,
/// the literal `runs` outside (today's behaviour).
pub fn runs_root(&self) -> PathBuf {
match &self.project {
Some(p) => {
let runs = p.toml.paths.runs.clone().unwrap_or_else(|| "runs".into());
p.root.join(runs)
}
None => PathBuf::from("runs"),
}
}
pub fn registry(&self) -> Registry {
Registry::open(self.runs_root().join("runs.jsonl"))
}
pub fn trace_store(&self) -> TraceStore {
TraceStore::open(self.runs_root())
}
/// The data archive root: `paths.data` inside a project when set,
/// the data-server default otherwise.
pub fn data_path(&self) -> String {
self.project
.as_ref()
.and_then(|p| p.toml.paths.data.clone())
.unwrap_or_else(|| data_server::DEFAULT_DATA_PATH.to_string())
}
pub fn provenance(&self) -> Option<ProjectProvenance> {
self.project.as_ref().map(|p| ProjectProvenance {
namespace: p.namespace.clone(),
dylib_sha256: p.dylib_sha256.clone(),
commit: p.commit.clone(),
})
}
}
/// Walk up from `start` to the nearest directory containing `Aura.toml`.
pub fn discover_from(start: &Path) -> Option<PathBuf> {
let mut dir = Some(start);
while let Some(d) = dir {
if d.join("Aura.toml").is_file() {
return Some(d.to_path_buf());
}
dir = d.parent();
}
None
}
fn parse_aura_toml(root: &Path) -> Result<AuraToml, ProjectError> {
let path = root.join("Aura.toml");
let text = std::fs::read_to_string(&path)
.map_err(|e| ProjectError::Toml(path.clone(), e.to_string()))?;
toml::from_str(&text).map_err(|e| ProjectError::Toml(path, e.to_string()))
}
/// Derive the cdylib artifact path from `cargo metadata` output (pure —
/// unit-testable on a canned JSON document).
fn artifact_from_metadata(
metadata_json: &str,
release: bool,
) -> Result<PathBuf, ProjectError> {
let v: serde_json::Value = serde_json::from_str(metadata_json)
.map_err(|e| ProjectError::CargoMetadata(e.to_string()))?;
let target_dir = v["target_directory"]
.as_str()
.ok_or_else(|| ProjectError::CargoMetadata("no target_directory".into()))?;
let packages = v["packages"]
.as_array()
.ok_or_else(|| ProjectError::CargoMetadata("no packages".into()))?;
let lib_name = packages
.iter()
.flat_map(|p| p["targets"].as_array().into_iter().flatten())
.find(|t| {
t["kind"]
.as_array()
.is_some_and(|k| k.iter().any(|s| s.as_str() == Some("cdylib")))
})
.and_then(|t| t["name"].as_str())
.ok_or_else(|| {
ProjectError::CargoMetadata(
"no cdylib target in the project crate (is `crate-type = \
[\"cdylib\"]` set?)"
.into(),
)
})?;
let profile = if release { "release" } else { "debug" };
let file = format!(
"{}{}{}",
std::env::consts::DLL_PREFIX,
lib_name.replace('-', "_"),
std::env::consts::DLL_SUFFIX
);
Ok(Path::new(target_dir).join(profile).join(file))
}
fn artifact_path(root: &Path, release: bool) -> Result<PathBuf, ProjectError> {
let out = std::process::Command::new("cargo")
.args(["metadata", "--format-version", "1", "--no-deps"])
.current_dir(root)
.output()
.map_err(|e| ProjectError::CargoMetadata(e.to_string()))?;
if !out.status.success() {
return Err(ProjectError::CargoMetadata(
String::from_utf8_lossy(&out.stderr).trim().to_string(),
));
}
artifact_from_metadata(&String::from_utf8_lossy(&out.stdout), release)
}
/// The vocabulary charter (decision log on the milestone reference issue):
/// non-empty namespace; every listed id `\<ns\>::`-prefixed; no duplicate in
/// the merged project ∪ std set; every listed id resolves (list↔resolver
/// cross-check). Pure — unit-testable without a dylib.
fn check_charter(
namespace: &str,
ids: &[&str],
resolve: &dyn Fn(&str) -> Option<PrimitiveBuilder>,
) -> Result<(), ProjectError> {
if namespace.is_empty() {
return Err(ProjectError::Charter("empty namespace".into()));
}
let prefix = format!("{namespace}::");
let mut seen = std::collections::BTreeSet::new();
for id in ids {
if !id.starts_with(&prefix) {
return Err(ProjectError::Charter(format!(
"type id `{id}` lacks the project prefix `{prefix}`"
)));
}
if !seen.insert(*id) {
return Err(ProjectError::Charter(format!("duplicate type id `{id}`")));
}
if std_vocabulary(id).is_some() || std_vocabulary_types().contains(id) {
return Err(ProjectError::Charter(format!(
"type id `{id}` collides with the std vocabulary"
)));
}
if resolve(id).is_none() {
return Err(ProjectError::Charter(format!(
"listed type id `{id}` does not resolve through the project \
resolver (list/resolver drift)"
)));
}
}
Ok(())
}
fn project_commit(root: &Path) -> Option<String> {
let head = std::process::Command::new("git")
.args(["-C"])
.arg(root)
.args(["rev-parse", "HEAD"])
.output()
.ok()
.filter(|o| o.status.success())?;
let mut sha = String::from_utf8_lossy(&head.stdout).trim().to_string();
let dirty = std::process::Command::new("git")
.args(["-C"])
.arg(root)
.args(["status", "--porcelain"])
.output()
.ok()
.filter(|o| o.status.success())?;
if !dirty.stdout.is_empty() {
sha.push_str("-dirty");
}
Some(sha)
}
/// Locate, load (load-and-hold), verify, and charter-check the project dylib.
pub fn load(root: &Path, release: bool) -> Result<ProjectEnv, ProjectError> {
let toml = parse_aura_toml(root)?;
let dylib_path = artifact_path(root, release)?;
if !dylib_path.is_file() {
return Err(ProjectError::ArtifactMissing(dylib_path));
}
let bytes = std::fs::read(&dylib_path)
.map_err(|e| ProjectError::Load(dylib_path.clone(), e.to_string()))?;
let dylib_sha256 = format!("{:x}", Sha256::digest(&bytes));
// Load-and-hold: leak the Library so every pointer read from the
// descriptor is valid for 'static. There is deliberately no unload path
// (one-shot process; ledger C13 note).
let lib = unsafe { libloading::Library::new(&dylib_path) }
.map_err(|e| ProjectError::Load(dylib_path.clone(), e.to_string()))?;
let lib: &'static libloading::Library = Box::leak(Box::new(lib));
let sym = unsafe { lib.get::<*const ProjectDescriptor>(AURA_PROJECT_SYMBOL) }
.map_err(|_| ProjectError::NotAProjectDylib(dylib_path.clone()))?;
let desc_ptr: *const ProjectDescriptor = *sym;
if desc_ptr.is_null() {
return Err(ProjectError::NotAProjectDylib(dylib_path));
}
// ---- C tier, validated front-to-back; no Rust-ABI field touched yet ----
let magic = unsafe { (*desc_ptr).magic };
if magic != AURA_DESCRIPTOR_MAGIC {
return Err(ProjectError::NotAProjectDylib(dylib_path));
}
let version = unsafe { (*desc_ptr).descriptor_version };
if version != AURA_DESCRIPTOR_VERSION {
return Err(ProjectError::Incompatible {
what: "descriptor version",
dylib: version.to_string(),
host: AURA_DESCRIPTOR_VERSION.to_string(),
});
}
let dylib_rustc = unsafe { (*desc_ptr).rustc_version.as_str() }
.ok_or_else(|| ProjectError::NotAProjectDylib(dylib_path.clone()))?;
if dylib_rustc != RUSTC_VERSION {
return Err(ProjectError::Incompatible {
what: "rustc version",
dylib: dylib_rustc.to_string(),
host: RUSTC_VERSION.to_string(),
});
}
let dylib_core = unsafe { (*desc_ptr).aura_core_version.as_str() }
.ok_or_else(|| ProjectError::NotAProjectDylib(dylib_path.clone()))?;
if dylib_core != CORE_VERSION {
return Err(ProjectError::Incompatible {
what: "aura-core version",
dylib: dylib_core.to_string(),
host: CORE_VERSION.to_string(),
});
}
let namespace = unsafe { (*desc_ptr).namespace.as_str() }
.ok_or_else(|| ProjectError::NotAProjectDylib(dylib_path.clone()))?
.to_string();
// ---- Rust tier: stamps match, the ABI is trusted from here on ----
let desc: &'static ProjectDescriptor = unsafe { &*desc_ptr };
let resolver = desc.vocabulary;
let type_id_list = (desc.type_ids)();
check_charter(&namespace, type_id_list, &|t| resolver(t))?;
Ok(ProjectEnv {
root: root.to_path_buf(),
toml,
namespace,
dylib_sha256,
commit: project_commit(root),
resolver,
type_id_list,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn discover_walks_up_to_aura_toml() {
let tmp = std::env::temp_dir().join(format!("aura-disc-{}", std::process::id()));
let nested = tmp.join("a/b/c");
std::fs::create_dir_all(&nested).unwrap();
assert_eq!(discover_from(&nested), None);
std::fs::write(tmp.join("Aura.toml"), "").unwrap();
assert_eq!(discover_from(&nested), Some(tmp.clone()));
assert_eq!(discover_from(&tmp), Some(tmp.clone()));
std::fs::remove_dir_all(&tmp).unwrap();
}
#[test]
fn aura_toml_parses_empty_partial_and_unknown_keys() {
let t: AuraToml = toml::from_str("").unwrap();
assert_eq!(t, AuraToml::default());
let t: AuraToml = toml::from_str("[paths]\nruns = \"r\"").unwrap();
assert_eq!(t.paths.runs, Some(PathBuf::from("r")));
assert_eq!(t.paths.data, None);
// unknown keys tolerated (forward-compat; serde default is lenient)
let t: AuraToml = toml::from_str("[future]\nx = 1").unwrap();
assert_eq!(t, AuraToml::default());
}
#[test]
fn artifact_path_derives_from_metadata_json() {
let json = r#"{
"target_directory": "/tmp/proj/target",
"packages": [{"targets": [
{"kind": ["cdylib"], "name": "demo-project"}
]}]
}"#;
let p = artifact_from_metadata(json, false).unwrap();
let expect = format!(
"/tmp/proj/target/debug/{}demo_project{}",
std::env::consts::DLL_PREFIX,
std::env::consts::DLL_SUFFIX
);
assert_eq!(p, PathBuf::from(expect));
let p = artifact_from_metadata(json, true).unwrap();
assert!(p.to_string_lossy().contains("/release/"));
// no cdylib target -> named error
let bad = r#"{"target_directory":"/t","packages":[{"targets":[{"kind":["lib"],"name":"x"}]}]}"#;
assert!(matches!(
artifact_from_metadata(bad, false),
Err(ProjectError::CargoMetadata(_))
));
}
fn none_resolver(_: &str) -> Option<PrimitiveBuilder> {
None
}
#[test]
fn charter_rejects_each_violation_and_accepts_valid() {
let ok_resolver = |t: &str| {
if t == "p::A" || t == "p::B" { std_vocabulary("SMA") } else { None }
};
// valid
assert!(check_charter("p", &["p::A", "p::B"], &ok_resolver).is_ok());
// empty namespace
assert!(matches!(
check_charter("", &[], &none_resolver),
Err(ProjectError::Charter(_))
));
// unprefixed id
let e = check_charter("p", &["A"], &ok_resolver).unwrap_err();
assert!(e.to_string().contains("lacks the project prefix"));
// duplicate id
let e = check_charter("p", &["p::A", "p::A"], &ok_resolver).unwrap_err();
assert!(e.to_string().contains("duplicate"));
// list/resolver drift
let e = check_charter("p", &["p::C"], &ok_resolver).unwrap_err();
assert!(e.to_string().contains("does not resolve"));
}
#[test]
fn env_std_collapses_to_current_defaults() {
let env = Env::std();
assert_eq!(env.runs_root(), PathBuf::from("runs"));
assert_eq!(env.data_path(), data_server::DEFAULT_DATA_PATH.to_string());
assert!(env.provenance().is_none());
assert!(env.resolve("SMA").is_some());
assert!(env.resolve("demo::Identity").is_none());
assert!(env.type_ids().contains(&"SMA"));
}
}
- Step 3: Register the module
In crates/aura-cli/src/main.rs, beside the existing mod graph_construct;
line near the top of the file, add:
mod project;
- Step 4: Build and unit-test the module
Run: cargo test -p aura-cli project::
Expected: PASS — discover_walks_up_to_aura_toml,
aura_toml_parses_empty_partial_and_unknown_keys,
artifact_path_derives_from_metadata_json,
charter_rejects_each_violation_and_accepts_valid,
env_std_collapses_to_current_defaults.
Task 5: threading fan-out (&Env through the verb chains)
Files:
- Modify:
crates/aura-cli/src/main.rs(sites enumerated per step) - Modify:
crates/aura-cli/src/graph_construct.rs:105-200
All edits in this one task — the compile gate at the end is unsatisfiable until every caller is threaded (self-review check 7).
- Step 1: global
--releaseflag + Env construction inmain()
Cli struct (main.rs:3669-3674) gains one field after command:
/// Load the project dylib from target/release instead of target/debug.
#[arg(long, global = true)]
release: bool,
In fn main() (main.rs:4561), after Cli::parse() and before the dispatch
match (4571), insert:
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(),
};
and thread &env into every arm of the dispatch match: each
dispatch_X(cmd) becomes dispatch_X(cmd, &env).
- Step 2: dispatch signatures
Each of the nine dispatch fns gains a trailing env: &project::Env
parameter (signature edit only, one line each):
dispatch_run (4170), dispatch_chart (4241), dispatch_graph (4249),
dispatch_generalize (4257), dispatch_runs (4265), dispatch_reproduce
(4279), dispatch_sweep (4285), dispatch_walkforward (4383),
dispatch_mc (4462).
- Step 3: thread the resolver chains
Each fn below gains a trailing env: &project::Env parameter, and every
&|t| std_vocabulary(t) inside it becomes &|t| env.resolve(t); callers
pass env down:
reproduce_family_in(fn 2199; site 2233) ←reproduce_family(2305) ←dispatch_reproduceblueprint_axis_probe(fn 2880; site 2881) ←list_blueprint_axes(2893),dispatch_run(4201), sweep/wf/mc probesblueprint_sweep_family(fn 2914; site 2923),blueprint_sweep_over(fn 2971; site 2975),run_oos_blueprint(fn 2999; site 3003),blueprint_walkforward_family(fn 3018; site 3031),blueprint_mc_family(fn 3082; site 3084),run_blueprint_sweep(fn 3153; site 3170),run_blueprint_walkforward(fn 3196; site 3207),run_blueprint_mc(fn 3226; site 3240)- direct dispatch sites: 4194 (
dispatch_run), 4294 (dispatch_sweep), 4391 (dispatch_walkforward), 4470 (dispatch_mc)
In graph_construct.rs: build_from_str (109), introspect_node (150),
introspect_unwired (168) gain env: &crate::project::Env; sites 113/151/170
swap std_vocabulary → |t| env.resolve(t) (matching each site's existing
call form); the --vocabulary listing at 196 iterates env.type_ids()
instead of std_vocabulary_types(); build_cmd (127) and introspect_cmd
(184) thread env from dispatch_graph.
Test-code and fieldtest callers of these fns (e.g. main.rs:5592-5848) pass
&project::Env::std().
-
Step 4: registry, trace-store, and data-path sites
-
Delete
fn default_registry()(main.rs:1513-1515). Replace its 10 call sites (1618, 1673, 1695, 2014, 2100, 2120, 2306, 3159, 3198, 3231) withenv.registry(), threadingenvinto each enclosing fn that lacks it. -
Replace the 10
TraceStore::open("runs")sites (201, 503, 559, 598, 1613, 1690, 2009, 2408, 3488, 3588) withenv.trace_store(), threadingenvlikewise (the two write-side helpers around 201/3588 gain anenvparam). -
Replace the 4 production
data_server::DEFAULT_DATA_PATHuses (660, 677, 718, 742) withenv.data_path()(theDataServer::new(...)constructions at 718/742 takeenv.data_path()); test-only uses stay. -
Step 5: compile gate + suite
Run: cargo build --workspace
Expected: 0 errors (every threaded caller updated in this task).
Run: cargo test --workspace
Expected: PASS, all pre-existing tests unchanged — outside a project every
Env::std() accessor returns today's literals, so goldens and manifest-shape
pins (cli_run.rs:34-39) stay byte-identical.
Task 6: provenance stamping on the blueprint-run paths
Files:
-
Modify:
crates/aura-cli/src/main.rs(three stamp spots) -
Step 1: stamp beside
topology_hash
At each spot where topology_hash is stamped today, add the project stamp
directly below it:
run_signal_r(stamps at 2821): afterreport.manifest.topology_hash = Some(...);addreport.manifest.project = env.provenance();run_blueprint_member(manifest built 2853, stamped 2855): after thetopology_hashstamp addmanifest.project = env.provenance();run_r_sma(stamps at 3554): after the stamp addreport.manifest.project = env.provenance();
Thread env into these fns if Task 5 did not already reach them
(run_blueprint_member at 2834 receives it from its reload-closure callers).
- Step 2: verify outside-project bytes unchanged
Run: cargo test -p aura-cli --test cli_run
Expected: PASS — env.provenance() is None outside a project, the key is
skipped, stdout pins unchanged.
Task 7: fixture project + e2e proof
Files:
-
Create:
crates/aura-cli/tests/fixtures/demo-project/Cargo.toml -
Create:
crates/aura-cli/tests/fixtures/demo-project/Aura.toml -
Create:
crates/aura-cli/tests/fixtures/demo-project/.gitignore -
Create:
crates/aura-cli/tests/fixtures/demo-project/src/lib.rs -
Create:
crates/aura-cli/tests/fixtures/demo-project/demo_signal.json -
Create:
crates/aura-cli/tests/project_load.rs -
Step 1: fixture
Cargo.toml
[package]
name = "demo-project"
version = "0.1.0"
edition = "2024"
publish = false
[lib]
crate-type = ["cdylib"]
[dependencies]
aura-core = { path = "../../../../aura-core" }
# Standalone workspace root: never a member of the engine workspace
# (docs/project-layout.md, the empty-[workspace] note).
[workspace]
- Step 2: fixture
Aura.tomland.gitignore
Aura.toml:
# demo fixture — static project context only (C17); paths only (cycle 0102).
[paths]
runs = "runs"
.gitignore:
/target
Cargo.lock
/runs
- Step 3: fixture
src/lib.rs
//! Fixture project for the load-boundary e2e (cycle 0102): one pass-through
//! node under the `demo` namespace, exported via the descriptor macro.
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(
"demo::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 {
"demo::Identity".to_string()
}
}
fn vocabulary(type_id: &str) -> Option<PrimitiveBuilder> {
match type_id {
"demo::Identity" => Some(Identity::builder()),
_ => None,
}
}
fn type_ids() -> &'static [&'static str] {
&["demo::Identity"]
}
aura_core::aura_project! {
namespace: "demo",
vocabulary: vocabulary,
type_ids: type_ids,
}
- Step 4: fixture
demo_signal.json
tests/fixtures/sma_signal.json extended by one demo::Identity stage on the
bias output (the graph genuinely needs the project vocabulary to load):
{"format_version":1,"blueprint":{"name":"demo_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":"demo::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"}]}}
- Step 5: e2e tests
crates/aura-cli/tests/project_load.rs
//! E2E: the project-as-crate load boundary (cycle 0102). Builds the fixture
//! project once (cargo, same toolchain, path-dep on this workspace's
//! aura-core), then drives the aura binary from inside the fixture dir.
//! `aura run` persists nothing without --trace, so the tracked fixture stays
//! clean (target/, Cargo.lock, runs/ are fixture-gitignored).
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
use std::sync::OnceLock;
fn fixture_dir() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/demo-project")
}
fn built_fixture() -> &'static PathBuf {
static BUILT: OnceLock<PathBuf> = OnceLock::new();
BUILT.get_or_init(|| {
let dir = fixture_dir();
let out = Command::new("cargo")
.arg("build")
.current_dir(&dir)
.output()
.expect("spawn cargo build for the fixture project");
assert!(
out.status.success(),
"fixture build failed:\n{}",
String::from_utf8_lossy(&out.stderr)
);
dir
})
}
fn aura(args: &[&str], cwd: &Path) -> Output {
Command::new(env!("CARGO_BIN_EXE_aura"))
.args(args)
.current_dir(cwd)
.output()
.expect("run aura")
}
#[test]
fn project_run_resolves_demo_node_and_is_bit_identical() {
let dir = built_fixture();
let a = aura(&["run", "demo_signal.json"], dir);
assert!(
a.status.success(),
"run failed: {}",
String::from_utf8_lossy(&a.stderr)
);
let b = aura(&["run", "demo_signal.json"], dir);
assert!(b.status.success());
assert_eq!(a.stdout, b.stdout, "two runs must be byte-identical (C1)");
}
#[test]
fn project_run_manifest_carries_provenance() {
let dir = built_fixture();
let out = aura(&["run", "demo_signal.json"], dir);
assert!(out.status.success());
let v: serde_json::Value =
serde_json::from_slice(&out.stdout).expect("run report is JSON");
let p = &v["manifest"]["project"];
assert_eq!(p["namespace"], "demo");
let sha = p["dylib_sha256"].as_str().expect("hash present");
assert_eq!(sha.len(), 64, "sha256 hex");
}
#[test]
fn introspect_vocabulary_lists_project_types() {
let dir = built_fixture();
let out = aura(&["graph", "introspect", "--vocabulary"], dir);
assert!(out.status.success());
let text = String::from_utf8_lossy(&out.stdout);
assert!(text.contains("demo::Identity"), "project id listed");
assert!(text.contains("SMA"), "std ids still listed");
}
#[test]
fn outside_a_project_the_demo_blueprint_is_unknown() {
// cwd = the aura-cli crate dir: no Aura.toml anywhere up the tree.
let cwd = Path::new(env!("CARGO_MANIFEST_DIR"));
let bp = fixture_dir().join("demo_signal.json");
let out = aura(&["run", bp.to_str().unwrap()], cwd);
assert!(!out.status.success());
let err = String::from_utf8_lossy(&out.stderr);
assert!(err.contains("UnknownNodeType"), "stderr: {err}");
}
#[test]
fn missing_artifact_refuses_with_build_hint() {
// A minimal never-built project: valid cargo metadata, no dylib on disk.
let tmp = std::env::temp_dir().join(format!("aura-nobuild-{}", std::process::id()));
std::fs::create_dir_all(tmp.join("src")).unwrap();
std::fs::write(tmp.join("Aura.toml"), "").unwrap();
std::fs::write(tmp.join("src/lib.rs"), "").unwrap();
std::fs::write(
tmp.join("Cargo.toml"),
"[package]\nname = \"nobuild\"\nversion = \"0.1.0\"\nedition = \"2024\"\n\n[lib]\ncrate-type = [\"cdylib\"]\n\n[workspace]\n",
)
.unwrap();
let out = aura(&["run", "x.json"], &tmp);
assert_eq!(out.status.code(), Some(1), "runtime refusal");
let err = String::from_utf8_lossy(&out.stderr);
assert!(err.contains("cargo build"), "build hint present: {err}");
std::fs::remove_dir_all(&tmp).ok();
}
- Step 6: run the e2e suite
Run: cargo test -p aura-cli --test project_load
Expected: PASS, all five tests (first run pays the fixture cargo build).
Task 8: docs and ledger alignment
Files:
-
Modify:
docs/project-layout.md:33-34 -
Modify:
docs/glossary.md:20-21 -
Modify:
docs/design/INDEX.md(C13 block ~1046-1055; C16 1148-1149; C17 1209-1210) -
Step 1: project-layout.md
Lines 33-34, before:
├── Aura.toml # STATIC context only: data paths, instrument/pip
│ # metadata, default broker & window, runs dir (no logic)
After:
├── Aura.toml # STATIC context only, paths only: data archive root,
│ # runs dir (no logic; instrument geometry is the
│ # recorded sidecar, C15 — never authored here)
- Step 2: glossary
docs/glossary.md:20-21 (the Aura.toml entry body), before:
The per-project declarative config holding only static context (data paths, instrument/pip metadata, default broker & window, runs dir), never logic. Its presence marks the project root, the way `Cargo.toml` marks a cargo crate.
After:
The per-project declarative config holding only static context — paths only: the data archive root and the runs dir — never logic, and never instrument geometry (that is the recorded sidecar, C15). Its presence marks the project root, the way `Cargo.toml` marks a cargo crate.
- Step 3: ledger C13 realization note
In docs/design/INDEX.md, at the end of the C13 block (after its **Why.**
paragraph, ~line 1055), append:
**Realization (cycle 0102 — the load boundary; per-invocation reload).** The
authoring-loop half is realized: a project is an external cdylib crate loaded
per invocation through a two-tier `#[repr(C)]` descriptor (`AURA_PROJECT`,
`aura-core::project`) — a C-ABI stamp prefix (rustc version + aura-core
version, baked per consuming build) validated **before** any Rust-ABI field is
touched, then the vocabulary resolver + enumerable type-id list behind the
stamp gate. "Hot-reload" reads, in v1, as **per-invocation load of the
freshest build**: the author (Claude) runs `cargo build`, the next `aura`
invocation locates the artifact via `cargo metadata` (debug default,
`--release` opt-in) and loads it **load-and-hold** (leaked, never unloaded).
Scope boundary: load-and-hold is trivially sound only because the CLI is a
one-shot process — a future long-running host (the open local-server thread,
C22) must re-solve reload (host restart or subprocess isolation), never
in-process unload. Mismatch of either stamp refuses (exit 1) naming both
sides; the project vocabulary is charter-checked at load (`::`-namespaced ids,
no duplicates against std, list↔resolver cross-check) — the invariant-9
data-plane discipline of the C24 enforcement-shift note, now enforced at the
one seam.
- Step 4: C16 and C17 field lists
C16 (lines 1148-1149), before:
strategy / experiment blueprints — plus a static `Aura.toml` (project context:
data paths, instrument/pip metadata, default broker & window, runs dir). During
After:
strategy / experiment blueprints — plus a static `Aura.toml` (project context,
paths only: data archive root, runs dir — cycle 0102). During
C17 (lines 1209-1211), before:
and reports metrics. Declarative config (`Aura.toml`) carries only **static
project context** (data paths, instrument/pip metadata, defaults, runs dir),
never logic. aura ships **no embedded coding-LLM**. IONOS LLMs are used only as a
After:
and reports metrics. Declarative config (`Aura.toml`) carries only **static
project context** (paths only: data archive root, runs dir — cycle 0102),
never logic. aura ships **no embedded coding-LLM**. IONOS LLMs are used only as a
- Step 5: open-threads entry (the schema is now designed)
docs/design/INDEX.md lines 2058-2063, before:
- **`aura new` scaffolder, the experiment-builder API, and `Aura.toml`'s
static-context schema** — `aura new` scaffolds a Rust project *crate* (node /
strategy / experiment blueprints) against the engine (C16/C20); the
experiment-builder API surface (harness wiring, structural axes, sweep
combinators) and `Aura.toml`'s schema (data paths, instrument/pip metadata,
default broker & window, runs dir) are not yet designed.
After:
- **`aura new` scaffolder and the experiment-builder API** — `aura new`
scaffolds a Rust project *crate* (node / strategy blueprints) against the
engine (C16/C20); the experiment-builder API surface (harness wiring,
structural axes, sweep combinators) is not yet designed. `Aura.toml`'s
schema was settled paths-only in cycle 0102 (data archive root, runs dir —
see the C13 realization note); the load boundary itself shipped there.
- Step 6: verify the lockstep
Run: grep -rn "instrument/pip" docs/project-layout.md docs/glossary.md docs/design/INDEX.md | grep -i "aura.toml" | wc -l
Expected: 0 (no Aura.toml context still lists instrument/pip).
Task 9: full regression gate
Files: none (verification only)
- Step 1: workspace build + suite
Run: cargo build --workspace
Expected: 0 errors, 0 warnings introduced.
Run: cargo test --workspace
Expected: PASS — every pre-existing test green unchanged, plus the new
aura-core project tests, node_name strip test, manifest round-trip test,
aura-cli project unit tests, and the five project_load 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 (or the pre-existing count, unchanged).
- Step 3: no stray literals
Run: grep -n 'TraceStore::open("runs")' crates/aura-cli/src/main.rs | wc -l
Expected: 0 (all ten sites route through env.trace_store()).
Run: grep -n 'fn default_registry' crates/aura-cli/src/main.rs | wc -l
Expected: 0 (retired in favour of env.registry()).