Blueprint constants: bind a node param as a structural constant outside param_space #55
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Part of the param-space line (refs #31). Surfaced in design discussion after #33.
Problem
#31 made blueprints value-empty: a tunable lives only in the injected param vector, declared as a param_space knob (
Sma::builder()forceslengthinto param_space, read fromp[0]at build). Correct for tuning params — values the run supplies, the sweep varies.But some values are structural constants, not tuning params: a value whose variation does not yield another valid point of the same strategy but deforms it into a different (or nonsensical) one. Example: an "SMA2-entry" strategy — "close of the last candle > high of the prior, SMA(2) as bias". The
2is bound to the two-candle construction; sweepinglengthto 5 does not give "the same strategy at another point", it breaks it. "SMA2-entry" is a different strategy from a length-generic "SMA-entry", not a point in its sweep space.For such a value, leaving the knob open and fixing it in the vector is wrong, not just verbose: param_space would declare a degree of freedom that does not exist, and a sweep over it would enumerate deformed strategies as valid family members.
The discriminator:
The pattern already exists
SimBroker::builder(0.0001)already captures a construction value that is not in param_space (SimBroker::builder(0.0001).schema().params.is_empty()is asserted in aura-std). So "a value in the blueprint, outside param_space" is already legitimate — but only for nodes that offer no sweep mode at all. The gap: param-bearing nodes (SMA, Exposure) have no constant path.Direction: a binding overlay, the twin of ParamAlias (C23)
NOT a second
builder_const(2)per node — that is per-node boilerplate, explodes combinatorially for multi-param nodes (which subset is constant?), and is not dynamic.Instead: a
ParamBinding { node, slot, value }overlay, structurally the twin of the existingParamAlias { name, node, slot }(C23):ParamAliasrelabels slot (node,slot) in param_space —collect_paramspushes with the new name.ParamBindingremoves slot (node,slot) from param_space + feedsvalueat compile —collect_paramsskips the push;lower_items/compile_with_paramsassembles the build vector from bound slots + the injected (shrunk) vector.Docks at the same sites as aliases (blueprint.rs
collect_paramsfor the param_space side;lower_items/compile_with_paramsfor the value side). The build-closure signature (Fn(&[Scalar])) is unchanged;param_space()is the self-reflecting surface that reports only the still-open knobs:bindis Blueprint -> Blueprint with a smaller param_space.builder_constwould be at most sugar overbind, never the primitive. The overlay form is uniform (one bind for every node), dynamic (decided per knob at construction time), and reflected in param_space — the properties a future blueprint-as-values collection needs.Invariant boundary (C9)
A future "blueprint registry" must stay a Rust module of blueprint values (cargo-native reuse, C9-ok) — NOT a by-name runtime node resolution (
"SMA"-> constructor), which is the node-registry C9 forbids and the RustAst/DSL trap C10 forbids. Blueprints are already first-class values (Composite/param_space/compile_with_params); "create a node from a blueprint dynamically" IScompile_with_params. This issue adds only thebindoverlay — no registry layer.Notes
depends on: nothing blocking
context: structural-constant binding, distinct from #35 (which is named-binding on the vector side only)
Update (2026-06-11) — the
ParamAliasframing is supersededThe "twin of the existing
ParamAlias(C23)" direction above is stale. Thisissue was filed before cycles 0031 (node-instance naming) and 0032 (
param_space()injectivity) landed. There is no longer a
ParamAlias { name, node, slot }overlayanywhere in the workspace — node-instance naming replaced it: each node carries its
own instance name (
.named(...), or the lowercased type label by default), andpath-qualification is folded directly into
collect_params(blueprint.rs), whichbuilds each
ParamSpec.nameas<prefix>.<node>.<param>from the node's own name.No separate relabel table sits beside
param_space().What this changes for the design (the substance — bind overlay, the
tuning-param/structural-constant discriminator, the additive stance — still holds):
ParamAliasstruct (that struct isgone). It is the twin of
collect_params' push: wherecollect_paramstodaypushes a slot under its path-qualified name,
.bind()would skip the push andfeed the value at compile from the bound slot, assembling the build vector from
bound slots + the injected (shrunk) vector.
param_space()reports only thestill-open knobs, exactly as before.
collect_params(theparam_space side — skip the push) and
lower_items/compile_with_params(thevalue side — inject at the bound slot). There is no alias site to share any more.
overlay-data placement (now with no
ParamAliasneighbour to sit beside),.bind()ergonomics, and how a named frozen strategy is exported.The C9 invariant boundary (a registry stays a Rust module of blueprint values,
never a by-name runtime node lookup) is unaffected.
Acceptance evidence from the milestone fieldtest (mw_3,
docs/specs/fieldtest-milestone-the-world-param-sweep.md, fieldtests/milestone-the-world-param-sweep/mw_3_structural_constant.rs).
The fieldtest drove the SMA2-entry case end-to-end as a downstream consumer and
confirmed both avenues are wrong in a recordable way:
param_space()knob fixed to 2: the knobsma2_entry.bias : I64is then indistinguishable from a genuinely-tunableknob, and
.axis("sma2_entry.bias", [2,5,10])would enumerate deformedstrategies as valid family members.
param_space()declares a degree offreedom the strategy's definition forbids.
SimBroker::builder(1e-4): verbatim compile wall —Sma::builder()takes no length arg (length is always an injected param), andSma::new(2)returns a builtSma, not aPrimitiveBuilder/BlueprintNode;the only conversion is
From<PrimitiveBuilder> for BlueprintNode(noFrom<Sma>).A blueprint holds value-empty recipes, never built nodes. The construction-constant
escape hatch exists for
SimBrokeronly because itsbuilder(pip_size)takesthe constant; there is no
Sma::builder_with(2). So the escape hatch isstructurally unreachable for a stock indicator node.
The wished-for consumer code (recorded verbatim in mw_3) is the shape #55 should
make work: a
Sma::builder().bind("length", 2)that removes the knob fromparam_space, or anSma::builder_with(2)recipe constructor. The.bind()nameis already reserved for exactly this in spec 0030.
Design reconciliation (specify, in-context entry)
Spec:
0034-blueprint-constant-bind. The forks below are still listed open inthis issue body (which predates the 2026-06-12 session); their status is updated
here so the record matches the design as settled.
Fork: overlay-data placement → Option B, builder-level bind.
.bind()is a method on
PrimitiveBuilderthat shrinks the node's declared param surfaceand wraps its build closure; it is not an overlay struct on
Composite. Thetwo sites the issue update names as "dock sites" (
collect_params,lower_items) are touched by zero lines — both already key offbuilder.params(), so the shrink propagates transparently. This inverts theissue update's framing (those sites are read-only beneficiaries, not edit
sites).
Provenance: user decision, 2026-06-12 session — verbatim "Ursprünglich war B
geplant" ("B was originally planned"), followed by an explicit green-light to
proceed with B as the settled direction.
Fork:
.bind()ergonomics (slot addressing) → by param name..bind(slot: &str, value: Scalar)matches the slot against the node's declaredParamSpec.name. This is not a fresh pick: it follows the established C23authoring-address-space convention as amended in cycle 0032
(
check_param_namespace_injective) — the by-name authoring address space isinjective, while the compilat stays wired by raw index. Name at the authoring
surface, index in the compilat;
.bind()sits on the authoring side.Provenance: C23 contract + cycle-0032 amendment (design ledger), not a new
orchestrator decision.
Fork: how a named frozen strategy is exported → NOT resolved; deferred,
out of scope for this cycle. This cycle ships the
.bind()overlay only. Theissue itself frames export as a "future blueprint-as-values collection"; it
keeps its own brainstorm when picked up. The C9 boundary (a blueprint registry
stays a Rust module of blueprint values, never a by-name runtime node lookup)
is unaffected and needs no work here.