diff --git a/docs/specs/0016-param-set-injection.md b/docs/specs/0016-param-set-injection.md index 2d4ec6e..1cd55d9 100644 --- a/docs/specs/0016-param-set-injection.md +++ b/docs/specs/0016-param-set-injection.md @@ -58,6 +58,27 @@ Determinism (C1) is preserved: the same vector against the same blueprint produc a bit-identical run, because building is a pure function of the param slice and the lowering/wiring is unchanged in structure. +**Blueprint rendering (C22 "structure before").** A value-empty blueprint has no +bound values, so the `aura graph` *blueprint view* (`render_blueprint`, pre-inline +cluster boxes) can no longer label a leaf `SMA(2)` — there is no `2` until a vector +is injected. The view instead renders the **param-generic** label from +`LeafFactory::label()`: the node type plus its tunable knob *names*, e.g. +`SMA(length)`, `Exposure(scale)`, bare `SimBroker`. Both SMAs of a cross render +identically as `SMA(length)` (they are the same recipe; their distinct values live +in the vector, and the graph's edges still distinguish them positionally). The +*compiled view* is unaffected: it renders post-build flat nodes via the unchanged +`Node::label()`, so a bound `SMA(2)` still shows there. This is the correct C22 +reading — structure (param-generic) before a run, values (bound) after. + +**Vestigial pre-build interface, removed.** `Composite::schema()` and the private +`BlueprintNode::schema()` derive a blueprint item's interface from its interior +*built* leaves' `schema()`. A value-empty leaf has no built node, and these methods +have no live caller — `compile` resolves every interface structurally on the built +flat nodes (`slot_kind`, the output-field range checks), not on pre-build item +schemas. They are removed (with their one dedicated unit test, +`composite_schema_derives_role_and_output_kinds`); this is why the factory carries +no input/output skeleton (the "build-then-wire" decision, above). + ## Concrete code shapes ### The user-facing program (the acceptance evidence) @@ -100,6 +121,7 @@ impl Sma { /// 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)), ) @@ -130,19 +152,33 @@ The new construction-contract type, in aura-core beside `Node`/`ParamSpec`: ```rust // crates/aura-core/src/node.rs (new) pub struct LeafFactory { + name: &'static str, params: Vec, build: Box Box>, } impl LeafFactory { pub fn new( + name: &'static str, params: Vec, build: impl Fn(&[Scalar]) -> Box + 'static, ) -> Self { - Self { params, build: Box::new(build) } + Self { name, params, build: Box::new(build) } } pub fn params(&self) -> &[ParamSpec] { &self.params } pub fn build(&self, p: &[Scalar]) -> Box { (self.build)(p) } + /// 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(", ")) + } + } } ``` @@ -211,12 +247,13 @@ pub enum CompileError { | Crate / file | Change | |---|---| -| `crates/aura-core/src/node.rs` | New `LeafFactory { params, build }` (+ `new`/`params`/`build`). `Node` trait unchanged — construction lives in the closure, not a new trait method. | +| `crates/aura-core/src/node.rs` | New `LeafFactory { name, params, build }` (+ `new`/`params`/`build`/`label`). `label()` renders the param-generic blueprint-view label from `name` + param knob names. `Node` trait unchanged — construction lives in the closure, not a new trait method. | | `crates/aura-core/src/scalar.rs` | New value accessors `Scalar::as_i64(self) -> Option` and `as_f64(self) -> Option` (mirroring `Scalar::kind`), used by the build closures to read the kind-checked slice. | | `crates/aura-core/src/lib.rs` | Re-export `LeafFactory`. | | `crates/aura-std/src/*.rs` | Each node gains `fn factory() -> LeafFactory`. With params: `Sma` (`length:I64`), `Exposure` (`scale:F64`), `LinComb::factory(arity)` declares `arity` × `weights[i]:F64` (the arity is topology — fixed per blueprint, C19 — taken as a factory arg; only the weight *values* are injected). Paramless: `Sub`, `Add`, `SimBroker`, `Recorder` (`params: vec![]`; their `build` closures forward the non-param construction args these nodes already take, e.g. the `Recorder` channel — captured by the closure). | -| `crates/aura-engine/src/blueprint.rs` | `BlueprintNode::Leaf(LeafFactory)`; `From` → `From`; `collect_params` reads `factory.params()`; `compile` → `compile_with_params(&self, &[Scalar])` (build-then-wire); `bootstrap_with_params`; two `CompileError` variants. `compile`/`bootstrap` no-param forms are retained as thin wrappers over the param-driven path with an empty vector (valid only when no params are declared). | -| Fixtures / CLI sample | `sma_cross(2, 4)` → `sma_cross()` (factory leaves); fixtures and the `aura run`/`aura graph` sample supply their point as a vector. Bit-identity and golden tests re-expressed against `bootstrap_with_params`. | +| `crates/aura-engine/src/blueprint.rs` | `BlueprintNode::Leaf(LeafFactory)`; `From` → `From`; `collect_params` reads `factory.params()`; `compile` → `compile_with_params(&self, &[Scalar])` (build-then-wire); `bootstrap_with_params`; two `CompileError` variants. `compile`/`bootstrap` no-param forms are retained as thin wrappers over the param-driven path with an empty vector (valid only when no params are declared). Removes the vestigial `Composite::schema` / `BlueprintNode::schema` (no live caller — interface resolution is structural on built flat nodes) and their unit test. | +| `crates/aura-cli/src/graph.rs` | `render_blueprint`'s per-leaf label comes from `LeafFactory::label()` (param-generic) instead of a built node's `Node::label()`. The compiled view is unchanged (post-build flat nodes still label valued). | +| Fixtures / CLI sample | `sma_cross(2, 4)` → `sma_cross()` (factory leaves); fixtures and the `aura run`/`aura graph` sample supply their point as a vector. `sample_blueprint_swapped` expresses its mis-wiring by swapping the injected vector, not the builder args (OQ3). Bit-identity, blueprint-view, and compiled-view golden tests re-expressed (blueprint-view labels become param-generic; compiled-view labels stay valued). | **Param declaration lives in two honest places.** `factory.params` (the param-generic recipe, read by `param_space()` pre-build) and the built node's @@ -281,6 +318,11 @@ not the node (C20, established at #30's close), and full domain validation is flat-node order (the #34 nested case, re-pinned in the unified form). - **Paramless harness.** `bootstrap_with_params(vec![])` on a paramless blueprint bootstraps and runs; a non-empty vector against it returns `ParamArity`. +- **Blueprint-view render (C22).** `render_blueprint` on a value-empty harness + renders param-generic labels (`SMA(length)`, `Exposure(scale)`, bare `SimBroker`); + the compiled view, run on built flat nodes, still renders valued labels + (`SMA(2)`). The `aura graph` golden tests are re-expressed accordingly + (blueprint-view goldens become param-generic; compiled-view goldens unchanged). ## Acceptance criteria