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
This commit is contained in:
@@ -16,9 +16,10 @@
|
||||
//!
|
||||
//! Delivered in cycle 0002 — the node contract:
|
||||
//!
|
||||
//! - [`Node`] — the `schema`/`eval` contract every node implements (C8), with
|
||||
//! [`NodeSchema`] / [`InputSpec`] declaring inputs (kind + lookback) and the
|
||||
//! output record ([`FieldSpec`] columns; length 1 = scalar);
|
||||
//! - [`Node`] — the `lookbacks`/`eval` contract every node implements (C8); the
|
||||
//! static signature ([`NodeSchema`] / [`PortSpec`] declaring input ports by kind
|
||||
//! and the output record of [`FieldSpec`] columns; length 1 = scalar) is declared
|
||||
//! on the node's [`PrimitiveBuilder`], pre-build;
|
||||
//! - [`Ctx`] — the per-`eval` read-side: zero-copy, financial-indexed [`Window`]
|
||||
//! access into each input (closing the cycle-0001 read-side gap on
|
||||
//! [`AnyColumn`]).
|
||||
@@ -39,5 +40,5 @@ pub use any::AnyColumn;
|
||||
pub use column::{Column, Window};
|
||||
pub use ctx::Ctx;
|
||||
pub use error::KindMismatch;
|
||||
pub use node::{FieldSpec, Firing, InputSpec, LeafFactory, Node, NodeSchema, ParamSpec};
|
||||
pub use node::{FieldSpec, Firing, Node, NodeSchema, ParamSpec, PortSpec, PrimitiveBuilder};
|
||||
pub use scalar::{Scalar, ScalarKind, Timestamp};
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
//! The node contract (C8): the interface every node implements. A node declares
|
||||
//! its inputs (each with its scalar kind, lookback depth, and firing policy, C6)
|
||||
//! 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 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.
|
||||
//! The node contract (C8): the interface every node implements. A node's static
|
||||
//! signature (its input ports — each a scalar kind + firing policy, C6 — its output
|
||||
//! record of 0..K base columns, C7, and its tunable params) is declared once on the
|
||||
//! node's `PrimitiveBuilder` (`NodeSchema`), pre-build. The one signature-adjacent
|
||||
//! quantity that may depend on an injected param — the per-input buffer lookback —
|
||||
//! is answered by `Node::lookbacks()`, read only by bootstrap. `eval` computes one
|
||||
//! cycle's row.
|
||||
//! Tunable params (C12/C19) are declared via the builder's `NodeSchema.params`
|
||||
//! (cycle 0015): each node's typed knobs, which `Composite::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};
|
||||
|
||||
@@ -24,12 +26,13 @@ pub enum Firing {
|
||||
Barrier(u8),
|
||||
}
|
||||
|
||||
/// One declared input of a node: its scalar kind, the lookback depth the engine
|
||||
/// must pre-size for it (must be >= 1), and its firing policy (C6).
|
||||
/// One declared input **port** of a node: its scalar kind and firing policy (C6).
|
||||
/// The lookback depth is NOT here — it is a build-time *sizing* concern answered by
|
||||
/// `Node::lookbacks()`, not part of the static signature (a node's lookback can
|
||||
/// depend on an injected param, e.g. `Sma`'s window = its `length`).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct InputSpec {
|
||||
pub struct PortSpec {
|
||||
pub kind: ScalarKind,
|
||||
pub lookback: usize,
|
||||
pub firing: Firing,
|
||||
}
|
||||
|
||||
@@ -57,49 +60,50 @@ pub struct ParamSpec {
|
||||
pub kind: ScalarKind,
|
||||
}
|
||||
|
||||
/// A param-generic blueprint leaf (C19): a node's declared tunable params plus a
|
||||
/// closure that builds a sized instance through the node's own constructor (the
|
||||
/// single sizing/validation gate). A blueprint holds these recipes, never built
|
||||
/// instances, so it stays value-empty until a param-set is injected (C19/C23).
|
||||
pub struct LeafFactory {
|
||||
/// A param-generic blueprint **primitive** recipe (C19): a node's full declared
|
||||
/// signature (`NodeSchema` — inputs/output/params) plus a closure that builds a
|
||||
/// sized instance through the node's own constructor (the single sizing/validation
|
||||
/// gate). A blueprint holds these recipes, never built instances, so it stays
|
||||
/// value-empty until a param-set is injected (C19/C23). The signature is declared
|
||||
/// here ONCE — the built node no longer re-declares it (closes the param-declared-
|
||||
/// twice drift, #36) and a value-empty recipe now exposes its full I/O interface
|
||||
/// pre-build (#43).
|
||||
pub struct PrimitiveBuilder {
|
||||
name: &'static str,
|
||||
params: Vec<ParamSpec>,
|
||||
schema: NodeSchema,
|
||||
// The build closure's type is exactly the recipe contract (a param slice in, a
|
||||
// boxed node out); a type alias would not clarify it.
|
||||
#[allow(clippy::type_complexity)]
|
||||
build: Box<dyn Fn(&[Scalar]) -> Box<dyn Node>>,
|
||||
}
|
||||
|
||||
impl LeafFactory {
|
||||
impl PrimitiveBuilder {
|
||||
/// `name` is the param-generic render label (the node type, e.g. `"SMA"`);
|
||||
/// `params` the declared knobs; `build` constructs a sized node from a
|
||||
/// `schema` the full declared signature; `build` constructs a sized node from a
|
||||
/// kind-checked param slice.
|
||||
pub fn new(
|
||||
name: &'static str,
|
||||
params: Vec<ParamSpec>,
|
||||
schema: NodeSchema,
|
||||
build: impl Fn(&[Scalar]) -> Box<dyn Node> + 'static,
|
||||
) -> Self {
|
||||
Self { name, params, build: Box::new(build) }
|
||||
Self { name, schema, build: Box::new(build) }
|
||||
}
|
||||
/// The declared tunable params (read by `Blueprint::param_space`, pre-build).
|
||||
/// The full declared signature (read pre-build by `Composite::param_space`,
|
||||
/// `BlueprintNode::signature`, and the renderer).
|
||||
pub fn schema(&self) -> &NodeSchema {
|
||||
&self.schema
|
||||
}
|
||||
/// The declared tunable params (a view into the signature; pre-build).
|
||||
pub fn params(&self) -> &[ParamSpec] {
|
||||
&self.params
|
||||
&self.schema.params
|
||||
}
|
||||
/// Build a sized node from its param slice (the slice is kind-checked by the
|
||||
/// caller before this runs).
|
||||
pub fn build(&self, params: &[Scalar]) -> Box<dyn Node> {
|
||||
(self.build)(params)
|
||||
}
|
||||
/// The param-generic render label for the blueprint view (C22 "structure
|
||||
/// before"): just the node type, e.g. `SMA`. A value-empty recipe has no values
|
||||
/// to show; the tunable knobs are surfaced by `Blueprint::param_space`. The
|
||||
/// factory label stays the bare type because alias / handle names are a
|
||||
/// *composite-level* concept — the renderer folds them into the leaf label at
|
||||
/// the composite boundary (`render_definition`'s `leaf_label`), where the
|
||||
/// `(node, slot)` → name mapping lives, not on the standalone factory. (Wide
|
||||
/// labels are safe: both `aura graph` views are flat since cycle 0017 — no
|
||||
/// cluster subgraph, so no sibling-overlap garble.) The compiled view labels the
|
||||
/// built node valued (`SMA(2)`) via `Node::label`, unaffected.
|
||||
/// The param-generic render label for the blueprint view (C22): just the node
|
||||
/// type, e.g. `SMA`.
|
||||
pub fn label(&self) -> String {
|
||||
self.name.to_string()
|
||||
}
|
||||
@@ -112,7 +116,7 @@ impl LeafFactory {
|
||||
/// the hot path — the `Vec`s are fine here.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct NodeSchema {
|
||||
pub inputs: Vec<InputSpec>,
|
||||
pub inputs: Vec<PortSpec>,
|
||||
pub output: Vec<FieldSpec>,
|
||||
pub params: Vec<ParamSpec>,
|
||||
}
|
||||
@@ -120,15 +124,22 @@ pub struct NodeSchema {
|
||||
/// The universal composable dataflow unit (C8): a **producer, consumer, or both**.
|
||||
/// A producer/transformer exposes one output port carrying a record of 1..K base
|
||||
/// columns; a **pure consumer (sink)** declares `output: vec![]` and records via
|
||||
/// an out-of-graph side effect in `eval` (cycle 0006). `schema` declares the
|
||||
/// interface; `eval` computes one cycle's row, returning a borrowed slice into a
|
||||
/// buffer the node owns. `None` = filter / not-yet-warmed-up / pure sink; a
|
||||
/// returned `Some(row)` must satisfy `row.len() == schema().output.len()` (so a
|
||||
/// an out-of-graph side effect in `eval` (cycle 0006). The static signature is
|
||||
/// declared on the node's `PrimitiveBuilder`; `eval` computes one cycle's row,
|
||||
/// returning a borrowed slice into a buffer the node owns. `None` = filter /
|
||||
/// not-yet-warmed-up / pure sink; a returned `Some(row)` must satisfy
|
||||
/// `row.len() == signature().output.len()` (so a
|
||||
/// pure consumer returns `None` or an empty `Some(&[])`) — the engine debug-asserts
|
||||
/// the width and forwards the row field-wise along the node's out-edges. `&mut self`
|
||||
/// because a node may keep its own derived state (and its output buffer).
|
||||
pub trait Node {
|
||||
fn schema(&self) -> NodeSchema;
|
||||
/// The per-input buffer **lookback** depth (each `>= 1`), in input-slot order —
|
||||
/// the only signature-adjacent quantity that may depend on an injected param
|
||||
/// (e.g. `Sma`'s window = its `length`). Read once by `Harness::bootstrap` to
|
||||
/// size each input column; never on the hot path. Length MUST equal the node's
|
||||
/// declared `signature().inputs.len()`.
|
||||
fn lookbacks(&self) -> Vec<usize>;
|
||||
|
||||
fn eval(&mut self, ctx: Ctx<'_>) -> Option<&[Scalar]>;
|
||||
/// A one-line, **non-load-bearing** render label (C23): a debug symbol for
|
||||
/// tracing / graph rendering (#13), never read by the run loop and never
|
||||
@@ -147,11 +158,11 @@ pub trait Node {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// A minimal node with an empty schema, reused across the label / factory tests.
|
||||
/// A minimal node with no inputs, reused across the label / builder tests.
|
||||
struct Bare;
|
||||
impl Node for Bare {
|
||||
fn schema(&self) -> NodeSchema {
|
||||
NodeSchema { inputs: vec![], output: vec![], params: vec![] }
|
||||
fn lookbacks(&self) -> Vec<usize> {
|
||||
vec![]
|
||||
}
|
||||
fn eval(&mut self, _ctx: Ctx<'_>) -> Option<&[Scalar]> {
|
||||
None
|
||||
@@ -159,9 +170,9 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn input_spec_carries_firing() {
|
||||
let a = InputSpec { kind: ScalarKind::F64, lookback: 1, firing: Firing::Any };
|
||||
let b = InputSpec { kind: ScalarKind::F64, lookback: 2, firing: Firing::Barrier(0) };
|
||||
fn port_spec_carries_firing() {
|
||||
let a = PortSpec { kind: ScalarKind::F64, firing: Firing::Any };
|
||||
let b = PortSpec { kind: ScalarKind::F64, firing: Firing::Barrier(0) };
|
||||
assert_eq!(a.firing, Firing::Any);
|
||||
assert_eq!(b.firing, Firing::Barrier(0));
|
||||
assert_ne!(a.firing, b.firing);
|
||||
@@ -174,34 +185,52 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn leaf_factory_label_is_the_bare_type() {
|
||||
fn primitive_builder_label_is_the_bare_type() {
|
||||
// param-generic: the label is just the node type, no knob suffix (the
|
||||
// ascii-dag blueprint view cannot render wide cluster-sibling labels).
|
||||
let with = LeafFactory::new(
|
||||
let with = PrimitiveBuilder::new(
|
||||
"SMA",
|
||||
vec![ParamSpec { name: "length".into(), kind: ScalarKind::I64 }],
|
||||
NodeSchema {
|
||||
inputs: vec![],
|
||||
output: vec![],
|
||||
params: vec![ParamSpec { name: "length".into(), kind: ScalarKind::I64 }],
|
||||
},
|
||||
|_| Box::new(Bare),
|
||||
);
|
||||
assert_eq!(with.label(), "SMA");
|
||||
let none = LeafFactory::new("Sub", vec![], |_| Box::new(Bare));
|
||||
let none = PrimitiveBuilder::new(
|
||||
"Sub",
|
||||
NodeSchema { inputs: vec![], output: vec![], params: vec![] },
|
||||
|_| Box::new(Bare),
|
||||
);
|
||||
assert_eq!(none.label(), "Sub");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn leaf_factory_build_runs_the_closure() {
|
||||
let f = LeafFactory::new("Bare", vec![], |_| Box::new(Bare));
|
||||
assert_eq!(f.build(&[]).schema().params, Vec::<ParamSpec>::new());
|
||||
fn primitive_builder_build_runs_the_closure() {
|
||||
let f = PrimitiveBuilder::new(
|
||||
"Bare",
|
||||
NodeSchema { inputs: vec![], output: vec![], params: vec![] },
|
||||
|_| Box::new(Bare),
|
||||
);
|
||||
assert_eq!(f.params(), Vec::<ParamSpec>::new());
|
||||
assert_eq!(f.build(&[]).lookbacks(), Vec::<usize>::new());
|
||||
}
|
||||
|
||||
#[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);
|
||||
let b = PrimitiveBuilder::new(
|
||||
"SMA",
|
||||
NodeSchema {
|
||||
inputs: vec![],
|
||||
output: vec![],
|
||||
params: vec![ParamSpec { name: "length".into(), kind: ScalarKind::I64 }],
|
||||
},
|
||||
|_| Box::new(Bare),
|
||||
);
|
||||
assert_eq!(
|
||||
b.schema().params,
|
||||
vec![ParamSpec { name: "length".into(), kind: ScalarKind::I64 }]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user