feat(aura-engine,aura-cli): named param binding — single-run .with()/.bootstrap() (0030 iter 1)
Bind a blueprint's open knobs by name for a single run instead of by a positional,
Scalar-wrapped Vec in param_space() order:
bp.with("sma_cross.fast", 2).with("sma_cross.slow", 4).with("scale", 0.5).bootstrap()
A pure authoring layer over the existing Composite::param_space / bootstrap_with_params
primitives; the engine run loop and the construction core are untouched (C1/C12/C19/C23
preserved). Raw literals lower via Into<Scalar> (the literal fixes the variant: 2->I64,
0.5->F64, pinned by e97906a); the match key is the exact param_space() name — path-qualified
for a composite-interior knob (sma_cross.fast), bare for a root-level knob (scale).
This iteration (single-run side):
- BindError vocabulary (name-qualified); the full enum incl. the sweep-only EmptyAxis is
defined and re-exported now, so the unconstructed variant is reachable public API, not
dead_code — EmptyAxis is constructed in iteration 2 (the sweep side).
- resolve(): the shared name-resolution core, a total error order — Phase 1 per binding in
input order (a UnknownKnob / b AmbiguousKnob / d DuplicateBinding), Phase 2 slot walk in
param_space() order (e MissingKnob / f KindMismatch); first failing check wins.
- Composite::with -> Binder -> Binder::bootstrap; .with() not .bind() (bind is reserved for
#55's structural-constant overlay).
- The CLI sample single-run test converted to the named form (behaviour-preserving).
Verified (run by the orchestrator, not the agent's claim): 9 new aura-engine tests green —
resolve round-trip + one per single-run BindError variant + two precedence tests + the C1
named-equals-positional equivalence over the sample composite harness; the converted CLI
sample test green; full `cargo test --workspace` green and `cargo clippy --workspace
--all-targets -- -D warnings` clean.
Iteration 2 (the sweep side: axis / SweepBinder / resolve_axes / EmptyAxis + the sweep CLI
conversion) is next. refs #35
This commit is contained in:
@@ -517,7 +517,10 @@ mod tests {
|
|||||||
// so a caller can bootstrap one point, run it, and drain both sinks.
|
// so a caller can bootstrap one point, run it, and drain both sinks.
|
||||||
let (bp, rx_eq, rx_ex) = sample_blueprint_with_sinks();
|
let (bp, rx_eq, rx_ex) = sample_blueprint_with_sinks();
|
||||||
let mut h = bp
|
let mut h = bp
|
||||||
.bootstrap_with_params(vec![Scalar::I64(2), Scalar::I64(4), Scalar::F64(0.5)])
|
.with("sma_cross.fast", 2)
|
||||||
|
.with("sma_cross.slow", 4)
|
||||||
|
.with("scale", 0.5)
|
||||||
|
.bootstrap()
|
||||||
.expect("sample blueprint compiles under a valid point");
|
.expect("sample blueprint compiles under a valid point");
|
||||||
h.run(vec![synthetic_prices()]);
|
h.run(vec![synthetic_prices()]);
|
||||||
assert!(!rx_eq.try_iter().collect::<Vec<_>>().is_empty(), "equity sink drained empty");
|
assert!(!rx_eq.try_iter().collect::<Vec<_>>().is_empty(), "equity sink drained empty");
|
||||||
|
|||||||
@@ -264,6 +264,102 @@ impl Composite {
|
|||||||
pub fn bootstrap(self) -> Result<Harness, CompileError> {
|
pub fn bootstrap(self) -> Result<Harness, CompileError> {
|
||||||
self.bootstrap_with_params(vec![])
|
self.bootstrap_with_params(vec![])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Begin binding this blueprint's knobs **by name** for a single run (the
|
||||||
|
/// fluent alternative to a positional `bootstrap_with_params` vector). The
|
||||||
|
/// bound name is the exact `param_space()` name — path-qualified for a knob
|
||||||
|
/// inside a composite (`sma_cross.fast`), bare for a root-level knob (`scale`).
|
||||||
|
pub fn with(self, name: &str, v: impl Into<Scalar>) -> Binder {
|
||||||
|
Binder { bp: self, bound: vec![(name.to_string(), v.into())] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A fault in resolving a named binding against `param_space()` — the authoring
|
||||||
|
/// layer over `bootstrap_with_params` / `sweep`. Name-qualified: each message
|
||||||
|
/// names the offending knob, not a slot index. The total error order is documented
|
||||||
|
/// on the resolution path; the first failing check wins.
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
|
pub enum BindError {
|
||||||
|
/// A bound name matches no `param_space()` slot's exact name.
|
||||||
|
UnknownKnob(String),
|
||||||
|
/// A `param_space()` slot was left unbound.
|
||||||
|
MissingKnob(String),
|
||||||
|
/// A bound value's kind does not equal the slot's declared kind.
|
||||||
|
KindMismatch { knob: String, expected: ScalarKind, got: ScalarKind },
|
||||||
|
/// The same name was bound twice.
|
||||||
|
DuplicateBinding(String),
|
||||||
|
/// A name matches the exact name of more than one slot.
|
||||||
|
AmbiguousKnob(String),
|
||||||
|
/// (sweep, iteration 2) An axis was given zero values.
|
||||||
|
EmptyAxis(String),
|
||||||
|
/// The resolved point passed name resolution but failed downstream bootstrap.
|
||||||
|
Compile(CompileError),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A fluent accumulator of named knob bindings for a single run, terminated by
|
||||||
|
/// [`Binder::bootstrap`]. Bindings are resolved against `param_space()` once, at
|
||||||
|
/// the terminal.
|
||||||
|
pub struct Binder {
|
||||||
|
bp: Composite,
|
||||||
|
bound: Vec<(String, Scalar)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Binder {
|
||||||
|
/// Bind one more knob by name; raw literals lower via `Into<Scalar>`.
|
||||||
|
pub fn with(mut self, name: &str, v: impl Into<Scalar>) -> Binder {
|
||||||
|
self.bound.push((name.to_string(), v.into()));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve the accumulated bindings against `param_space()` and bootstrap.
|
||||||
|
pub fn bootstrap(self) -> Result<Harness, BindError> {
|
||||||
|
let space = self.bp.param_space();
|
||||||
|
let point = resolve(&space, &self.bound)?;
|
||||||
|
self.bp.bootstrap_with_params(point).map_err(BindError::Compile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve named bindings to a positional `Vec<Scalar>` in `param_space()` slot
|
||||||
|
/// order. (Body filled in Step 3.)
|
||||||
|
fn resolve(space: &[ParamSpec], bound: &[(String, Scalar)]) -> Result<Vec<Scalar>, BindError> {
|
||||||
|
// Phase 1 — per binding in input order: claim slots by exact name (a, b, d).
|
||||||
|
let mut claimed: Vec<Option<Scalar>> = vec![None; space.len()];
|
||||||
|
for (name, value) in bound {
|
||||||
|
let matches: Vec<usize> = space
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter(|(_, p)| p.name == *name)
|
||||||
|
.map(|(i, _)| i)
|
||||||
|
.collect();
|
||||||
|
match matches.as_slice() {
|
||||||
|
[] => return Err(BindError::UnknownKnob(name.clone())), // a
|
||||||
|
[idx] => {
|
||||||
|
if claimed[*idx].is_some() {
|
||||||
|
return Err(BindError::DuplicateBinding(name.clone())); // d
|
||||||
|
}
|
||||||
|
claimed[*idx] = Some(*value);
|
||||||
|
}
|
||||||
|
_ => return Err(BindError::AmbiguousKnob(name.clone())), // b
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Phase 2 — slot walk in param_space() order (e, f).
|
||||||
|
let mut point = Vec::with_capacity(space.len());
|
||||||
|
for (i, p) in space.iter().enumerate() {
|
||||||
|
match claimed[i] {
|
||||||
|
None => return Err(BindError::MissingKnob(p.name.clone())), // e
|
||||||
|
Some(v) => {
|
||||||
|
if v.kind() != p.kind {
|
||||||
|
return Err(BindError::KindMismatch {
|
||||||
|
knob: p.name.clone(),
|
||||||
|
expected: p.kind,
|
||||||
|
got: v.kind(),
|
||||||
|
}); // f
|
||||||
|
}
|
||||||
|
point.push(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(point)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A construction-phase fault, caught before the flat compilat reaches
|
/// A construction-phase fault, caught before the flat compilat reaches
|
||||||
@@ -737,6 +833,134 @@ mod tests {
|
|||||||
use aura_std::{Ema, Exposure, Recorder, SimBroker, Sma, Sub};
|
use aura_std::{Ema, Exposure, Recorder, SimBroker, Sma, Sub};
|
||||||
use std::sync::mpsc;
|
use std::sync::mpsc;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resolve_named_equals_positional_vector() {
|
||||||
|
// order-independent: shuffled bindings resolve to slot order
|
||||||
|
let space = vec![
|
||||||
|
ParamSpec { name: "sma_cross.fast".into(), kind: ScalarKind::I64 },
|
||||||
|
ParamSpec { name: "sma_cross.slow".into(), kind: ScalarKind::I64 },
|
||||||
|
ParamSpec { name: "scale".into(), kind: ScalarKind::F64 },
|
||||||
|
];
|
||||||
|
let bound = vec![
|
||||||
|
("scale".to_string(), Scalar::F64(0.5)),
|
||||||
|
("sma_cross.fast".to_string(), Scalar::I64(2)),
|
||||||
|
("sma_cross.slow".to_string(), Scalar::I64(4)),
|
||||||
|
];
|
||||||
|
assert_eq!(
|
||||||
|
resolve(&space, &bound),
|
||||||
|
Ok(vec![Scalar::I64(2), Scalar::I64(4), Scalar::F64(0.5)]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resolve_unknown_knob() {
|
||||||
|
let space = vec![ParamSpec { name: "scale".into(), kind: ScalarKind::F64 }];
|
||||||
|
assert_eq!(
|
||||||
|
resolve(&space, &[("nope".to_string(), Scalar::F64(0.5))]),
|
||||||
|
Err(BindError::UnknownKnob("nope".to_string())),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resolve_missing_knob() {
|
||||||
|
let space = vec![
|
||||||
|
ParamSpec { name: "a".into(), kind: ScalarKind::I64 },
|
||||||
|
ParamSpec { name: "b".into(), kind: ScalarKind::I64 },
|
||||||
|
];
|
||||||
|
assert_eq!(
|
||||||
|
resolve(&space, &[("a".to_string(), Scalar::I64(1))]),
|
||||||
|
Err(BindError::MissingKnob("b".to_string())),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resolve_kind_mismatch() {
|
||||||
|
let space = vec![ParamSpec { name: "scale".into(), kind: ScalarKind::F64 }];
|
||||||
|
assert_eq!(
|
||||||
|
resolve(&space, &[("scale".to_string(), Scalar::I64(2))]),
|
||||||
|
Err(BindError::KindMismatch {
|
||||||
|
knob: "scale".to_string(),
|
||||||
|
expected: ScalarKind::F64,
|
||||||
|
got: ScalarKind::I64,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resolve_duplicate_binding() {
|
||||||
|
let space = vec![ParamSpec { name: "a".into(), kind: ScalarKind::I64 }];
|
||||||
|
assert_eq!(
|
||||||
|
resolve(
|
||||||
|
&space,
|
||||||
|
&[("a".to_string(), Scalar::I64(1)), ("a".to_string(), Scalar::I64(2))],
|
||||||
|
),
|
||||||
|
Err(BindError::DuplicateBinding("a".to_string())),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resolve_ambiguous_knob() {
|
||||||
|
// two slots with the identical exact name
|
||||||
|
let space = vec![
|
||||||
|
ParamSpec { name: "dup".into(), kind: ScalarKind::I64 },
|
||||||
|
ParamSpec { name: "dup".into(), kind: ScalarKind::I64 },
|
||||||
|
];
|
||||||
|
assert_eq!(
|
||||||
|
resolve(&space, &[("dup".to_string(), Scalar::I64(1))]),
|
||||||
|
Err(BindError::AmbiguousKnob("dup".to_string())),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resolve_precedence_unknown_before_kind_mismatch() {
|
||||||
|
// Phase-1 (unknown) wins over Phase-2 (kind mismatch)
|
||||||
|
let space = vec![ParamSpec { name: "scale".into(), kind: ScalarKind::F64 }];
|
||||||
|
let bound = vec![
|
||||||
|
("typo".to_string(), Scalar::I64(9)),
|
||||||
|
("scale".to_string(), Scalar::I64(2)),
|
||||||
|
];
|
||||||
|
assert_eq!(resolve(&space, &bound), Err(BindError::UnknownKnob("typo".to_string())));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resolve_precedence_unknown_before_duplicate() {
|
||||||
|
// intra-binding: check a (unknown) precedes check d (duplicate)
|
||||||
|
let space = vec![ParamSpec { name: "a".into(), kind: ScalarKind::I64 }];
|
||||||
|
let bound = vec![
|
||||||
|
("typo".to_string(), Scalar::I64(1)),
|
||||||
|
("typo".to_string(), Scalar::I64(2)),
|
||||||
|
];
|
||||||
|
assert_eq!(resolve(&space, &bound), Err(BindError::UnknownKnob("typo".to_string())));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn named_binder_runs_bit_identical_to_positional() {
|
||||||
|
// C1 equivalence: the named builder bootstraps to a run bit-identical to
|
||||||
|
// the positional vector, over the sample composite harness.
|
||||||
|
let (bp, comp_eq, comp_ex) = composite_sma_cross_harness();
|
||||||
|
let mut named = bp
|
||||||
|
.with("sma_cross.fast", 2)
|
||||||
|
.with("sma_cross.slow", 4)
|
||||||
|
.with("scale", 0.5)
|
||||||
|
.bootstrap()
|
||||||
|
.expect("named binding resolves and bootstraps");
|
||||||
|
named.run(vec![synthetic_prices()]);
|
||||||
|
let named_eq = comp_eq.try_iter().collect::<Vec<_>>();
|
||||||
|
let named_ex = comp_ex.try_iter().collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let (bp2, pos_eq, pos_ex) = composite_sma_cross_harness();
|
||||||
|
let mut positional = bp2
|
||||||
|
.bootstrap_with_params(vec![Scalar::I64(2), Scalar::I64(4), Scalar::F64(0.5)])
|
||||||
|
.expect("positional bootstrap");
|
||||||
|
positional.run(vec![synthetic_prices()]);
|
||||||
|
let pos_eq_v = pos_eq.try_iter().collect::<Vec<_>>();
|
||||||
|
let pos_ex_v = pos_ex.try_iter().collect::<Vec<_>>();
|
||||||
|
|
||||||
|
assert!(!named_eq.is_empty(), "named run drained empty");
|
||||||
|
assert_eq!(named_eq, pos_eq_v, "equity stream: named must equal positional");
|
||||||
|
assert_eq!(named_ex, pos_ex_v, "exposure stream: named must equal positional");
|
||||||
|
}
|
||||||
|
|
||||||
/// One f64 input port with `Firing::Any` (the common case for these fixtures).
|
/// One f64 input port with `Firing::Any` (the common case for these fixtures).
|
||||||
fn f64_any() -> PortSpec {
|
fn f64_any() -> PortSpec {
|
||||||
PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "in".into() }
|
PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "in".into() }
|
||||||
|
|||||||
@@ -38,7 +38,8 @@ mod report;
|
|||||||
mod sweep;
|
mod sweep;
|
||||||
|
|
||||||
pub use blueprint::{
|
pub use blueprint::{
|
||||||
aliases_on, signature_of, BlueprintNode, CompileError, Composite, OutField, ParamAlias, Role,
|
aliases_on, signature_of, BindError, Binder, BlueprintNode, CompileError, Composite, OutField,
|
||||||
|
ParamAlias, Role,
|
||||||
};
|
};
|
||||||
pub use graph_model::model_to_json;
|
pub use graph_model::model_to_json;
|
||||||
pub use harness::{BootstrapError, Edge, FlatGraph, Harness, SourceSpec, Target};
|
pub use harness::{BootstrapError, Edge, FlatGraph, Harness, SourceSpec, Target};
|
||||||
|
|||||||
Reference in New Issue
Block a user