plan: 0016 param-set injection (#31)
Four crate-gated tasks for spec 0016: aura-core (LeafFactory + Scalar accessors), aura-std (factory() on the 7 nodes), aura-engine (value-empty Leaf, build-then-wire compile_with_params/bootstrap_with_params, kind+arity errors, vestigial schema removal, fixtures + injection tests re-expressed), aura-cli (param-generic render, sample + goldens, swap moved to the compiled view). Compile gates are crate-scoped so each task's gate is satisfiable with all its callers threaded in-task. refs #31
This commit is contained in:
@@ -0,0 +1,696 @@
|
||||
# Param-set injection — Implementation Plan
|
||||
|
||||
> **Parent spec:** `docs/specs/0016-param-set-injection.md`
|
||||
>
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: use the `implement` skill to run
|
||||
> this plan. Steps use `- [ ]` checkboxes for tracking.
|
||||
|
||||
**Goal:** Make a blueprint value-empty — a leaf is a `LeafFactory` recipe
|
||||
(`params → sized node`) — and bind a positional `Scalar` vector at bootstrap via a
|
||||
new build-then-wire compile path, with kind + arity checks.
|
||||
|
||||
**Architecture:** `LeafFactory { name, params, build }` lands in aura-core; the 7
|
||||
aura-std nodes expose `factory()`; aura-engine's `BlueprintNode::Leaf` becomes a
|
||||
factory, `compile_with_params`/`bootstrap_with_params` build each leaf from its
|
||||
kind-checked param slice while lowering (the existing structural inline/edge/source
|
||||
rewrite is unchanged), and the vestigial pre-build `schema` methods are removed;
|
||||
aura-cli's blueprint render reads the param-generic `LeafFactory::label()` and the
|
||||
sample/goldens are re-expressed against the vector.
|
||||
|
||||
**Tech Stack:** Rust workspace — aura-core (Node/Scalar contract), aura-std (nodes),
|
||||
aura-engine (blueprint/compile/harness), aura-cli (run/graph faces).
|
||||
|
||||
**Sequencing (compile gates):** The `Leaf(LeafFactory)` change breaks every
|
||||
blueprint-leaf author site until repaired, so tasks are gated crate-by-crate:
|
||||
Task 1 `cargo build -p aura-core`, Task 2 `-p aura-std`, Task 3 `cargo build -p
|
||||
aura-engine --all-targets`, Task 4 `cargo build -p aura-cli --all-targets` then
|
||||
`cargo test --workspace`. Tasks 1–2 are purely additive (no breakage); Task 3 is
|
||||
the breaking change and repairs every aura-engine site (incl. its own tests) in
|
||||
one task so its compile gate is satisfiable; Task 4 repairs aura-cli + runs the
|
||||
workspace gate.
|
||||
|
||||
---
|
||||
|
||||
## Files this plan creates or modifies
|
||||
|
||||
- Modify: `crates/aura-core/src/node.rs` — add `LeafFactory`.
|
||||
- Modify: `crates/aura-core/src/scalar.rs` — add `as_i64`/`as_f64`.
|
||||
- Modify: `crates/aura-core/src/lib.rs` — re-export `LeafFactory`.
|
||||
- Modify: `crates/aura-std/src/{sma,exposure,lincomb,sub,add,sim_broker,recorder}.rs`
|
||||
— each gains `fn factory(...)` + a factory↔schema params test.
|
||||
- Modify: `crates/aura-engine/src/blueprint.rs` — `Leaf(LeafFactory)`,
|
||||
`From<LeafFactory>`, `collect_params`, `compile_with_params`,
|
||||
`bootstrap_with_params`, `CompileError` variants, remove vestigial `schema`
|
||||
methods + their test, re-express fixtures + tests.
|
||||
- Modify: `crates/aura-cli/src/graph.rs` — `render_blueprint` uses
|
||||
`LeafFactory::label()`.
|
||||
- Modify: `crates/aura-cli/src/main.rs` — sample blueprint → factories + vector;
|
||||
param-form call sites; re-capture blueprint-view goldens; move the swap to the
|
||||
compiled view.
|
||||
|
||||
---
|
||||
|
||||
## Task 1: aura-core — `LeafFactory` + `Scalar` accessors
|
||||
|
||||
**Files:**
|
||||
- Modify: `crates/aura-core/src/node.rs`
|
||||
- Modify: `crates/aura-core/src/scalar.rs`
|
||||
- Modify: `crates/aura-core/src/lib.rs`
|
||||
|
||||
- [ ] **Step 1: Add `LeafFactory` to `node.rs`**
|
||||
|
||||
After the `ParamSpec` struct (ends `node.rs:58`) and before the `NodeSchema` doc,
|
||||
add (the `use` at `node.rs:11` already imports `Scalar`):
|
||||
|
||||
```rust
|
||||
/// A param-generic blueprint leaf (C19): a node's declared tunable params plus a
|
||||
/// closure that builds a sized instance through the node's own constructor (the
|
||||
/// single sizing/validation gate). A blueprint holds these recipes, never built
|
||||
/// instances, so it stays value-empty until a param-set is injected (C19/C23).
|
||||
pub struct LeafFactory {
|
||||
name: &'static str,
|
||||
params: Vec<ParamSpec>,
|
||||
build: Box<dyn Fn(&[Scalar]) -> Box<dyn Node>>,
|
||||
}
|
||||
|
||||
impl LeafFactory {
|
||||
/// `name` is the param-generic render label (the node type, e.g. `"SMA"`);
|
||||
/// `params` the declared knobs; `build` constructs a sized node from a
|
||||
/// kind-checked param slice.
|
||||
pub fn new(
|
||||
name: &'static str,
|
||||
params: Vec<ParamSpec>,
|
||||
build: impl Fn(&[Scalar]) -> Box<dyn Node> + 'static,
|
||||
) -> Self {
|
||||
Self { name, params, build: Box::new(build) }
|
||||
}
|
||||
/// The declared tunable params (read by `Blueprint::param_space`, pre-build).
|
||||
pub fn params(&self) -> &[ParamSpec] {
|
||||
&self.params
|
||||
}
|
||||
/// Build a sized node from its param slice (the slice is kind-checked by the
|
||||
/// caller before this runs).
|
||||
pub fn build(&self, params: &[Scalar]) -> Box<dyn Node> {
|
||||
(self.build)(params)
|
||||
}
|
||||
/// The param-generic render label for the blueprint view (C22 "structure
|
||||
/// before"): the node type plus its tunable param *names* — no values, a
|
||||
/// value-empty recipe has none — e.g. `SMA(length)`, `LinComb(weights[0],
|
||||
/// weights[1])`, or bare `SimBroker` when paramless.
|
||||
pub fn label(&self) -> String {
|
||||
if self.params.is_empty() {
|
||||
self.name.to_string()
|
||||
} else {
|
||||
let knobs: Vec<&str> = self.params.iter().map(|p| p.name.as_str()).collect();
|
||||
format!("{}({})", self.name, knobs.join(", "))
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add value accessors to `scalar.rs`**
|
||||
|
||||
Inside the existing `impl Scalar` block (after `kind`, `scalar.rs:30-37`), add:
|
||||
|
||||
```rust
|
||||
/// The `i64` payload, or `None` if this scalar is not an `I64`.
|
||||
pub fn as_i64(self) -> Option<i64> {
|
||||
if let Scalar::I64(v) = self { Some(v) } else { None }
|
||||
}
|
||||
/// The `f64` payload, or `None` if this scalar is not an `F64`.
|
||||
pub fn as_f64(self) -> Option<f64> {
|
||||
if let Scalar::F64(v) = self { Some(v) } else { None }
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Re-export `LeafFactory`**
|
||||
|
||||
In `crates/aura-core/src/lib.rs:42`, add `LeafFactory` to the `pub use node::{...}`
|
||||
list (keep alphabetical): `pub use node::{FieldSpec, Firing, InputSpec, LeafFactory,
|
||||
Node, NodeSchema, ParamSpec};`
|
||||
|
||||
- [ ] **Step 4: Tests in `node.rs` and `scalar.rs`**
|
||||
|
||||
In `node.rs` tests (reuse the `Bare` node already defined in that module, `node.rs`
|
||||
test mod), add:
|
||||
|
||||
```rust
|
||||
#[test]
|
||||
fn leaf_factory_label_is_param_generic() {
|
||||
let with = LeafFactory::new(
|
||||
"SMA",
|
||||
vec![ParamSpec { name: "length".into(), kind: ScalarKind::I64 }],
|
||||
|_| Box::new(Bare),
|
||||
);
|
||||
assert_eq!(with.label(), "SMA(length)");
|
||||
let none = LeafFactory::new("Sub", vec![], |_| Box::new(Bare));
|
||||
assert_eq!(none.label(), "Sub");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn leaf_factory_build_runs_the_closure() {
|
||||
let f = LeafFactory::new("Bare", vec![], |_| Box::new(Bare));
|
||||
assert_eq!(f.build(&[]).schema().params, Vec::<ParamSpec>::new());
|
||||
}
|
||||
```
|
||||
|
||||
In `scalar.rs` tests, add:
|
||||
|
||||
```rust
|
||||
#[test]
|
||||
fn scalar_value_accessors_are_kind_exact() {
|
||||
assert_eq!(Scalar::I64(3).as_i64(), Some(3));
|
||||
assert_eq!(Scalar::I64(3).as_f64(), None);
|
||||
assert_eq!(Scalar::F64(0.5).as_f64(), Some(0.5));
|
||||
assert_eq!(Scalar::F64(0.5).as_i64(), None);
|
||||
}
|
||||
```
|
||||
|
||||
(If `scalar.rs` has no `#[cfg(test)] mod tests`, add one with `use super::*;`.)
|
||||
|
||||
- [ ] **Step 5: Gate**
|
||||
|
||||
Run: `cargo test -p aura-core`
|
||||
Expected: PASS, including `leaf_factory_label_is_param_generic`,
|
||||
`leaf_factory_build_runs_the_closure`, `scalar_value_accessors_are_kind_exact`.
|
||||
|
||||
---
|
||||
|
||||
## Task 2: aura-std — `factory()` on the 7 nodes
|
||||
|
||||
**Files:** Modify each of
|
||||
`crates/aura-std/src/{sma,exposure,lincomb,sub,add,sim_broker,recorder}.rs`.
|
||||
|
||||
Each `factory()` is an inherent method in the node's existing `impl <Node>` block
|
||||
(beside `new`). Add `LeafFactory` to each file's `use aura_core::{...}` line.
|
||||
|
||||
- [ ] **Step 1: `Sma::factory` (`sma.rs`)**
|
||||
|
||||
```rust
|
||||
/// The param-generic recipe for a blueprint leaf: declares `length` and builds
|
||||
/// through `Sma::new` (the single sizing/validation gate; the slice is
|
||||
/// kind-checked before `build` runs, so the typed read is total).
|
||||
pub fn factory() -> LeafFactory {
|
||||
LeafFactory::new(
|
||||
"SMA",
|
||||
vec![ParamSpec { name: "length".into(), kind: ScalarKind::I64 }],
|
||||
|p| Box::new(Sma::new(p[0].as_i64().expect("length slot is I64") as usize)),
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: `Exposure::factory` (`exposure.rs`)**
|
||||
|
||||
```rust
|
||||
pub fn factory() -> LeafFactory {
|
||||
LeafFactory::new(
|
||||
"Exposure",
|
||||
vec![ParamSpec { name: "scale".into(), kind: ScalarKind::F64 }],
|
||||
|p| Box::new(Exposure::new(p[0].as_f64().expect("scale slot is F64"))),
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: `LinComb::factory(arity)` (`lincomb.rs`)**
|
||||
|
||||
The arity is topology (fixed per blueprint, C19), taken as a factory arg; only the
|
||||
weight *values* are injected.
|
||||
|
||||
```rust
|
||||
pub fn factory(arity: usize) -> LeafFactory {
|
||||
let params = (0..arity)
|
||||
.map(|i| ParamSpec { name: format!("weights[{i}]"), kind: ScalarKind::F64 })
|
||||
.collect();
|
||||
LeafFactory::new(
|
||||
"LinComb",
|
||||
params,
|
||||
|p| Box::new(LinComb::new(
|
||||
p.iter().map(|s| s.as_f64().expect("weight slot is F64")).collect(),
|
||||
)),
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: paramless `Sub`/`Add::factory` (`sub.rs`, `add.rs`)**
|
||||
|
||||
```rust
|
||||
// sub.rs
|
||||
pub fn factory() -> LeafFactory {
|
||||
LeafFactory::new("Sub", vec![], |_| Box::new(Sub::new()))
|
||||
}
|
||||
// add.rs
|
||||
pub fn factory() -> LeafFactory {
|
||||
LeafFactory::new("Add", vec![], |_| Box::new(Add::new()))
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 5: `SimBroker::factory(pip_size)` (`sim_broker.rs`)**
|
||||
|
||||
`pip_size` is metadata (C10/C15), not a tunable param — captured by the closure.
|
||||
|
||||
```rust
|
||||
pub fn factory(pip_size: f64) -> LeafFactory {
|
||||
LeafFactory::new("SimBroker", vec![], move |_| Box::new(SimBroker::new(pip_size)))
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 6: `Recorder::factory(kinds, firing, tx)` (`recorder.rs`)**
|
||||
|
||||
The channel + kinds + firing are non-param construction args — captured; `tx` is
|
||||
cloned per build (`mpsc::Sender: Clone`).
|
||||
|
||||
```rust
|
||||
pub fn factory(
|
||||
kinds: Vec<ScalarKind>,
|
||||
firing: Firing,
|
||||
tx: Sender<(Timestamp, Vec<Scalar>)>,
|
||||
) -> LeafFactory {
|
||||
LeafFactory::new("Recorder", vec![], move |_| {
|
||||
Box::new(Recorder::new(&kinds, firing, tx.clone()))
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 7: factory↔schema params agreement test (one per node)**
|
||||
|
||||
Add to each node's `#[cfg(test)] mod tests` a test asserting `factory().params()`
|
||||
equals the built node's `schema().params`. Example for `sma.rs`:
|
||||
|
||||
```rust
|
||||
#[test]
|
||||
fn factory_params_match_built_node_schema() {
|
||||
let f = Sma::factory();
|
||||
let built = f.build(&[Scalar::I64(3)]);
|
||||
assert_eq!(f.params(), built.schema().params.as_slice());
|
||||
}
|
||||
```
|
||||
|
||||
Mirror it per node with a valid sample slice: `Exposure` `&[Scalar::F64(0.5)]`;
|
||||
`LinComb::factory(2)` `&[Scalar::F64(1.0), Scalar::F64(-1.0)]`; `Sub`/`Add` `&[]`;
|
||||
`SimBroker::factory(0.0001)` `&[]`; `Recorder::factory(vec![ScalarKind::F64],
|
||||
Firing::Any, tx)` `&[]` (make a throwaway `mpsc::channel()` for `tx`).
|
||||
|
||||
- [ ] **Step 8: Gate**
|
||||
|
||||
Run: `cargo test -p aura-std`
|
||||
Expected: PASS, including the 7 `factory_params_match_built_node_schema` tests.
|
||||
|
||||
---
|
||||
|
||||
## Task 3: aura-engine — value-empty leaf, build-then-wire compile, errors
|
||||
|
||||
**Files:** Modify `crates/aura-engine/src/blueprint.rs`.
|
||||
|
||||
- [ ] **Step 1: `BlueprintNode::Leaf` + the lift**
|
||||
|
||||
Change the enum (`blueprint.rs:27-30`) and replace the generic `From<N>`
|
||||
(`blueprint.rs:33-37`):
|
||||
|
||||
```rust
|
||||
pub enum BlueprintNode {
|
||||
Leaf(LeafFactory),
|
||||
Composite(Composite),
|
||||
}
|
||||
|
||||
impl From<LeafFactory> for BlueprintNode {
|
||||
fn from(factory: LeafFactory) -> Self {
|
||||
BlueprintNode::Leaf(factory)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Add `LeafFactory` and `Scalar` to the `use aura_core::{...}` at `blueprint.rs:14`.
|
||||
|
||||
- [ ] **Step 2: Remove the vestigial pre-build `schema` methods**
|
||||
|
||||
Delete the `impl BlueprintNode { fn schema(&self) -> NodeSchema {...} }` block
|
||||
(`blueprint.rs:39-48`) and `Composite::schema` (`blueprint.rs:103-114`). Both have
|
||||
no live caller — `compile` resolves every interface on the built flat nodes. Delete
|
||||
the unit test `composite_schema_derives_role_and_output_kinds` (`blueprint.rs:435`).
|
||||
|
||||
- [ ] **Step 3: `collect_params` reads `factory.params()`**
|
||||
|
||||
In `collect_params` (`blueprint.rs:217-240`), the `Leaf` arm (`220-229`):
|
||||
|
||||
```rust
|
||||
BlueprintNode::Leaf(factory) => {
|
||||
for p in factory.params() {
|
||||
let name = if prefix.is_empty() {
|
||||
p.name.clone()
|
||||
} else {
|
||||
format!("{prefix}.{}", p.name)
|
||||
};
|
||||
out.push(ParamSpec { name, kind: p.kind });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
(`param_space` at `blueprint.rs:166-170` is otherwise unchanged.)
|
||||
|
||||
- [ ] **Step 4: Two new `CompileError` variants**
|
||||
|
||||
In `enum CompileError` (`blueprint.rs:120-130`) add:
|
||||
|
||||
```rust
|
||||
/// An injected param value's scalar kind does not match the slot's declared
|
||||
/// kind. `slot` is the flat param-space index.
|
||||
ParamKindMismatch { slot: usize, expected: ScalarKind, got: ScalarKind },
|
||||
/// The injected vector's length does not equal the sum of declared params.
|
||||
ParamArity { expected: usize, got: usize },
|
||||
```
|
||||
|
||||
`ScalarKind` is already imported (`blueprint.rs:14`).
|
||||
|
||||
- [ ] **Step 5: Thread params+cursor through `lower_items` / `inline_composite`**
|
||||
|
||||
`lower_items` (`blueprint.rs:255-274`) gains `params: &[Scalar]` and `cursor: &mut
|
||||
usize`, and its `Leaf` arm builds (kind-checking) instead of moving a node:
|
||||
|
||||
```rust
|
||||
fn lower_items(
|
||||
items: Vec<BlueprintNode>,
|
||||
params: &[Scalar],
|
||||
cursor: &mut usize,
|
||||
flat_nodes: &mut Vec<Box<dyn Node>>,
|
||||
flat_edges: &mut Vec<Edge>,
|
||||
) -> Result<Vec<ItemLowering>, CompileError> {
|
||||
let mut lowerings = Vec::with_capacity(items.len());
|
||||
for item in items {
|
||||
match item {
|
||||
BlueprintNode::Leaf(factory) => {
|
||||
let n = factory.params().len();
|
||||
let slice = ¶ms[*cursor..*cursor + n]; // in range: arity checked up front
|
||||
for (i, spec) in factory.params().iter().enumerate() {
|
||||
let got = slice[i].kind();
|
||||
if got != spec.kind {
|
||||
return Err(CompileError::ParamKindMismatch {
|
||||
slot: *cursor + i,
|
||||
expected: spec.kind,
|
||||
got,
|
||||
});
|
||||
}
|
||||
}
|
||||
let index = flat_nodes.len();
|
||||
flat_nodes.push(factory.build(slice));
|
||||
*cursor += n;
|
||||
lowerings.push(ItemLowering::Leaf { index });
|
||||
}
|
||||
BlueprintNode::Composite(c) => {
|
||||
lowerings.push(inline_composite(c, params, cursor, flat_nodes, flat_edges)?);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(lowerings)
|
||||
}
|
||||
```
|
||||
|
||||
`inline_composite` (`blueprint.rs:278-339`) gains the same `params: &[Scalar]` +
|
||||
`cursor: &mut usize` params and forwards them on its recursive `lower_items` call
|
||||
(`blueprint.rs:295`): `let interior = lower_items(nodes, params, cursor,
|
||||
flat_nodes, flat_edges)?;`. Its signature line becomes:
|
||||
|
||||
```rust
|
||||
fn inline_composite(
|
||||
c: Composite,
|
||||
params: &[Scalar],
|
||||
cursor: &mut usize,
|
||||
flat_nodes: &mut Vec<Box<dyn Node>>,
|
||||
flat_edges: &mut Vec<Edge>,
|
||||
) -> Result<ItemLowering, CompileError> {
|
||||
```
|
||||
|
||||
- [ ] **Step 6: `compile_with_params` + `bootstrap_with_params` + thin no-param wrappers**
|
||||
|
||||
Replace `compile` (`blueprint.rs:179-204`) and `bootstrap` (`207-210`) with the
|
||||
param-driven path plus no-param wrappers. The arity is checked up front via
|
||||
`param_space().len()` so the per-leaf slices never overrun (only kind can fail):
|
||||
|
||||
```rust
|
||||
/// Compile the value-empty recipe under an injected param vector: build each
|
||||
/// leaf from its kind-checked slice while lowering, then rewrite edges/sources
|
||||
/// exactly as before (structure is param-invariant, C19/C23). The vector is
|
||||
/// total and positional — one value per `param_space()` slot, in slot order.
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub fn compile_with_params(
|
||||
self,
|
||||
params: &[Scalar],
|
||||
) -> Result<(Vec<Box<dyn Node>>, Vec<SourceSpec>, Vec<Edge>), CompileError> {
|
||||
let expected = self.param_space().len();
|
||||
if params.len() != expected {
|
||||
return Err(CompileError::ParamArity { expected, got: params.len() });
|
||||
}
|
||||
let mut flat_nodes: Vec<Box<dyn Node>> = Vec::new();
|
||||
let mut flat_edges: Vec<Edge> = Vec::new();
|
||||
let mut cursor = 0usize;
|
||||
let lowerings = lower_items(self.nodes, params, &mut cursor, &mut flat_nodes, &mut flat_edges)?;
|
||||
|
||||
for e in &self.edges {
|
||||
for fe in rewrite_edge(e, &lowerings, &flat_nodes)? {
|
||||
flat_edges.push(fe);
|
||||
}
|
||||
}
|
||||
let mut flat_sources: Vec<SourceSpec> = Vec::with_capacity(self.sources.len());
|
||||
for src in &self.sources {
|
||||
let mut targets: Vec<Target> = Vec::new();
|
||||
for t in &src.targets {
|
||||
targets.extend(resolve_target(t, &lowerings)?);
|
||||
}
|
||||
flat_sources.push(SourceSpec { kind: src.kind, targets });
|
||||
}
|
||||
Ok((flat_nodes, flat_sources, flat_edges))
|
||||
}
|
||||
|
||||
/// No-param compile (a blueprint that declares no params); errors `ParamArity`
|
||||
/// if any param is declared.
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub fn compile(self) -> Result<(Vec<Box<dyn Node>>, Vec<SourceSpec>, Vec<Edge>), CompileError> {
|
||||
self.compile_with_params(&[])
|
||||
}
|
||||
|
||||
/// Compile under an injected vector, then hand the flat compilat to the
|
||||
/// unchanged `Harness::bootstrap`.
|
||||
pub fn bootstrap_with_params(self, params: Vec<Scalar>) -> Result<Harness, CompileError> {
|
||||
let (nodes, sources, edges) = self.compile_with_params(¶ms)?;
|
||||
Harness::bootstrap(nodes, sources, edges).map_err(CompileError::Bootstrap)
|
||||
}
|
||||
|
||||
/// No-param bootstrap (paramless blueprint).
|
||||
pub fn bootstrap(self) -> Result<Harness, CompileError> {
|
||||
self.bootstrap_with_params(vec![])
|
||||
}
|
||||
```
|
||||
|
||||
(Keep the existing `#[allow(clippy::type_complexity)]` + comment that sat above
|
||||
`compile`.)
|
||||
|
||||
- [ ] **Step 7: Re-express the fixtures (mechanical)**
|
||||
|
||||
Every blueprint-leaf author site in the test module changes by the rules below; the
|
||||
hand-wired `hand_wired_sma_cross_harness` (which builds nodes directly into
|
||||
`Harness::bootstrap`, not via `BlueprintNode`) is **unchanged**.
|
||||
|
||||
- `Sma::new(k).into()` → `Sma::factory().into()`; the `k` moves into the caller's
|
||||
bootstrap/compile vector (`Scalar::I64(k)`).
|
||||
- `Exposure::new(s).into()` → `Exposure::factory().into()`; `s` → `Scalar::F64(s)`.
|
||||
- `SimBroker::new(p).into()` → `SimBroker::factory(p).into()` (pip captured, no
|
||||
vector slot).
|
||||
- `Recorder::new(&[K..], f, tx).into()` → `Recorder::factory(vec![K..], f, tx).into()`.
|
||||
- `LinComb::new(w).into()` → `LinComb::factory(w.len()).into()`; the weights →
|
||||
`w.iter().map(|x| Scalar::F64(*x))` in the caller's vector.
|
||||
- `sma_cross(fast, slow)` builder (`blueprint.rs:721-732`) → `sma_cross()` taking no
|
||||
args, building two `Sma::factory()` leaves; callers move `fast`/`slow` into their
|
||||
vector.
|
||||
- A test-local node `Leaf(Box::new(X))` (sites `440`, `535`, `576`, `618`, `648`)
|
||||
→ an inline factory: `BlueprintNode::Leaf(LeafFactory::new("X", vec![], |_|
|
||||
Box::new(X::new())))` (or `X::new().into()` if that test node is given a
|
||||
`factory()`; inline is simpler for one-off test nodes).
|
||||
- `bp.bootstrap()` → `bp.bootstrap_with_params(vec![..])` with the vector matching
|
||||
the leaves' declared params in `param_space()` order; `bp.compile()` →
|
||||
`bp.compile_with_params(&[..])`. For a paramless fixture, `bootstrap_with_params(
|
||||
vec![])` / `compile_with_params(&[])` (or the thin `bootstrap()`/`compile()`).
|
||||
|
||||
`composite_sma_cross_harness` (`blueprint.rs:736-766`) becomes value-empty:
|
||||
`Composite(sma_cross())`, `Exposure::factory().into()`, `SimBroker::factory(0.0001)
|
||||
.into()`, `Recorder::factory(vec![ScalarKind::F64], Firing::Any, tx_eq).into()`,
|
||||
etc. Its declared `param_space()` is `[length:I64, length:I64, scale:F64]`, so its
|
||||
point vector is `vec![Scalar::I64(2), Scalar::I64(4), Scalar::F64(0.5)]`.
|
||||
|
||||
- [ ] **Step 8: Re-express the load-bearing tests**
|
||||
|
||||
- `composite_sma_cross_runs_bit_identical_to_hand_wired` (`blueprint.rs:768-792`):
|
||||
build the composite via `bp.bootstrap_with_params(vec![Scalar::I64(2),
|
||||
Scalar::I64(4), Scalar::F64(0.5)])`; the hand-wired side stays `Sma::new(2)`,
|
||||
`Sma::new(4)`, `Exposure::new(0.5)`. Assertions unchanged (traces bit-identical).
|
||||
- `param_space_mirrors_compiled_flat_node_param_order` (`802-834`) and
|
||||
`..._under_nesting` (`886-936`): call `bp.compile_with_params(&[..])` with the
|
||||
matching vector instead of `bp.compile()`; the `flat_nodes.iter().flat_map(|n|
|
||||
n.schema().params)` projection and the kind-by-slot assertions are unchanged
|
||||
(built nodes still carry `schema().params`). For the single-level case the vector
|
||||
is `[Scalar::I64(2), Scalar::I64(4), Scalar::F64(0.5)]`; for the nested
|
||||
`strategy → { fast_slow → [Sma, Sma, Sub], LinComb }` case it is `[Scalar::I64(2),
|
||||
Scalar::I64(4), Scalar::F64(1.0), Scalar::F64(-1.0)]`.
|
||||
- `param_space_is_flat_path_qualified_and_slot_disambiguated` (`837`),
|
||||
`top_level_leaf_params_are_unqualified` (`876`), `param_space_is_deterministic`
|
||||
(`885`), `param_space_empty_for_paramless_and_empty_blueprints` (`896`): rebuild
|
||||
their blueprints with factory leaves; the `param_space()` assertions are
|
||||
unchanged.
|
||||
|
||||
- [ ] **Step 9: New injection tests**
|
||||
|
||||
Add to the test module:
|
||||
|
||||
```rust
|
||||
#[test]
|
||||
fn injecting_a_different_vector_changes_the_run() {
|
||||
let prices = synthetic_prices();
|
||||
let (bp, eq, _ex) = composite_sma_cross_harness();
|
||||
let mut a = bp.bootstrap_with_params(vec![Scalar::I64(2), Scalar::I64(4), Scalar::F64(0.5)])
|
||||
.expect("compiles");
|
||||
a.run(vec![prices.clone()]);
|
||||
let a_eq = eq.try_iter().collect::<Vec<_>>();
|
||||
|
||||
let (bp2, eq2, _ex2) = composite_sma_cross_harness();
|
||||
let mut b = bp2.bootstrap_with_params(vec![Scalar::I64(5), Scalar::I64(20), Scalar::F64(1.0)])
|
||||
.expect("compiles");
|
||||
b.run(vec![prices]);
|
||||
let b_eq = eq2.try_iter().collect::<Vec<_>>();
|
||||
|
||||
assert!(!a_eq.is_empty() && !b_eq.is_empty(), "both traces populated");
|
||||
assert_ne!(a_eq, b_eq, "a different vector must yield a different run");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wrong_kind_is_a_param_kind_mismatch() {
|
||||
let (bp, _eq, _ex) = composite_sma_cross_harness();
|
||||
// slot 0 is I64 (an SMA length); inject F64 there
|
||||
let err = bp.bootstrap_with_params(vec![Scalar::F64(2.0), Scalar::I64(4), Scalar::F64(0.5)])
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, CompileError::ParamKindMismatch { slot: 0, .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wrong_arity_is_a_param_arity_error() {
|
||||
let (short, _e1, _x1) = composite_sma_cross_harness();
|
||||
assert!(matches!(
|
||||
short.bootstrap_with_params(vec![Scalar::I64(2)]).unwrap_err(),
|
||||
CompileError::ParamArity { expected: 3, got: 1 }
|
||||
));
|
||||
let (long, _e2, _x2) = composite_sma_cross_harness();
|
||||
assert!(matches!(
|
||||
long.bootstrap_with_params(
|
||||
vec![Scalar::I64(2), Scalar::I64(4), Scalar::F64(0.5), Scalar::F64(0.0)]
|
||||
).unwrap_err(),
|
||||
CompileError::ParamArity { expected: 3, got: 4 }
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn same_vector_bootstraps_identically() {
|
||||
let prices = synthetic_prices();
|
||||
let (bp, eq, _ex) = composite_sma_cross_harness();
|
||||
let mut a = bp.bootstrap_with_params(vec![Scalar::I64(3), Scalar::I64(9), Scalar::F64(0.7)])
|
||||
.expect("compiles");
|
||||
a.run(vec![prices.clone()]);
|
||||
let (bp2, eq2, _ex2) = composite_sma_cross_harness();
|
||||
let mut b = bp2.bootstrap_with_params(vec![Scalar::I64(3), Scalar::I64(9), Scalar::F64(0.7)])
|
||||
.expect("compiles");
|
||||
b.run(vec![prices]);
|
||||
assert_eq!(eq.try_iter().collect::<Vec<_>>(), eq2.try_iter().collect::<Vec<_>>());
|
||||
}
|
||||
```
|
||||
|
||||
(If `composite_sma_cross_harness` returns a fresh blueprint+receivers per call,
|
||||
each test calls it anew as shown; keep its existing return signature.)
|
||||
|
||||
- [ ] **Step 10: Gate**
|
||||
|
||||
Run: `cargo build -p aura-engine --all-targets`
|
||||
Expected: 0 errors (every fixture site repaired).
|
||||
|
||||
Run: `cargo test -p aura-engine`
|
||||
Expected: PASS — bit-identity, both mirror tests, the four new injection tests, and
|
||||
all `param_space*` tests green.
|
||||
|
||||
---
|
||||
|
||||
## Task 4: aura-cli — param-generic render + sample + goldens
|
||||
|
||||
**Files:** Modify `crates/aura-cli/src/graph.rs`, `crates/aura-cli/src/main.rs`.
|
||||
|
||||
- [ ] **Step 1: `render_blueprint` reads `LeafFactory::label()` (`graph.rs`)**
|
||||
|
||||
The two leaf arms (`graph.rs:60-63` top-level, `:69-72` composite-interior) push
|
||||
`node.label()`. The leaf is now a `LeafFactory`; push `factory.label()`:
|
||||
|
||||
```rust
|
||||
BlueprintNode::Leaf(factory) => {
|
||||
let id = labels.len();
|
||||
labels.push(factory.label());
|
||||
item_display.push(ItemDisplay::Leaf(id));
|
||||
}
|
||||
```
|
||||
|
||||
and inside the composite loop:
|
||||
|
||||
```rust
|
||||
BlueprintNode::Leaf(factory) => {
|
||||
let id = labels.len();
|
||||
labels.push(factory.label());
|
||||
interior_ids.push(id);
|
||||
}
|
||||
```
|
||||
|
||||
(`render_compilat` / the compiled-view renderer operates on built flat nodes via
|
||||
`Node::label()` and is unchanged.)
|
||||
|
||||
- [ ] **Step 2: Sample blueprint → factories (`main.rs`)**
|
||||
|
||||
- `sma_cross(name, fast, slow)` (`main.rs:120-131`): build two `Sma::factory()`
|
||||
leaves; drop `fast`/`slow` from the builder (they move to the injected vector).
|
||||
Keep `name` for the composite.
|
||||
- `build_sample(fast, slow)` (`main.rs:136-161`): the four `.into()` lifts become
|
||||
`Exposure::factory().into()`, `SimBroker::factory(0.0001).into()`,
|
||||
`Recorder::factory(...).into()` per their constructors; the composite is
|
||||
`BlueprintNode::Composite(sma_cross(name))`. `build_sample` no longer bakes
|
||||
`fast`/`slow`.
|
||||
- `sample_blueprint` (`main.rs:164-166`) and the `bp.compile()` sites
|
||||
(`main.rs:180,221,272`): supply the point vector. The sample's `param_space()` is
|
||||
`[length:I64, length:I64, scale:F64]`, so its vector is `vec![Scalar::I64(2),
|
||||
Scalar::I64(4), Scalar::F64(0.5)]`. Use `compile_with_params(&[..])` /
|
||||
`bootstrap_with_params(vec![..])` at those sites.
|
||||
|
||||
- [ ] **Step 3: Move the swap to the compiled view (`main.rs`)**
|
||||
|
||||
`sample_blueprint_swapped` (`main.rs:201-203`) + `swapped_sma_inputs_render_differently`
|
||||
(`main.rs:230`): the blueprint view is now param-generic and identical for both
|
||||
orderings, so the swap is not observable there. Re-express the swap as a different
|
||||
injected vector (`vec![Scalar::I64(4), Scalar::I64(2), Scalar::F64(0.5)]`) and
|
||||
assert the **compiled** view differs (`render_compilat` of the compiled flat nodes
|
||||
shows `SMA(4)`/`SMA(2)` swapped), not the blueprint view. Rename the test to its
|
||||
new premise (e.g. `swapped_param_vector_changes_the_compiled_render`).
|
||||
|
||||
- [ ] **Step 4: Re-capture the blueprint-view goldens (`main.rs`)**
|
||||
|
||||
`blueprint_view_shows_cluster_and_param_labels` (`main.rs:206`), `blueprint_view_golden`
|
||||
(`:238`): the blueprint-view labels become param-generic — `SMA(length)` (both
|
||||
SMAs identical), `Exposure(scale)`, `SimBroker`. Update the pinned ASCII strings to
|
||||
the param-generic form. The compiled-view goldens `compiled_view_dissolves_the_composite_boundary`
|
||||
(`:219`) and `compiled_view_golden` (`:270`) stay valued (`SMA(2)`, `SMA(4)`,
|
||||
`Exposure(0.5)`, `SimBroker(0.0001)`) — do not change them.
|
||||
|
||||
To get the exact new blueprint-view bytes, run the rendering in a scratch
|
||||
assertion or `cargo run -- graph` (blueprint view) after Steps 1-2 compile, and
|
||||
paste the produced ASCII verbatim into the golden. Do not hand-guess box-drawing
|
||||
columns.
|
||||
|
||||
- [ ] **Step 5: Gate**
|
||||
|
||||
Run: `cargo build -p aura-cli --all-targets`
|
||||
Expected: 0 errors.
|
||||
|
||||
Run: `cargo test --workspace`
|
||||
Expected: PASS — all crates green, including the re-captured goldens and the
|
||||
re-premised swap test.
|
||||
|
||||
Run: `cargo clippy --workspace --all-targets -- -D warnings`
|
||||
Expected: clean.
|
||||
Reference in New Issue
Block a user