Files
Aura/docs/plans/0102-project-load-boundary.md
T
Brummel af5f825d60 plan: 0102 project-as-crate load boundary
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
2026-07-02 15:44:05 +02:00

51 KiB
Raw Blame History

The project-as-crate load boundary — Implementation Plan

Parent spec: docs/specs/0102-project-load-boundary.md

For agentic workers: REQUIRED SUB-SKILL: use the implement skill 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 — bake AURA_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-38pub mod project;
  • Modify: crates/aura-core/src/node.rs:143-147 — namespace strip
  • Modify: crates/aura-engine/src/report.rs:58-60,270-332project field + struct + legacy test
  • Modify: 21 further RunManifest { literals (enumerated in Task 3)
  • Modify: crates/aura-cli/Cargo.toml — add libloading, 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: None through 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 the mod 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 --release flag + Env construction in main()

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_reproduce
  • blueprint_axis_probe (fn 2880; site 2881) ← list_blueprint_axes (2893), dispatch_run (4201), sweep/wf/mc probes
  • blueprint_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) with env.registry(), threading env into each enclosing fn that lacks it.

  • Replace the 10 TraceStore::open("runs") sites (201, 503, 559, 598, 1613, 1690, 2009, 2408, 3488, 3588) with env.trace_store(), threading env likewise (the two write-side helpers around 201/3588 gain an env param).

  • Replace the 4 production data_server::DEFAULT_DATA_PATH uses (660, 677, 718, 742) with env.data_path() (the DataServer::new(...) constructions at 718/742 take env.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): after report.manifest.topology_hash = Some(...); add report.manifest.project = env.provenance();
  • run_blueprint_member (manifest built 2853, stamped 2855): after the topology_hash stamp add manifest.project = env.provenance();
  • run_r_sma (stamps at 3554): after the stamp add report.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.toml and .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()).