1b3909316e
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
95 lines
2.9 KiB
Rust
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()));
|
|
}
|
|
}
|