feat(aura-core,aura-std,aura-engine): declare node tunable params (#30)

Cycle A of milestone 'The World — parameter-space & sweep'. A node now declares
its tunable parameters in its C8 schema, and a blueprint aggregates them into one
flat, inspectable param-space — the root that unlocks the C12 orchestration axes
(#31 bind, #32 sweep), filling the gap C8/C23 name 'deliberately not in the schema
yet'.

- aura-core: ParamSpec { name: String, kind: ScalarKind } as a third schema-
  declaration type; NodeSchema gains a third field 'params'. name is String (not
  &'static like FieldSpec) because a vector knob carries a runtime index and
  aggregation prefixes the composite path.
- aura-std: Sma declares [length:I64], Exposure [scale:F64], LinComb expands to N
  flat [weights[i]:F64] (N = its input arity, topology-fixed per C19); Sub/Add/
  SimBroker/Recorder declare none (pip_size is metadata, C10/C15; Recorder is
  wiring).
- aura-engine: Blueprint::param_space() walks the graph-as-data depth-first in
  lower_items order, path-qualifying via the already-public Composite::name() — a
  read-only projection (C9). compile/inline_composite/lower_items are untouched, so
  the compilat stays bit-identical (composite_sma_cross_runs_bit_identical_to_hand_wired
  and the golden render tests stay green).

Design: param identity is positional (slot after the deterministic inline order,
C23 'by index not name'); the path-qualified name is a non-load-bearing debug
symbol — same-type siblings in one composite share a name, uniqueness is at the
slot. Flat over a structured arity-bearing ParamSpec because flattening is
unavoidable (a sweep enumerates a flat point-space) and structure-in-the-runtime
is the nested-composite reading C23 rejects (see spec 0015 for the full rationale).

Scope is declaration + aggregation + inspection only; binding (#31), sweep
enumeration (#32), search-range, validity-constraint, and default-range/slider are
deferred. An E2E test pins the load-bearing C23/#31 invariant: param_space() slot
order mirrors compile()'s flat-node param order, kind-by-slot, on the SMA-cross
harness. fieldtests/ (excluded crates) left as frozen snapshots.

Verified: cargo build/test --workspace green (64 tests), clippy --all-targets -D
warnings clean; compile/inline diff empty.

closes #30
This commit is contained in:
2026-06-07 15:29:58 +02:00
parent 6911fa52ed
commit 931109df58
12 changed files with 240 additions and 14 deletions
+1 -1
View File
@@ -39,5 +39,5 @@ pub use any::AnyColumn;
pub use column::{Column, Window};
pub use ctx::Ctx;
pub use error::KindMismatch;
pub use node::{FieldSpec, Firing, InputSpec, Node, NodeSchema};
pub use node::{FieldSpec, Firing, InputSpec, Node, NodeSchema, ParamSpec};
pub use scalar::{Scalar, ScalarKind, Timestamp};
+32 -3
View File
@@ -3,8 +3,10 @@
//! and its output record (0..K base columns, C7; an empty output declares a
//! pure consumer / sink role, C8) via `schema`, and computes one cycle's row
//! via `eval`.
//! Tunable params (C12/C19) are deliberately not part of the schema yet — see
//! spec 0002's "Out of scope".
//! Tunable params (C12/C19) are declared via `params` (cycle 0015): each node's
//! typed knobs, which `Blueprint::param_space` aggregates into the sweep's flat,
//! path-qualified param-space (C8/C23). Identity is positional (slot); the name is
//! a non-load-bearing debug symbol.
use crate::{Ctx, Scalar, ScalarKind};
@@ -41,6 +43,20 @@ pub struct FieldSpec {
pub kind: ScalarKind,
}
/// One declared tunable parameter of a node (C8/C12): its render name and scalar
/// kind. The name is a **non-load-bearing** debug symbol (path-qualified at
/// aggregation, as `FieldSpec.name` already is); a param's identity is its
/// positional slot in the blueprint's aggregated param-space (C23 — by index, not
/// by name). Unlike `FieldSpec.name` (`&'static str`), the name is a `String`: a
/// vector knob carries a runtime index (`weights[0]`) and aggregation prefixes the
/// composite path (`strategy.weights[0]`). Permitted kinds: `I64`/`F64`/`Bool`;
/// `Timestamp` is a structural axis (C20), never a numeric knob.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ParamSpec {
pub name: String,
pub kind: ScalarKind,
}
/// A node's declared interface: its inputs (in order) and its output record — an
/// ordered list of named base columns; length 1 is a scalar (the degenerate
/// case), and an **empty** `output` (`vec![]`) declares a **pure consumer**
@@ -50,6 +66,7 @@ pub struct FieldSpec {
pub struct NodeSchema {
pub inputs: Vec<InputSpec>,
pub output: Vec<FieldSpec>,
pub params: Vec<ParamSpec>,
}
/// The universal composable dataflow unit (C8): a **producer, consumer, or both**.
@@ -97,7 +114,7 @@ mod tests {
struct Bare;
impl Node for Bare {
fn schema(&self) -> NodeSchema {
NodeSchema { inputs: vec![], output: vec![] }
NodeSchema { inputs: vec![], output: vec![], params: vec![] }
}
fn eval(&mut self, _ctx: Ctx<'_>) -> Option<&[Scalar]> {
None
@@ -105,4 +122,16 @@ mod tests {
}
assert_eq!(Bare.label(), "node");
}
#[test]
fn schema_carries_declared_params() {
let s = NodeSchema {
inputs: vec![],
output: vec![],
params: vec![ParamSpec { name: "length".into(), kind: ScalarKind::I64 }],
};
assert_eq!(s.params.len(), 1);
assert_eq!(s.params[0].name, "length");
assert_eq!(s.params[0].kind, ScalarKind::I64);
}
}