feat(aura-core,aura-std,aura-engine,aura-cli): inject a param-set vector at bootstrap (#31)

Cycle B of milestone "The World — parameter-space & sweep". A blueprint is now
value-empty: a leaf holds a param-generic recipe, not a built node, and a positional
Scalar vector is bound slot-by-slot at bootstrap — so one blueprint bootstraps into
many distinct instances under different vectors, with no cdylib rebuild (C12/C19).
This is the binding primitive a sweep (#32) drives.

Ratified design (brainstorm): value-empty reconstruct-through-new() over
mutate-in-place and over a default-bearing variant. The value lives only in the
injected vector (no baked default), keeping the blueprint a pure param-generic
recipe (C19); every injected value flows through the node's own constructor (the
single sizing/validation gate).

- aura-core: LeafFactory { name, params, build } — the recipe (params -> sized node
  through `new`); Scalar::as_i64/as_f64 value accessors. Node trait unchanged.
- aura-std: each of the 7 nodes exposes factory() (SMA length:I64, Exposure
  scale:F64, LinComb arity x weights[i]:F64; Sub/Add/SimBroker/Recorder paramless,
  capturing their non-param construction args — pip_size, the Recorder channel).
- aura-engine: BlueprintNode::Leaf(LeafFactory), From<LeafFactory>; param_space()
  reads factory.params() pre-build; compile_with_params/bootstrap_with_params build
  each leaf from its kind-checked slice while lowering (build-then-wire), arity
  checked up front via param_space().len(); CompileError::{ParamKindMismatch,
  ParamArity}. compile/inline/edge-rewrite are structurally unchanged, so the
  compilat stays bit-identical for a given point (the bit-identity and both
  param_space mirror tests stay green). The vestigial pre-build Composite::schema /
  BlueprintNode::schema (no live caller — interface resolution is structural on the
  built flat nodes) are removed.
- aura-cli: the blueprint view labels leaves by bare type via LeafFactory::label()
  (`[SMA]`) — the ascii-dag renderer cannot render wide cluster-sibling labels (see
  spec); the compiled view still labels valued (`SMA(2)`). The sample supplies its
  point as a vector; the mis-wiring swap moved to the compiled view.

The #34 dual-traversal drift hazard is subsumed: compile_with_params consumes the
vector in the same recipe walk param_space() reports, so the two share one
traversal.

Verified: cargo build/test --workspace green (127 tests, incl. bit-identity, both
mirror tests, and 4 new injection tests — different-vector-different-run, kind
mismatch, arity, determinism); clippy --workspace --all-targets -D warnings clean;
`aura graph` blueprint view renders cleanly.

Scope: one vector -> one instance. Deferred: sweep enumeration (#32), domain
validation (#32/C20), single-run authoring convenience (#35).

closes #31
This commit is contained in:
2026-06-07 21:04:52 +02:00
parent b02f31cdd4
commit 4b64409036
13 changed files with 469 additions and 168 deletions
+4 -4
View File
@@ -57,18 +57,18 @@ pub fn render_blueprint(bp: &Blueprint) -> String {
// interior edges (the interior ids are in hand here).
for item in bp.nodes() {
match item {
BlueprintNode::Leaf(node) => {
BlueprintNode::Leaf(factory) => {
let id = labels.len();
labels.push(node.label());
labels.push(factory.label());
item_display.push(ItemDisplay::Leaf(id));
}
BlueprintNode::Composite(c) => {
let mut interior_ids = Vec::with_capacity(c.nodes().len());
for inner in c.nodes() {
match inner {
BlueprintNode::Leaf(node) => {
BlueprintNode::Leaf(factory) => {
let id = labels.len();
labels.push(node.label());
labels.push(factory.label());
interior_ids.push(id);
}
BlueprintNode::Composite(_) => unimplemented!(
+70 -50
View File
@@ -116,11 +116,12 @@ fn run_sample() -> RunReport {
/// The SMA-cross signal as a named composite (price -> fast/slow SMA -> spread).
/// CLI-local sample builder; the engine ships no sample (the duplication with
/// `blueprint.rs`'s test helper is the dedup tracked in #14).
fn sma_cross(name: &str, fast: usize, slow: usize) -> Composite {
/// `blueprint.rs`'s test helper is the dedup tracked in #14). Value-empty: the SMA
/// lengths are injected at compile, not baked here.
fn sma_cross(name: &str) -> Composite {
Composite::new(
name,
vec![Sma::new(fast).into(), Sma::new(slow).into(), Sub::new().into()],
vec![Sma::factory().into(), Sma::factory().into(), Sub::factory().into()],
vec![
Edge { from: 0, to: 2, slot: 0, from_field: 0 },
Edge { from: 1, to: 2, slot: 1, from_field: 0 },
@@ -130,19 +131,20 @@ fn sma_cross(name: &str, fast: usize, slow: usize) -> Composite {
)
}
/// The sample signal-quality blueprint, parameterized by the SMA windows so a
/// test can author a deliberately swapped variant. Recorders need a channel to
/// construct; the receivers are dropped because the render never runs the graph.
fn build_sample(fast: usize, slow: usize) -> Blueprint {
/// The sample signal-quality blueprint (value-empty): a recipe whose SMA lengths +
/// exposure scale are injected at compile via the point vector (see
/// `sample_point`). Recorders need a channel to construct; the receivers are
/// dropped because the render never runs the graph.
fn build_sample() -> Blueprint {
let (tx_eq, _rx_eq) = mpsc::channel();
let (tx_ex, _rx_ex) = mpsc::channel();
Blueprint::new(
vec![
BlueprintNode::Composite(sma_cross("sma_cross", fast, slow)),
Exposure::new(0.5).into(),
SimBroker::new(0.0001).into(),
Recorder::new(&[ScalarKind::F64], Firing::Any, tx_eq).into(),
Recorder::new(&[ScalarKind::F64], Firing::Any, tx_ex).into(),
BlueprintNode::Composite(sma_cross("sma_cross")),
Exposure::factory().into(),
SimBroker::factory(0.0001).into(),
Recorder::factory(vec![ScalarKind::F64], Firing::Any, tx_eq).into(),
Recorder::factory(vec![ScalarKind::F64], Firing::Any, tx_ex).into(),
],
vec![SourceSpec {
kind: ScalarKind::F64,
@@ -162,7 +164,13 @@ fn build_sample(fast: usize, slow: usize) -> Blueprint {
/// The built-in sample rendered by `aura graph`.
fn sample_blueprint() -> Blueprint {
build_sample(2, 4)
build_sample()
}
/// The point vector injected into the sample blueprint, in `param_space()` slot
/// order: `[fast SMA length, slow SMA length, exposure scale]`.
fn sample_point() -> Vec<Scalar> {
vec![Scalar::I64(2), Scalar::I64(4), Scalar::F64(0.5)]
}
fn main() {
@@ -177,7 +185,8 @@ fn main() {
let compiled = args.next().as_deref() == Some("--compiled");
let bp = sample_blueprint();
let out = if compiled {
let (nodes, sources, edges) = bp.compile().expect("valid sample blueprint");
let (nodes, sources, edges) =
bp.compile_with_params(&sample_point()).expect("valid sample blueprint");
graph::render_compilat(&nodes, &sources, &edges)
} else {
graph::render_blueprint(&bp)
@@ -196,21 +205,23 @@ fn main() {
mod tests {
use super::*;
/// The sample authored with fast/slow SMA windows swapped — the mis-wiring
/// the render must surface. Test-only: nothing outside tests builds it.
fn sample_blueprint_swapped() -> Blueprint {
build_sample(4, 2)
/// The sample's point vector with the fast/slow SMA lengths swapped — the
/// mis-wiring the compiled render must surface. The blueprint is param-generic
/// and identical for both orderings; only the injected vector differs.
fn swapped_point() -> Vec<Scalar> {
vec![Scalar::I64(4), Scalar::I64(2), Scalar::F64(0.5)]
}
#[test]
fn blueprint_view_shows_cluster_and_param_labels() {
fn blueprint_view_shows_cluster_and_param_generic_labels() {
let out = graph::render_blueprint(&sample_blueprint());
// the composite renders as a named cluster box
assert!(out.contains("sma_cross"), "missing composite name:\n{out}");
// param-carrying labels disambiguate the two SMAs
assert!(out.contains("SMA(2)"), "missing SMA(2):\n{out}");
assert!(out.contains("SMA(4)"), "missing SMA(4):\n{out}");
for needle in ["Sub", "Exposure(0.5)", "SimBroker(0.0001)", "Recorder"] {
// the value-empty blueprint view labels leaves param-generically by bare
// type (no values, and no knob suffix — the ascii-dag layout cannot render
// wide cluster-sibling labels); both SMAs render identically as [SMA]
assert!(out.contains("[SMA]"), "missing [SMA]:\n{out}");
for needle in ["Sub", "Exposure", "SimBroker", "Recorder"] {
assert!(out.contains(needle), "missing {needle}:\n{out}");
}
}
@@ -218,7 +229,8 @@ mod tests {
#[test]
fn compiled_view_dissolves_the_composite_boundary() {
let bp = sample_blueprint();
let (nodes, sources, edges) = bp.compile().expect("valid sample");
let (nodes, sources, edges) =
bp.compile_with_params(&sample_point()).expect("valid sample");
let out = graph::render_compilat(&nodes, &sources, &edges);
// node labels survive inlining...
assert!(out.contains("SMA(2)") && out.contains("SMA(4)"), "labels lost:\n{out}");
@@ -227,11 +239,18 @@ mod tests {
}
#[test]
fn swapped_sma_inputs_render_differently() {
fn swapped_param_vector_changes_the_compiled_render() {
// the property the cycle exists to buy: a mis-wiring is no longer invisible.
let correct = graph::render_blueprint(&sample_blueprint());
let swapped = graph::render_blueprint(&sample_blueprint_swapped());
assert_ne!(correct, swapped, "a fast/slow SMA swap must change the render");
// The blueprint is now param-generic (identical for both orderings), so the
// swap is observable only after the vector is injected — in the COMPILED
// view, where the flat nodes carry their valued labels (SMA(2)/SMA(4)).
let (cn, cs, ce) =
sample_blueprint().compile_with_params(&sample_point()).expect("valid sample");
let correct = graph::render_compilat(&cn, &cs, &ce);
let (sn, ss, se) =
sample_blueprint().compile_with_params(&swapped_point()).expect("valid sample");
let swapped = graph::render_compilat(&sn, &ss, &se);
assert_ne!(correct, swapped, "a fast/slow SMA swap must change the compiled render");
}
#[test]
@@ -239,27 +258,27 @@ mod tests {
let out = graph::render_blueprint(&sample_blueprint());
// ascii-dag's Sugiyama layout is deterministic (no RNG); these are the
// exact bytes `aura graph` emits (render() ends in three newlines).
let expected = r#" [source:F64]
┌─────────└┐────────┐
╔═════╪══════════╪═══╗ │
║ sma_cross ║ │
║ ↓ ║ │
║ [SMA(2)] [SMA(4)] ║ │
└──┌──────┘ ║ │
║ ↓ ┌─╫───┘
║ [Sub]
║ │
╚════════╪══════════╪═
[Exposure(0.5)] │
┌───────┘────────┐ │
↓─┘
[Recorder] [SimBroker(0.0001)]
[Recorder]
let expected = r#" [source:F64]
───────└───────┐
│ │
╔═══╪═══════╪═══╗ │
║ sma_cross ║ │
║ ↓ ↓ ║ │
║ [SMA] [SMA] ║ │
└──────┘ ║ │
║ ↓ ║┌──┘
║ [Sub] ║
║ │ ║
╚════════╪══════╝
[Exposure] │
┌───┘───────┼┐
└↓
[Recorder] [SimBroker]
[Recorder]
"#;
@@ -269,7 +288,8 @@ mod tests {
#[test]
fn compiled_view_golden() {
let bp = sample_blueprint();
let (nodes, sources, edges) = bp.compile().expect("valid sample");
let (nodes, sources, edges) =
bp.compile_with_params(&sample_point()).expect("valid sample");
let out = graph::render_compilat(&nodes, &sources, &edges);
let expected = r#" [source:F64]
┌────────└─┐──────┐