spec: 0016 amend — value-empty render surface + vestigial schema removal (#31)

Plan-recon surfaced two surfaces the first draft under-scoped; resolved here after
a user design decision on the render form.

- Blueprint rendering (C22): a value-empty blueprint has no bound values, so the
  `aura graph` blueprint view can no longer label a leaf `SMA(2)`. LeafFactory gains
  `name` + `label()`, and the blueprint view renders the param-generic form
  `SMA(length)` (type + tunable knob names, no values), chosen over a bare `SMA` to
  surface the knobs the milestone is about. The compiled view (post-build flat
  nodes) is unchanged — it still labels valued. Correct C22 reading: structure
  before, values after.
- Vestigial pre-build interface removed: `Composite::schema` / `BlueprintNode::schema`
  derive an item interface from built interior leaves; a value-empty leaf has none,
  and both methods have no live caller (compile resolves every interface structurally
  on the built flat nodes). Removed with their unit test. This is why the factory
  carries no io skeleton — the build-then-wire decision stands, no schema duplication.
- OQ3: the swapped-sample mis-wiring moves from the builder args to the injected
  vector; its observability moves to the compiled view (the blueprint view is
  param-generic and identical for both orderings).

Grounding-check re-run: PASS (LeafFactory::label internally consistent; the
no-live-caller claim for the removed methods verified; blueprint-view vs
compiled-view goldens correctly partitioned).

refs #31
This commit is contained in:
2026-06-07 20:11:06 +02:00
parent b9edeaf2db
commit e9eea1ada3
+46 -4
View File
@@ -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<ParamSpec>,
build: Box<dyn Fn(&[Scalar]) -> Box<dyn Node>>,
}
impl LeafFactory {
pub fn new(
name: &'static str,
params: Vec<ParamSpec>,
build: impl Fn(&[Scalar]) -> Box<dyn Node> + '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<dyn Node> { (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<i64>` and `as_f64(self) -> Option<f64>` (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<N>``From<LeafFactory>`; `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<N>``From<LeafFactory>`; `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