Files
Aura/crates/aura-std/src/add.rs
T
Brummel 1b3909316e feat(aura): node signature lives in the blueprint; collapse Blueprint into Composite
Consolidate the node data structure so every node's signature (NodeSchema:
inputs/output/params) is declared once and exists in the blueprint pre-build,
and dissolve the special "root graph" type. Behaviour-preserving (C1).

Signature vs sizing
- NodeSchema is now the static signature only: InputSpec -> PortSpec{kind,firing},
  with lookback removed. The signature is fully static per blueprint (input
  kinds/firing, output fields, params); LinComb's variable arity is a builder arg,
  not an injected param.
- The one param-dependent quantity, an input's buffer lookback (e.g. Sma's window =
  its injected length), moves out of the signature to Node::lookbacks() -> Vec<usize>,
  read only by bootstrap for sizing. Node::schema() is removed.
- LeafFactory -> PrimitiveBuilder, which carries the full NodeSchema. The built node
  no longer re-declares it: closes the params-declared-twice drift (#36, the 8
  per-node factory_params_match_built_node_schema lockstep tests are deleted — their
  subject is now structurally impossible) and a value-empty recipe exposes its full
  I/O interface pre-build (#43).

Root is just a bound composite
- struct Blueprint is deleted; its compile/bootstrap/param_space methods move onto
  Composite. Role gains source: Option<ScalarKind> (None = open interior port,
  Some = bound ingestion feed). A composite is runnable iff every root role is bound;
  the "main graph" is no longer a category, only the fully-source-bound composite.
  New error CompileError::UnboundRootRole for an open root role.
- BlueprintNode::signature() answers uniformly for both arms: Primitive returns the
  builder's declared schema, Composite derives it from the interior (role kinds in,
  OutField kinds out, aggregated params), pre-build, no build.

compile -> FlatGraph -> bootstrap
- compile validates structure pre-build via signature() (validate_wiring: range +
  kind, returning the same variants as before, so an edge kind fault is now caught
  before any build closure fires) and emits FlatGraph{nodes,signatures,sources,edges}.
- bootstrap consumes the FlatGraph: kinds/firing/output from the carried signatures,
  buffer depth from node.lookbacks(). SourceSpec survives as the flat descriptor.

Renames: BlueprintNode::Leaf -> Primitive, LeafFactory -> PrimitiveBuilder.

Render (aura-cli/src/graph.rs) is migrated compile-only: it takes &Composite, maps
bound roles to the same source-entry shape, so both render goldens reproduce
byte-identical output (no re-capture needed). Render-fidelity tuning is the next cycle.

Verification (orchestrator-run, not agent-reported): cargo build --workspace green;
cargo test --workspace 150 passed / 0 failed; cargo clippy --workspace --all-targets
-D warnings clean. All pinned determinism/run-output tests pass with values unchanged;
no behavioural assertion was altered to go green. 5 new tests assert the signature is
pre-build and uniform, that compile rejects a kind mismatch without building (via a
panicking builder), UnboundRootRole, and lookbacks()/signature arity agreement.

Deferred to cycle-close audit (per plan): docs/design/INDEX.md and some aura-std
module docs still name the old Node::schema()/LeafFactory/BlueprintNode::Leaf/
Blueprint::param_space contracts; prose reconciliation is the architect's at audit.

closes #43 #36
2026-06-09 12:49:22 +02:00

95 lines
2.9 KiB
Rust

//! `Add` — two-input f64 sum (input 0 plus input 1), the companion to `Sub`.
//! Combines two signal streams into one — the most basic combinator for the
//! north-star "combine one signal with another" research move (C10).
use aura_core::{Ctx, FieldSpec, Firing, Node, NodeSchema, PortSpec, PrimitiveBuilder, Scalar, ScalarKind};
/// Two-input f64 sum: input 0 plus input 1. Emits `None` until both inputs
/// have a value.
///
/// # Firing and warm-up
///
/// Both inputs are [`Firing::Any`](aura_core::Firing::Any) — a *mode-A as-of
/// join*: the node fires on every cycle in which either leg is fresh (once both
/// have produced a value), pairing the fresh leg with the held value of the
/// other. Until both legs have a value it emits `None` (no cold-leg-as-`0.0`).
/// With heterogeneous sources sharing a timestamp (same `ts` from two sources =
/// two distinct cycles, C4), a fired node emits one row per *cycle*, so a
/// recorded combined stream may carry more than one row per timestamp.
pub struct Add {
out: [Scalar; 1],
}
impl Add {
/// Build an `Add` node.
pub fn new() -> Self {
Self { out: [Scalar::F64(0.0)] }
}
/// The param-generic recipe for a blueprint primitive: paramless, builds through
/// `Add::new`.
pub fn builder() -> PrimitiveBuilder {
PrimitiveBuilder::new(
"Add",
NodeSchema {
inputs: vec![
PortSpec { kind: ScalarKind::F64, firing: Firing::Any },
PortSpec { kind: ScalarKind::F64, firing: Firing::Any },
],
output: vec![FieldSpec { name: "value", kind: ScalarKind::F64 }],
params: vec![],
},
|_| Box::new(Add::new()),
)
}
}
impl Default for Add {
fn default() -> Self {
Self::new()
}
}
impl Node for Add {
fn lookbacks(&self) -> Vec<usize> {
vec![1, 1]
}
fn eval(&mut self, ctx: Ctx<'_>) -> Option<&[Scalar]> {
let a = ctx.f64_in(0);
let b = ctx.f64_in(1);
if a.is_empty() || b.is_empty() {
return None;
}
self.out[0] = Scalar::F64(a[0] + b[0]);
Some(&self.out)
}
fn label(&self) -> String {
"Add".to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
use aura_core::{AnyColumn, Timestamp};
#[test]
fn add_is_sum_once_both_inputs_present() {
let mut add = Add::new();
let mut inputs = vec![
AnyColumn::with_capacity(ScalarKind::F64, 1),
AnyColumn::with_capacity(ScalarKind::F64, 1),
];
// only input 0 present -> None
inputs[0].push(Scalar::F64(10.0)).unwrap();
assert_eq!(add.eval(Ctx::new(&inputs, Timestamp(0))), None);
// both present -> a + b
inputs[1].push(Scalar::F64(4.0)).unwrap();
assert_eq!(add.eval(Ctx::new(&inputs, Timestamp(0))), Some([Scalar::F64(14.0)].as_slice()));
}
}