feat(aura-core): PrimitiveBuilder::bind — fix a node param as a structural constant
Add `PrimitiveBuilder::bind(slot, value)`: bind a declared param to a structural constant, removed from param_space entirely rather than pinned in the injected vector. This closes the gap #55 names — param-bearing nodes (Sma/Exposure/LinComb) had no constant path, only the no-sweep-mode SimBroker precedent. The motivating case is "SMA2-entry": the 2 is bound to the two-candle construction, so sweeping it would enumerate deformed strategies as valid family members. Approach (Option B, builder-level — the originally-planned direction): bind shrinks the builder's declared param surface (schema.params) and wraps its build closure to re-splice the captured constant at its original positional slot. The construction layer (collect_params / lower_items / param_space / compile_with_params) is byte-unchanged: both dock sites already key off builder.params(), so the shrink propagates for free. Addressing is by param name (the by-name authoring address space, C23/0032); the compilat stays wired by raw index — names never reach it. bind enforces "exactly one open param matches" itself (collect-all-then-reject, panic on zero / on duplicate), mirroring the resolve binder's posture, because a node's own schema.params is not covered by check_param_namespace_injective. Chained binds reconstruct the correct positional vector because each layer computes its slot index relative to the param list it sees (no global position table). Considered and rejected: a per-node builder_const(N) constructor (explodes combinatorially over which subset is constant, not dynamic); a ParamBinding overlay struct on Composite (would need new bookkeeping the builder-level form avoids). Verified: cargo build/clippy/test --workspace all green; new tests cover slot removal, positional reconstruction (chained + partial, via eval), the three panic paths, and the composite param_space projection; blueprint.rs construction layer confirmed byte-unchanged. Deferred (out of scope, follow-up issue): exporting a named frozen strategy as a blueprint-as-values collection — the issue's third fork, needs its own brainstorm. closes #55
This commit is contained in:
@@ -1882,6 +1882,30 @@ mod tests {
|
||||
assert_eq!(space[2].kind, ScalarKind::F64);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn param_space_reflects_only_open_knobs() {
|
||||
use aura_std::{Exposure, Sma};
|
||||
// "sma2_entry": Sma "bias" with length bound to 2 (a structural constant)
|
||||
// plus Exposure "exp" whose `scale` stays open. The bound knob must be
|
||||
// absent from param_space; only the open one remains.
|
||||
let strat = Composite::new(
|
||||
"sma2_entry",
|
||||
vec![
|
||||
Sma::builder().named("bias").bind("length", Scalar::I64(2)).into(),
|
||||
Exposure::builder().named("exp").into(),
|
||||
],
|
||||
vec![], // edges — irrelevant to param_space()
|
||||
vec![], // input_roles
|
||||
vec![], // output
|
||||
);
|
||||
let space = strat.param_space();
|
||||
let names: Vec<&str> = space.iter().map(|p| p.name.as_str()).collect();
|
||||
// collect_params runs with an empty prefix, so a top-level leaf is qualified
|
||||
// by its OWN node segment, not the root composite's name → "exp.scale".
|
||||
// `bias.length` is GONE (bound), not present-but-fixed.
|
||||
assert_eq!(names, ["exp.scale"]);
|
||||
}
|
||||
|
||||
/// E2E (issue #34): the C23/#31 mirror invariant *under composite nesting*.
|
||||
/// `param_space()` (via `collect_params`) duplicates `lower_items`' depth-
|
||||
/// first traversal rather than sharing it, so the two orders must stay in
|
||||
|
||||
Reference in New Issue
Block a user