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:
+647
-295
File diff suppressed because it is too large
Load Diff
@@ -21,7 +21,7 @@
|
||||
//! sources are four cycles, so RustAst's `cycle_id`-equality barrier could never
|
||||
//! fire across sources — the timestamp generalizes it faithfully.
|
||||
|
||||
use aura_core::{AnyColumn, Ctx, Firing, Node, Scalar, ScalarKind, Timestamp};
|
||||
use aura_core::{AnyColumn, Ctx, Firing, Node, NodeSchema, Scalar, ScalarKind, Timestamp};
|
||||
|
||||
/// Forwards one field (`from_field`) of a producer's output record into a
|
||||
/// consumer's input slot. Consuming a whole record is N such edges, one per field
|
||||
@@ -51,6 +51,18 @@ pub struct SourceSpec {
|
||||
pub targets: Vec<Target>,
|
||||
}
|
||||
|
||||
/// The flat, type-erased, index-wired output of `Composite::compile` — the target
|
||||
/// `Harness::bootstrap` consumes (C23's "compilat", now a named type). `signatures[i]`
|
||||
/// is the static signature of `nodes[i]` (gathered from each primitive's builder at
|
||||
/// lowering); `bootstrap` reads kinds/output from it and `nodes[i].lookbacks()` for
|
||||
/// buffer depth. `sources` are the lowered bound roles, in role-declaration order.
|
||||
pub struct FlatGraph {
|
||||
pub nodes: Vec<Box<dyn Node>>,
|
||||
pub signatures: Vec<NodeSchema>,
|
||||
pub sources: Vec<SourceSpec>,
|
||||
pub edges: Vec<Edge>,
|
||||
}
|
||||
|
||||
/// A wiring fault caught once, at bootstrap — C7's "type check paid at wiring"
|
||||
/// generalized to the whole topology.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
@@ -107,41 +119,41 @@ impl core::fmt::Debug for Harness {
|
||||
}
|
||||
|
||||
impl Harness {
|
||||
/// Bind nodes + wiring into a frozen, runnable graph. Sizes each node's input
|
||||
/// columns from its `schema`, lifts each input's firing policy, initializes
|
||||
/// per-slot freshness state, kind-checks every source target and edge, and
|
||||
/// topologically orders the nodes (Kahn), rejecting any directed cycle.
|
||||
pub fn bootstrap(
|
||||
nodes: Vec<Box<dyn Node>>,
|
||||
sources: Vec<SourceSpec>,
|
||||
edges: Vec<Edge>,
|
||||
) -> Result<Harness, BootstrapError> {
|
||||
/// Bind a flat graph into a frozen, runnable graph. Sizes each node's input
|
||||
/// columns — KIND/firing from its carried signature, DEPTH from the built node's
|
||||
/// `lookbacks()` — lifts each input's firing policy, initializes per-slot
|
||||
/// freshness state, kind-checks every source target and edge, and topologically
|
||||
/// orders the nodes (Kahn), rejecting any directed cycle.
|
||||
pub fn bootstrap(flat: FlatGraph) -> Result<Harness, BootstrapError> {
|
||||
let FlatGraph { nodes, signatures, sources, edges } = flat;
|
||||
let n = nodes.len();
|
||||
|
||||
let schemas: Vec<_> = nodes.iter().map(|nd| nd.schema()).collect();
|
||||
|
||||
// size each node's input columns from its schema; lift firing; init slots
|
||||
// size each node's input columns: KIND/firing from the carried signature,
|
||||
// DEPTH from the built node's lookbacks() (the one param-dependent quantity)
|
||||
let mut boxes: Vec<NodeBox> = Vec::with_capacity(n);
|
||||
for (nd, schema) in nodes.into_iter().zip(schemas.iter()) {
|
||||
let inputs: Vec<AnyColumn> = schema
|
||||
for (nd, sig) in nodes.into_iter().zip(signatures.iter()) {
|
||||
let depths = nd.lookbacks();
|
||||
debug_assert_eq!(depths.len(), sig.inputs.len(), "lookbacks() arity == signature inputs");
|
||||
let inputs: Vec<AnyColumn> = sig
|
||||
.inputs
|
||||
.iter()
|
||||
.map(|spec| AnyColumn::with_capacity(spec.kind, spec.lookback))
|
||||
.zip(depths)
|
||||
.map(|(spec, depth)| AnyColumn::with_capacity(spec.kind, depth))
|
||||
.collect();
|
||||
let firing: Vec<Firing> = schema.inputs.iter().map(|spec| spec.firing).collect();
|
||||
let slots: Vec<SlotState> = schema
|
||||
let firing: Vec<Firing> = sig.inputs.iter().map(|spec| spec.firing).collect();
|
||||
let slots: Vec<SlotState> = sig
|
||||
.inputs
|
||||
.iter()
|
||||
.map(|_| SlotState { fresh_at: 0, last_ts: Timestamp(i64::MIN) })
|
||||
.collect();
|
||||
let out_len = schema.output.len();
|
||||
let out_len = sig.output.len();
|
||||
boxes.push(NodeBox { node: nd, inputs, firing, slots, out_len });
|
||||
}
|
||||
|
||||
// source targets: each source's value must match each of its target slots' kind
|
||||
for src in &sources {
|
||||
for t in &src.targets {
|
||||
let s = schemas.get(t.node).ok_or(BootstrapError::BadIndex)?;
|
||||
let s = signatures.get(t.node).ok_or(BootstrapError::BadIndex)?;
|
||||
let slot = s.inputs.get(t.slot).ok_or(BootstrapError::BadIndex)?;
|
||||
if slot.kind != src.kind {
|
||||
return Err(BootstrapError::KindMismatch {
|
||||
@@ -155,8 +167,8 @@ impl Harness {
|
||||
// edges: indices in range, producer output field kind == consumer slot kind
|
||||
let mut out_edges: Vec<Vec<Edge>> = vec![Vec::new(); n];
|
||||
for &e in &edges {
|
||||
let from = schemas.get(e.from).ok_or(BootstrapError::BadIndex)?;
|
||||
let to = schemas.get(e.to).ok_or(BootstrapError::BadIndex)?;
|
||||
let from = signatures.get(e.from).ok_or(BootstrapError::BadIndex)?;
|
||||
let to = signatures.get(e.to).ok_or(BootstrapError::BadIndex)?;
|
||||
let field = from.output.get(e.from_field).ok_or(BootstrapError::BadIndex)?;
|
||||
let slot = to.inputs.get(e.slot).ok_or(BootstrapError::BadIndex)?;
|
||||
if field.kind != slot.kind {
|
||||
@@ -333,10 +345,9 @@ fn group_id(f: &Firing) -> Option<u8> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
// InputSpec / NodeSchema are not imported by harness.rs production code (it
|
||||
// only reads `nd.schema()` fields, never naming the types), so they are not
|
||||
// brought in by `use super::*` — the fixtures construct them, so import here.
|
||||
use aura_core::{FieldSpec, InputSpec, NodeSchema};
|
||||
// PortSpec / NodeSchema name the fixtures' declared signatures, brought in here
|
||||
// (production code reads them via the carried signatures, never naming the types).
|
||||
use aura_core::{FieldSpec, NodeSchema, PortSpec};
|
||||
use aura_std::{Exposure, Recorder, Sma, SimBroker, Sub};
|
||||
use std::sync::mpsc;
|
||||
|
||||
@@ -345,6 +356,33 @@ mod tests {
|
||||
points.iter().map(|&(t, v)| (Timestamp(t), Scalar::F64(v))).collect()
|
||||
}
|
||||
|
||||
/// One f64 input port with the given firing policy (lookback 1 is the bootstrap
|
||||
/// default for these test fixtures, supplied by their `lookbacks()`).
|
||||
fn f64_port(firing: Firing) -> PortSpec {
|
||||
PortSpec { kind: ScalarKind::F64, firing }
|
||||
}
|
||||
|
||||
/// Bootstrap a hand-wired graph from boxed nodes + their declared signatures.
|
||||
/// The signatures parallel `nodes` (one per node, in order); bootstrap reads
|
||||
/// kinds/firing from them and depth from each node's `lookbacks()`.
|
||||
fn boot(
|
||||
nodes: Vec<Box<dyn Node>>,
|
||||
signatures: Vec<NodeSchema>,
|
||||
sources: Vec<SourceSpec>,
|
||||
edges: Vec<Edge>,
|
||||
) -> Result<Harness, BootstrapError> {
|
||||
Harness::bootstrap(FlatGraph { nodes, signatures, sources, edges })
|
||||
}
|
||||
|
||||
/// The declared signature of a `Recorder` over `kinds` with the given firing.
|
||||
fn recorder_sig(kinds: &[ScalarKind], firing: Firing) -> NodeSchema {
|
||||
NodeSchema {
|
||||
inputs: kinds.iter().map(|&kind| PortSpec { kind, firing }).collect(),
|
||||
output: vec![],
|
||||
params: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
// --- firing-policy fixtures (test-local; not library nodes — C9: examples
|
||||
// for the engine's own tests, no speculative aura-std surface) ---
|
||||
|
||||
@@ -353,17 +391,19 @@ mod tests {
|
||||
struct AsOfSum {
|
||||
out: [Scalar; 1],
|
||||
}
|
||||
impl Node for AsOfSum {
|
||||
fn schema(&self) -> NodeSchema {
|
||||
impl AsOfSum {
|
||||
fn sig() -> NodeSchema {
|
||||
NodeSchema {
|
||||
inputs: vec![
|
||||
InputSpec { kind: ScalarKind::F64, lookback: 1, firing: Firing::Any },
|
||||
InputSpec { kind: ScalarKind::F64, lookback: 1, firing: Firing::Any },
|
||||
],
|
||||
inputs: vec![f64_port(Firing::Any), f64_port(Firing::Any)],
|
||||
output: vec![FieldSpec { name: "value", kind: ScalarKind::F64 }],
|
||||
params: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Node for AsOfSum {
|
||||
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);
|
||||
@@ -380,17 +420,19 @@ mod tests {
|
||||
struct BarrierSum {
|
||||
out: [Scalar; 1],
|
||||
}
|
||||
impl Node for BarrierSum {
|
||||
fn schema(&self) -> NodeSchema {
|
||||
impl BarrierSum {
|
||||
fn sig() -> NodeSchema {
|
||||
NodeSchema {
|
||||
inputs: vec![
|
||||
InputSpec { kind: ScalarKind::F64, lookback: 1, firing: Firing::Barrier(0) },
|
||||
InputSpec { kind: ScalarKind::F64, lookback: 1, firing: Firing::Barrier(0) },
|
||||
],
|
||||
inputs: vec![f64_port(Firing::Barrier(0)), f64_port(Firing::Barrier(0))],
|
||||
output: vec![FieldSpec { name: "value", kind: ScalarKind::F64 }],
|
||||
params: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Node for BarrierSum {
|
||||
fn lookbacks(&self) -> Vec<usize> {
|
||||
vec![1, 1]
|
||||
}
|
||||
fn eval(&mut self, ctx: Ctx<'_>) -> Option<&[Scalar]> {
|
||||
self.out[0] = Scalar::F64(ctx.f64_in(0)[0] + ctx.f64_in(1)[0]);
|
||||
Some(&self.out)
|
||||
@@ -403,18 +445,23 @@ mod tests {
|
||||
struct MixedSum {
|
||||
out: [Scalar; 1],
|
||||
}
|
||||
impl Node for MixedSum {
|
||||
fn schema(&self) -> NodeSchema {
|
||||
impl MixedSum {
|
||||
fn sig() -> NodeSchema {
|
||||
NodeSchema {
|
||||
inputs: vec![
|
||||
InputSpec { kind: ScalarKind::F64, lookback: 1, firing: Firing::Barrier(0) },
|
||||
InputSpec { kind: ScalarKind::F64, lookback: 1, firing: Firing::Barrier(0) },
|
||||
InputSpec { kind: ScalarKind::F64, lookback: 1, firing: Firing::Any },
|
||||
f64_port(Firing::Barrier(0)),
|
||||
f64_port(Firing::Barrier(0)),
|
||||
f64_port(Firing::Any),
|
||||
],
|
||||
output: vec![FieldSpec { name: "value", kind: ScalarKind::F64 }],
|
||||
params: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Node for MixedSum {
|
||||
fn lookbacks(&self) -> Vec<usize> {
|
||||
vec![1, 1, 1]
|
||||
}
|
||||
fn eval(&mut self, ctx: Ctx<'_>) -> Option<&[Scalar]> {
|
||||
let a = ctx.f64_in(0);
|
||||
let b = ctx.f64_in(1);
|
||||
@@ -434,16 +481,10 @@ mod tests {
|
||||
struct Ohlcv {
|
||||
out: [Scalar; 5],
|
||||
}
|
||||
impl Node for Ohlcv {
|
||||
fn schema(&self) -> NodeSchema {
|
||||
impl Ohlcv {
|
||||
fn sig() -> NodeSchema {
|
||||
NodeSchema {
|
||||
inputs: vec![
|
||||
InputSpec { kind: ScalarKind::F64, lookback: 1, firing: Firing::Barrier(0) },
|
||||
InputSpec { kind: ScalarKind::F64, lookback: 1, firing: Firing::Barrier(0) },
|
||||
InputSpec { kind: ScalarKind::F64, lookback: 1, firing: Firing::Barrier(0) },
|
||||
InputSpec { kind: ScalarKind::F64, lookback: 1, firing: Firing::Barrier(0) },
|
||||
InputSpec { kind: ScalarKind::F64, lookback: 1, firing: Firing::Barrier(0) },
|
||||
],
|
||||
inputs: vec![f64_port(Firing::Barrier(0)); 5],
|
||||
output: vec![
|
||||
FieldSpec { name: "open", kind: ScalarKind::F64 },
|
||||
FieldSpec { name: "high", kind: ScalarKind::F64 },
|
||||
@@ -454,6 +495,11 @@ mod tests {
|
||||
params: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Node for Ohlcv {
|
||||
fn lookbacks(&self) -> Vec<usize> {
|
||||
vec![1; 5]
|
||||
}
|
||||
fn eval(&mut self, ctx: Ctx<'_>) -> Option<&[Scalar]> {
|
||||
for i in 0..5 {
|
||||
let w = ctx.f64_in(i);
|
||||
@@ -473,10 +519,10 @@ mod tests {
|
||||
struct TwoField {
|
||||
out: [Scalar; 2],
|
||||
}
|
||||
impl Node for TwoField {
|
||||
fn schema(&self) -> NodeSchema {
|
||||
impl TwoField {
|
||||
fn sig() -> NodeSchema {
|
||||
NodeSchema {
|
||||
inputs: vec![InputSpec { kind: ScalarKind::F64, lookback: 1, firing: Firing::Any }],
|
||||
inputs: vec![f64_port(Firing::Any)],
|
||||
output: vec![
|
||||
FieldSpec { name: "f", kind: ScalarKind::F64 },
|
||||
FieldSpec { name: "i", kind: ScalarKind::I64 },
|
||||
@@ -484,6 +530,11 @@ mod tests {
|
||||
params: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Node for TwoField {
|
||||
fn lookbacks(&self) -> Vec<usize> {
|
||||
vec![1]
|
||||
}
|
||||
fn eval(&mut self, _ctx: Ctx<'_>) -> Option<&[Scalar]> {
|
||||
self.out[0] = Scalar::F64(0.0);
|
||||
self.out[1] = Scalar::I64(0);
|
||||
@@ -498,14 +549,19 @@ mod tests {
|
||||
out: [Scalar; 1],
|
||||
tx: mpsc::Sender<(Timestamp, Vec<Scalar>)>,
|
||||
}
|
||||
impl Node for TapForward {
|
||||
fn schema(&self) -> NodeSchema {
|
||||
impl TapForward {
|
||||
fn sig() -> NodeSchema {
|
||||
NodeSchema {
|
||||
inputs: vec![InputSpec { kind: ScalarKind::F64, lookback: 1, firing: Firing::Any }],
|
||||
inputs: vec![f64_port(Firing::Any)],
|
||||
output: vec![FieldSpec { name: "value", kind: ScalarKind::F64 }],
|
||||
params: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Node for TapForward {
|
||||
fn lookbacks(&self) -> Vec<usize> {
|
||||
vec![1]
|
||||
}
|
||||
fn eval(&mut self, ctx: Ctx<'_>) -> Option<&[Scalar]> {
|
||||
let w = ctx.f64_in(0);
|
||||
if w.is_empty() {
|
||||
@@ -522,11 +578,15 @@ mod tests {
|
||||
fn chain_source_sma_runs() {
|
||||
// node 0 = SMA(3); source -> SMA(3).in0; node 1 = Recorder taps node 0.
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let mut h = Harness::bootstrap(
|
||||
let mut h = boot(
|
||||
vec![
|
||||
Box::new(Sma::new(3)),
|
||||
Box::new(Recorder::new(&[ScalarKind::F64], Firing::Any, tx)),
|
||||
],
|
||||
vec![
|
||||
Sma::builder().schema().clone(),
|
||||
recorder_sig(&[ScalarKind::F64], Firing::Any),
|
||||
],
|
||||
vec![SourceSpec { kind: ScalarKind::F64, targets: vec![Target { node: 0, slot: 0 }] }],
|
||||
vec![Edge { from: 0, to: 1, slot: 0, from_field: 0 }],
|
||||
)
|
||||
@@ -550,13 +610,19 @@ mod tests {
|
||||
// 0 = SMA(2), 1 = SMA(4), 2 = Sub; source fans into both SMAs; SMAs join
|
||||
// into Sub; node 3 = Recorder taps Sub — the 0003 baseline on the new API.
|
||||
let build = |tx| {
|
||||
Harness::bootstrap(
|
||||
boot(
|
||||
vec![
|
||||
Box::new(Sma::new(2)),
|
||||
Box::new(Sma::new(4)),
|
||||
Box::new(Sub::new()),
|
||||
Box::new(Recorder::new(&[ScalarKind::F64], Firing::Any, tx)),
|
||||
],
|
||||
vec![
|
||||
Sma::builder().schema().clone(),
|
||||
Sma::builder().schema().clone(),
|
||||
Sub::builder().schema().clone(),
|
||||
recorder_sig(&[ScalarKind::F64], Firing::Any),
|
||||
],
|
||||
vec![SourceSpec {
|
||||
kind: ScalarKind::F64,
|
||||
targets: vec![Target { node: 0, slot: 0 }, Target { node: 1, slot: 0 }],
|
||||
@@ -598,11 +664,15 @@ mod tests {
|
||||
// AsOfSum @0; node 1 = Recorder taps it. source 0 ticks t=1..4; source 1
|
||||
// ticks t=2,4 (slower); both AsOfSum inputs Any.
|
||||
let build = |tx| {
|
||||
Harness::bootstrap(
|
||||
boot(
|
||||
vec![
|
||||
Box::new(AsOfSum { out: [Scalar::F64(0.0)] }),
|
||||
Box::new(Recorder::new(&[ScalarKind::F64], Firing::Any, tx)),
|
||||
],
|
||||
vec![
|
||||
AsOfSum::sig(),
|
||||
recorder_sig(&[ScalarKind::F64], Firing::Any),
|
||||
],
|
||||
vec![
|
||||
SourceSpec { kind: ScalarKind::F64, targets: vec![Target { node: 0, slot: 0 }] },
|
||||
SourceSpec { kind: ScalarKind::F64, targets: vec![Target { node: 0, slot: 1 }] },
|
||||
@@ -640,11 +710,15 @@ mod tests {
|
||||
fn mode_b_barrier_fires_only_on_timestamp_coincidence() {
|
||||
// identical wiring to mode A, but both BarrierSum inputs are Barrier(0).
|
||||
let build = |tx| {
|
||||
Harness::bootstrap(
|
||||
boot(
|
||||
vec![
|
||||
Box::new(BarrierSum { out: [Scalar::F64(0.0)] }),
|
||||
Box::new(Recorder::new(&[ScalarKind::F64], Firing::Any, tx)),
|
||||
],
|
||||
vec![
|
||||
BarrierSum::sig(),
|
||||
recorder_sig(&[ScalarKind::F64], Firing::Any),
|
||||
],
|
||||
vec![
|
||||
SourceSpec { kind: ScalarKind::F64, targets: vec![Target { node: 0, slot: 0 }] },
|
||||
SourceSpec { kind: ScalarKind::F64, targets: vec![Target { node: 0, slot: 1 }] },
|
||||
@@ -683,13 +757,19 @@ mod tests {
|
||||
// that cycle's timestamp, so once both SMAs warm and emit in the same
|
||||
// cycle, both barrier inputs share the timestamp and the barrier fires.
|
||||
let build = |tx| {
|
||||
Harness::bootstrap(
|
||||
boot(
|
||||
vec![
|
||||
Box::new(Sma::new(2)),
|
||||
Box::new(Sma::new(4)),
|
||||
Box::new(BarrierSum { out: [Scalar::F64(0.0)] }),
|
||||
Box::new(Recorder::new(&[ScalarKind::F64], Firing::Any, tx)),
|
||||
],
|
||||
vec![
|
||||
Sma::builder().schema().clone(),
|
||||
Sma::builder().schema().clone(),
|
||||
BarrierSum::sig(),
|
||||
recorder_sig(&[ScalarKind::F64], Firing::Any),
|
||||
],
|
||||
vec![SourceSpec {
|
||||
kind: ScalarKind::F64,
|
||||
targets: vec![Target { node: 0, slot: 0 }, Target { node: 1, slot: 0 }],
|
||||
@@ -730,11 +810,15 @@ mod tests {
|
||||
fn mixed_a_and_b_or_combine_on_one_node() {
|
||||
// MixedSum @0 (in0,in1 barrier group 0; in2 as-of); node 1 = Recorder taps it.
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let mut h = Harness::bootstrap(
|
||||
let mut h = boot(
|
||||
vec![
|
||||
Box::new(MixedSum { out: [Scalar::F64(0.0)] }),
|
||||
Box::new(Recorder::new(&[ScalarKind::F64], Firing::Any, tx)),
|
||||
],
|
||||
vec![
|
||||
MixedSum::sig(),
|
||||
recorder_sig(&[ScalarKind::F64], Firing::Any),
|
||||
],
|
||||
vec![
|
||||
SourceSpec { kind: ScalarKind::F64, targets: vec![Target { node: 0, slot: 0 }] },
|
||||
SourceSpec { kind: ScalarKind::F64, targets: vec![Target { node: 0, slot: 1 }] },
|
||||
@@ -763,8 +847,12 @@ mod tests {
|
||||
#[test]
|
||||
fn bootstrap_rejects_a_cycle() {
|
||||
// two SMA(1) nodes wired a -> b -> a
|
||||
let err = Harness::bootstrap(
|
||||
let err = boot(
|
||||
vec![Box::new(Sma::new(1)), Box::new(Sma::new(1))],
|
||||
vec![
|
||||
Sma::builder().schema().clone(),
|
||||
Sma::builder().schema().clone(),
|
||||
],
|
||||
vec![],
|
||||
vec![Edge { from: 0, to: 1, slot: 0, from_field: 0 }, Edge { from: 1, to: 0, slot: 0, from_field: 0 }],
|
||||
)
|
||||
@@ -775,8 +863,11 @@ mod tests {
|
||||
#[test]
|
||||
fn bootstrap_rejects_a_kind_mismatch() {
|
||||
// SMA(1) declares an f64 input; an i64 source mismatches
|
||||
let err = Harness::bootstrap(
|
||||
let err = boot(
|
||||
vec![Box::new(Sma::new(1))],
|
||||
vec![
|
||||
Sma::builder().schema().clone(),
|
||||
],
|
||||
vec![SourceSpec { kind: ScalarKind::I64, targets: vec![Target { node: 0, slot: 0 }] }],
|
||||
vec![],
|
||||
)
|
||||
@@ -792,8 +883,12 @@ mod tests {
|
||||
// an edge target node (9) that does not exist -> BadIndex. (The old trigger
|
||||
// — an out-of-range observe index — is gone with `observe`; BadIndex itself
|
||||
// is unchanged, only the path that reaches it.)
|
||||
let err = Harness::bootstrap(
|
||||
let err = boot(
|
||||
vec![Box::new(Sma::new(1)), Box::new(Sma::new(1))],
|
||||
vec![
|
||||
Sma::builder().schema().clone(),
|
||||
Sma::builder().schema().clone(),
|
||||
],
|
||||
vec![SourceSpec { kind: ScalarKind::F64, targets: vec![Target { node: 0, slot: 0 }] }],
|
||||
vec![Edge { from: 0, to: 9, slot: 0, from_field: 0 }],
|
||||
)
|
||||
@@ -827,7 +922,7 @@ mod tests {
|
||||
// taps all five fields via five edges. The barrier fires once all five share
|
||||
// the timestamp, so each bar is recorded once, on the fifth cycle of its ts.
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let mut h = Harness::bootstrap(
|
||||
let mut h = boot(
|
||||
vec![
|
||||
Box::new(Ohlcv { out: [Scalar::F64(0.0); 5] }),
|
||||
Box::new(Recorder::new(
|
||||
@@ -836,6 +931,10 @@ mod tests {
|
||||
tx,
|
||||
)),
|
||||
],
|
||||
vec![
|
||||
Ohlcv::sig(),
|
||||
recorder_sig(&[ScalarKind::F64, ScalarKind::F64, ScalarKind::F64, ScalarKind::F64, ScalarKind::F64], Firing::Any),
|
||||
],
|
||||
ohlcv_sources(),
|
||||
vec![
|
||||
Edge { from: 0, to: 1, slot: 0, from_field: 0 }, // open
|
||||
@@ -876,12 +975,17 @@ mod tests {
|
||||
// Proves from_field routes the right columns (not field 0) and the two
|
||||
// bound fields are co-fresh (Sub's Any inputs both fire in the bar's cycle).
|
||||
let build = |tx| {
|
||||
Harness::bootstrap(
|
||||
boot(
|
||||
vec![
|
||||
Box::new(Ohlcv { out: [Scalar::F64(0.0); 5] }),
|
||||
Box::new(Sub::new()),
|
||||
Box::new(Recorder::new(&[ScalarKind::F64], Firing::Any, tx)),
|
||||
],
|
||||
vec![
|
||||
Ohlcv::sig(),
|
||||
Sub::builder().schema().clone(),
|
||||
recorder_sig(&[ScalarKind::F64], Firing::Any),
|
||||
],
|
||||
ohlcv_sources(),
|
||||
vec![
|
||||
Edge { from: 0, to: 1, slot: 0, from_field: 1 }, // high
|
||||
@@ -917,12 +1021,17 @@ mod tests {
|
||||
// (field 0) -> close - open; the Recorder taps Sub. Proves two edges on one
|
||||
// record read two different fields (3 and 0, not the high/low pair above).
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let mut h = Harness::bootstrap(
|
||||
let mut h = boot(
|
||||
vec![
|
||||
Box::new(Ohlcv { out: [Scalar::F64(0.0); 5] }),
|
||||
Box::new(Sub::new()),
|
||||
Box::new(Recorder::new(&[ScalarKind::F64], Firing::Any, tx)),
|
||||
],
|
||||
vec![
|
||||
Ohlcv::sig(),
|
||||
Sub::builder().schema().clone(),
|
||||
recorder_sig(&[ScalarKind::F64], Firing::Any),
|
||||
],
|
||||
ohlcv_sources(),
|
||||
vec![
|
||||
Edge { from: 0, to: 1, slot: 0, from_field: 3 }, // close
|
||||
@@ -947,8 +1056,12 @@ mod tests {
|
||||
fn bootstrap_rejects_from_field_out_of_range() {
|
||||
// Sma(0) has a 1-field output (index 0 only); an edge reading field 9 is
|
||||
// out of range -> BadIndex (caught before any kind check).
|
||||
let err = Harness::bootstrap(
|
||||
let err = boot(
|
||||
vec![Box::new(Sma::new(1)), Box::new(Sma::new(1))],
|
||||
vec![
|
||||
Sma::builder().schema().clone(),
|
||||
Sma::builder().schema().clone(),
|
||||
],
|
||||
vec![SourceSpec { kind: ScalarKind::F64, targets: vec![Target { node: 0, slot: 0 }] }],
|
||||
vec![Edge { from: 0, to: 1, slot: 0, from_field: 9 }],
|
||||
)
|
||||
@@ -961,8 +1074,12 @@ mod tests {
|
||||
// TwoField(0) output: field 0 f64, field 1 i64. Binding field 1 (i64) into
|
||||
// Sma(1)'s f64 input slot is a per-field kind mismatch -> KindMismatch. (The
|
||||
// mismatch is field-specific: from_field 0 would have matched.)
|
||||
let err = Harness::bootstrap(
|
||||
let err = boot(
|
||||
vec![Box::new(TwoField { out: [Scalar::F64(0.0), Scalar::I64(0)] }), Box::new(Sma::new(1))],
|
||||
vec![
|
||||
TwoField::sig(),
|
||||
Sma::builder().schema().clone(),
|
||||
],
|
||||
vec![SourceSpec { kind: ScalarKind::F64, targets: vec![Target { node: 0, slot: 0 }] }],
|
||||
vec![Edge { from: 0, to: 1, slot: 0, from_field: 1 }],
|
||||
)
|
||||
@@ -979,13 +1096,19 @@ mod tests {
|
||||
// streams (the #2 headline). Each drained stream is individually correct.
|
||||
let (tx_fast, rx_fast) = mpsc::channel();
|
||||
let (tx_slow, rx_slow) = mpsc::channel();
|
||||
let mut h = Harness::bootstrap(
|
||||
let mut h = boot(
|
||||
vec![
|
||||
Box::new(Sma::new(2)),
|
||||
Box::new(Sma::new(4)),
|
||||
Box::new(Recorder::new(&[ScalarKind::F64], Firing::Any, tx_fast)),
|
||||
Box::new(Recorder::new(&[ScalarKind::F64], Firing::Any, tx_slow)),
|
||||
],
|
||||
vec![
|
||||
Sma::builder().schema().clone(),
|
||||
Sma::builder().schema().clone(),
|
||||
recorder_sig(&[ScalarKind::F64], Firing::Any),
|
||||
recorder_sig(&[ScalarKind::F64], Firing::Any),
|
||||
],
|
||||
vec![SourceSpec {
|
||||
kind: ScalarKind::F64,
|
||||
targets: vec![Target { node: 0, slot: 0 }, Target { node: 1, slot: 0 }],
|
||||
@@ -1023,7 +1146,7 @@ mod tests {
|
||||
// A 5-input Recorder taps all five OHLCV fields via five field-wise edges
|
||||
// (0005: N edges, no whole-record bind); its recorded row is the whole bar.
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let mut h = Harness::bootstrap(
|
||||
let mut h = boot(
|
||||
vec![
|
||||
Box::new(Ohlcv { out: [Scalar::F64(0.0); 5] }),
|
||||
Box::new(Recorder::new(
|
||||
@@ -1032,6 +1155,10 @@ mod tests {
|
||||
tx,
|
||||
)),
|
||||
],
|
||||
vec![
|
||||
Ohlcv::sig(),
|
||||
recorder_sig(&[ScalarKind::F64, ScalarKind::F64, ScalarKind::F64, ScalarKind::F64, ScalarKind::F64], Firing::Any),
|
||||
],
|
||||
ohlcv_sources(),
|
||||
vec![
|
||||
Edge { from: 0, to: 1, slot: 0, from_field: 0 },
|
||||
@@ -1071,12 +1198,15 @@ mod tests {
|
||||
// at t=1,2,3,4; only on cycle 4 are all slots warm, so it records once,
|
||||
// holding the earlier-ticked values.
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let mut h = Harness::bootstrap(
|
||||
let mut h = boot(
|
||||
vec![Box::new(Recorder::new(
|
||||
&[ScalarKind::I64, ScalarKind::F64, ScalarKind::Bool, ScalarKind::Timestamp],
|
||||
Firing::Any,
|
||||
tx,
|
||||
))],
|
||||
vec![
|
||||
recorder_sig(&[ScalarKind::I64, ScalarKind::F64, ScalarKind::Bool, ScalarKind::Timestamp], Firing::Any),
|
||||
],
|
||||
vec![
|
||||
SourceSpec { kind: ScalarKind::I64, targets: vec![Target { node: 0, slot: 0 }] },
|
||||
SourceSpec { kind: ScalarKind::F64, targets: vec![Target { node: 0, slot: 1 }] },
|
||||
@@ -1111,11 +1241,15 @@ mod tests {
|
||||
// one node is producer and sink at once (C8 "both").
|
||||
let (tx_tap, rx_tap) = mpsc::channel();
|
||||
let (tx_down, rx_down) = mpsc::channel();
|
||||
let mut h = Harness::bootstrap(
|
||||
let mut h = boot(
|
||||
vec![
|
||||
Box::new(TapForward { out: [Scalar::F64(0.0)], tx: tx_tap }),
|
||||
Box::new(Recorder::new(&[ScalarKind::F64], Firing::Any, tx_down)),
|
||||
],
|
||||
vec![
|
||||
TapForward::sig(),
|
||||
recorder_sig(&[ScalarKind::F64], Firing::Any),
|
||||
],
|
||||
vec![SourceSpec { kind: ScalarKind::F64, targets: vec![Target { node: 0, slot: 0 }] }],
|
||||
vec![Edge { from: 0, to: 1, slot: 0, from_field: 0 }],
|
||||
)
|
||||
@@ -1137,11 +1271,15 @@ mod tests {
|
||||
// Two fresh harnesses, two channels, identical input -> bit-identical
|
||||
// recorded streams (C1).
|
||||
let build = |tx| {
|
||||
Harness::bootstrap(
|
||||
boot(
|
||||
vec![
|
||||
Box::new(Sma::new(3)),
|
||||
Box::new(Recorder::new(&[ScalarKind::F64], Firing::Any, tx)),
|
||||
],
|
||||
vec![
|
||||
Sma::builder().schema().clone(),
|
||||
recorder_sig(&[ScalarKind::F64], Firing::Any),
|
||||
],
|
||||
vec![SourceSpec { kind: ScalarKind::F64, targets: vec![Target { node: 0, slot: 0 }] }],
|
||||
vec![Edge { from: 0, to: 1, slot: 0, from_field: 0 }],
|
||||
)
|
||||
@@ -1168,12 +1306,15 @@ mod tests {
|
||||
// A 2-input Barrier(0) recorder records only on cycles where both inputs
|
||||
// share the timestamp — the recorder's OWN firing policy gates recording.
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let mut h = Harness::bootstrap(
|
||||
let mut h = boot(
|
||||
vec![Box::new(Recorder::new(
|
||||
&[ScalarKind::F64, ScalarKind::F64],
|
||||
Firing::Barrier(0),
|
||||
tx,
|
||||
))],
|
||||
vec![
|
||||
recorder_sig(&[ScalarKind::F64, ScalarKind::F64], Firing::Barrier(0)),
|
||||
],
|
||||
vec![
|
||||
SourceSpec { kind: ScalarKind::F64, targets: vec![Target { node: 0, slot: 0 }] },
|
||||
SourceSpec { kind: ScalarKind::F64, targets: vec![Target { node: 0, slot: 1 }] },
|
||||
@@ -1200,12 +1341,15 @@ mod tests {
|
||||
// A 2-input Any recorder records on any-fresh once both are warm (as-of),
|
||||
// holding the stale input.
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let mut h = Harness::bootstrap(
|
||||
let mut h = boot(
|
||||
vec![Box::new(Recorder::new(
|
||||
&[ScalarKind::F64, ScalarKind::F64],
|
||||
Firing::Any,
|
||||
tx,
|
||||
))],
|
||||
vec![
|
||||
recorder_sig(&[ScalarKind::F64, ScalarKind::F64], Firing::Any),
|
||||
],
|
||||
vec![
|
||||
SourceSpec { kind: ScalarKind::F64, targets: vec![Target { node: 0, slot: 0 }] },
|
||||
SourceSpec { kind: ScalarKind::F64, targets: vec![Target { node: 0, slot: 1 }] },
|
||||
@@ -1236,11 +1380,15 @@ mod tests {
|
||||
// Recorder's f64 input slot is a per-field kind mismatch -> KindMismatch
|
||||
// (0005's check already covers recorder edges; recording adds no new hole).
|
||||
let (tx, _rx) = mpsc::channel();
|
||||
let err = Harness::bootstrap(
|
||||
let err = boot(
|
||||
vec![
|
||||
Box::new(TwoField { out: [Scalar::F64(0.0), Scalar::I64(0)] }),
|
||||
Box::new(Recorder::new(&[ScalarKind::F64], Firing::Any, tx)),
|
||||
],
|
||||
vec![
|
||||
TwoField::sig(),
|
||||
recorder_sig(&[ScalarKind::F64], Firing::Any),
|
||||
],
|
||||
vec![SourceSpec { kind: ScalarKind::F64, targets: vec![Target { node: 0, slot: 0 }] }],
|
||||
vec![Edge { from: 0, to: 1, slot: 0, from_field: 1 }],
|
||||
)
|
||||
@@ -1265,13 +1413,19 @@ mod tests {
|
||||
// correct stream — node fan-out (not source fan-out: a single SMA(3)
|
||||
// output is forwarded down three edges to three recorders).
|
||||
let build = |t1, t2, t3| {
|
||||
Harness::bootstrap(
|
||||
boot(
|
||||
vec![
|
||||
Box::new(Sma::new(3)),
|
||||
Box::new(Recorder::new(&[ScalarKind::F64], Firing::Any, t1)),
|
||||
Box::new(Recorder::new(&[ScalarKind::F64], Firing::Any, t2)),
|
||||
Box::new(Recorder::new(&[ScalarKind::F64], Firing::Any, t3)),
|
||||
],
|
||||
vec![
|
||||
Sma::builder().schema().clone(),
|
||||
recorder_sig(&[ScalarKind::F64], Firing::Any),
|
||||
recorder_sig(&[ScalarKind::F64], Firing::Any),
|
||||
recorder_sig(&[ScalarKind::F64], Firing::Any),
|
||||
],
|
||||
vec![SourceSpec {
|
||||
kind: ScalarKind::F64,
|
||||
targets: vec![Target { node: 0, slot: 0 }],
|
||||
@@ -1322,7 +1476,7 @@ mod tests {
|
||||
// other input is a second producer (SMA(4)) — yields both the raw tap
|
||||
// and the downstream-combined stream, each independently correct.
|
||||
let build = |t_raw, t_sub| {
|
||||
Harness::bootstrap(
|
||||
boot(
|
||||
vec![
|
||||
Box::new(Sma::new(2)), // 0: shared producer
|
||||
Box::new(Sma::new(4)), // 1: second producer
|
||||
@@ -1330,6 +1484,13 @@ mod tests {
|
||||
Box::new(Recorder::new(&[ScalarKind::F64], Firing::Any, t_raw)), // 3: raw tap of 0
|
||||
Box::new(Recorder::new(&[ScalarKind::F64], Firing::Any, t_sub)), // 4: tap of Sub
|
||||
],
|
||||
vec![
|
||||
Sma::builder().schema().clone(),
|
||||
Sma::builder().schema().clone(),
|
||||
Sub::builder().schema().clone(),
|
||||
recorder_sig(&[ScalarKind::F64], Firing::Any),
|
||||
recorder_sig(&[ScalarKind::F64], Firing::Any),
|
||||
],
|
||||
vec![SourceSpec {
|
||||
kind: ScalarKind::F64,
|
||||
targets: vec![Target { node: 0, slot: 0 }, Target { node: 1, slot: 0 }],
|
||||
@@ -1384,13 +1545,19 @@ mod tests {
|
||||
// tap fires on every SMA push; the BarrierSum fires only on the cycles
|
||||
// where the held SMA output and a second source coincide on a timestamp.
|
||||
let build = |t_any, t_bar| {
|
||||
Harness::bootstrap(
|
||||
boot(
|
||||
vec![
|
||||
Box::new(Sma::new(2)), // 0: shared producer (src A)
|
||||
Box::new(Recorder::new(&[ScalarKind::F64], Firing::Any, t_any)), // 1: as-of tap of 0
|
||||
Box::new(BarrierSum { out: [Scalar::F64(0.0)] }), // 2: SMA(2) + src B (barrier)
|
||||
Box::new(Recorder::new(&[ScalarKind::F64], Firing::Any, t_bar)), // 3: tap of barrier
|
||||
],
|
||||
vec![
|
||||
Sma::builder().schema().clone(),
|
||||
recorder_sig(&[ScalarKind::F64], Firing::Any),
|
||||
BarrierSum::sig(),
|
||||
recorder_sig(&[ScalarKind::F64], Firing::Any),
|
||||
],
|
||||
vec![
|
||||
SourceSpec { kind: ScalarKind::F64, targets: vec![Target { node: 0, slot: 0 }] }, // A
|
||||
SourceSpec { kind: ScalarKind::F64, targets: vec![Target { node: 2, slot: 1 }] }, // B
|
||||
@@ -1441,13 +1608,19 @@ mod tests {
|
||||
// -> SMA(2) -> recorder) propagates values correctly through depth, with
|
||||
// each stage's warm-up delaying the tail — closes "deep chains untested".
|
||||
let build = |tx| {
|
||||
Harness::bootstrap(
|
||||
boot(
|
||||
vec![
|
||||
Box::new(Sma::new(2)), // 0
|
||||
Box::new(Sma::new(2)), // 1
|
||||
Box::new(Sma::new(2)), // 2
|
||||
Box::new(Recorder::new(&[ScalarKind::F64], Firing::Any, tx)), // 3 tail
|
||||
],
|
||||
vec![
|
||||
Sma::builder().schema().clone(),
|
||||
Sma::builder().schema().clone(),
|
||||
Sma::builder().schema().clone(),
|
||||
recorder_sig(&[ScalarKind::F64], Firing::Any),
|
||||
],
|
||||
vec![SourceSpec {
|
||||
kind: ScalarKind::F64,
|
||||
targets: vec![Target { node: 0, slot: 0 }],
|
||||
@@ -1486,7 +1659,7 @@ mod tests {
|
||||
// run, yields each parallel stream correctly — closes "wide layers
|
||||
// untested" and exercises multi-sink at width.
|
||||
let build = |t2, t3, t4| {
|
||||
Harness::bootstrap(
|
||||
boot(
|
||||
vec![
|
||||
Box::new(Sma::new(2)), // 0
|
||||
Box::new(Sma::new(3)), // 1
|
||||
@@ -1495,6 +1668,14 @@ mod tests {
|
||||
Box::new(Recorder::new(&[ScalarKind::F64], Firing::Any, t3)), // 4
|
||||
Box::new(Recorder::new(&[ScalarKind::F64], Firing::Any, t4)), // 5
|
||||
],
|
||||
vec![
|
||||
Sma::builder().schema().clone(),
|
||||
Sma::builder().schema().clone(),
|
||||
Sma::builder().schema().clone(),
|
||||
recorder_sig(&[ScalarKind::F64], Firing::Any),
|
||||
recorder_sig(&[ScalarKind::F64], Firing::Any),
|
||||
recorder_sig(&[ScalarKind::F64], Firing::Any),
|
||||
],
|
||||
vec![SourceSpec {
|
||||
kind: ScalarKind::F64,
|
||||
targets: vec![
|
||||
@@ -1559,7 +1740,7 @@ mod tests {
|
||||
// deterministic — the "pure compute substrate carries arbitrary
|
||||
// M-producer x N-consumer x K-sink DAGs" gate.
|
||||
let build = |t_raw, t_sub, t_i64| {
|
||||
Harness::bootstrap(
|
||||
boot(
|
||||
vec![
|
||||
Box::new(Sma::new(2)), // 0: shared f64 producer
|
||||
Box::new(Sma::new(4)), // 1: second f64 producer
|
||||
@@ -1568,6 +1749,14 @@ mod tests {
|
||||
Box::new(Recorder::new(&[ScalarKind::F64], Firing::Any, t_sub)), // 4
|
||||
Box::new(Recorder::new(&[ScalarKind::I64], Firing::Any, t_i64)), // 5: i64 sink
|
||||
],
|
||||
vec![
|
||||
Sma::builder().schema().clone(),
|
||||
Sma::builder().schema().clone(),
|
||||
Sub::builder().schema().clone(),
|
||||
recorder_sig(&[ScalarKind::F64], Firing::Any),
|
||||
recorder_sig(&[ScalarKind::F64], Firing::Any),
|
||||
recorder_sig(&[ScalarKind::I64], Firing::Any),
|
||||
],
|
||||
vec![
|
||||
SourceSpec {
|
||||
kind: ScalarKind::F64,
|
||||
@@ -1634,7 +1823,7 @@ mod tests {
|
||||
#[test]
|
||||
fn signal_quality_loop_records_pip_equity() {
|
||||
let (tx_eq, rx_eq) = mpsc::channel();
|
||||
let mut h = Harness::bootstrap(
|
||||
let mut h = boot(
|
||||
vec![
|
||||
Box::new(Sma::new(2)), // 0 fast
|
||||
Box::new(Sma::new(4)), // 1 slow
|
||||
@@ -1643,6 +1832,14 @@ mod tests {
|
||||
Box::new(SimBroker::new(0.0001)), // 4 pip equity
|
||||
Box::new(Recorder::new(&[ScalarKind::F64], Firing::Any, tx_eq)), // 5 sink
|
||||
],
|
||||
vec![
|
||||
Sma::builder().schema().clone(),
|
||||
Sma::builder().schema().clone(),
|
||||
Sub::builder().schema().clone(),
|
||||
Exposure::builder().schema().clone(),
|
||||
SimBroker::builder(0.0001).schema().clone(),
|
||||
recorder_sig(&[ScalarKind::F64], Firing::Any),
|
||||
],
|
||||
vec![SourceSpec {
|
||||
kind: ScalarKind::F64,
|
||||
targets: vec![
|
||||
@@ -1692,7 +1889,7 @@ mod tests {
|
||||
fn signal_quality_loop_is_deterministic() {
|
||||
let build = || {
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let mut h = Harness::bootstrap(
|
||||
let mut h = boot(
|
||||
vec![
|
||||
Box::new(Sma::new(2)),
|
||||
Box::new(Sma::new(4)),
|
||||
@@ -1701,6 +1898,14 @@ mod tests {
|
||||
Box::new(SimBroker::new(0.0001)),
|
||||
Box::new(Recorder::new(&[ScalarKind::F64], Firing::Any, tx)),
|
||||
],
|
||||
vec![
|
||||
Sma::builder().schema().clone(),
|
||||
Sma::builder().schema().clone(),
|
||||
Sub::builder().schema().clone(),
|
||||
Exposure::builder().schema().clone(),
|
||||
SimBroker::builder(0.0001).schema().clone(),
|
||||
recorder_sig(&[ScalarKind::F64], Firing::Any),
|
||||
],
|
||||
vec![SourceSpec {
|
||||
kind: ScalarKind::F64,
|
||||
targets: vec![
|
||||
|
||||
@@ -36,15 +36,14 @@ mod harness;
|
||||
mod report;
|
||||
|
||||
pub use blueprint::{
|
||||
aliases_on, signature_of, Blueprint, BlueprintNode, CompileError, Composite, OutField,
|
||||
ParamAlias, Role,
|
||||
aliases_on, signature_of, BlueprintNode, CompileError, Composite, OutField, ParamAlias, Role,
|
||||
};
|
||||
pub use harness::{BootstrapError, Edge, Harness, SourceSpec, Target};
|
||||
pub use harness::{BootstrapError, Edge, FlatGraph, Harness, SourceSpec, Target};
|
||||
pub use report::{f64_field, summarize, RunManifest, RunMetrics, RunReport};
|
||||
// #29: re-export the core scalar vocabulary a Blueprint builder needs
|
||||
// (SourceSpec.kind is a ScalarKind; sources/Recorder columns are Scalar /
|
||||
// Firing / Timestamp) so a graph builder has one import surface, not two.
|
||||
pub use aura_core::{Firing, ParamSpec, Scalar, ScalarKind, Timestamp};
|
||||
pub use aura_core::{Firing, NodeSchema, ParamSpec, PortSpec, Scalar, ScalarKind, Timestamp};
|
||||
|
||||
#[cfg(test)]
|
||||
mod reexport_tests {
|
||||
|
||||
@@ -198,11 +198,21 @@ pub fn f64_field(rows: &[(Timestamp, Vec<Scalar>)], field: usize) -> Vec<(Timest
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{Edge, Harness, SourceSpec, Target};
|
||||
use aura_core::{Firing, ScalarKind};
|
||||
use crate::{Edge, FlatGraph, Harness, SourceSpec, Target};
|
||||
use aura_core::{Firing, NodeSchema, PortSpec, ScalarKind};
|
||||
use aura_std::{Exposure, Recorder, SimBroker, Sma, Sub};
|
||||
use std::sync::mpsc;
|
||||
|
||||
/// The declared signature of a `Recorder` over one f64 column (the sink shape
|
||||
/// the two-sink harness uses).
|
||||
fn f64_recorder_sig() -> NodeSchema {
|
||||
NodeSchema {
|
||||
inputs: vec![PortSpec { kind: ScalarKind::F64, firing: Firing::Any }],
|
||||
output: vec![],
|
||||
params: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
/// Build an f64 source stream from (timestamp, value) points (mirrors the
|
||||
/// harness.rs test helper; the e2e test needs its own copy — the harness
|
||||
/// test module's is private to that module).
|
||||
@@ -221,8 +231,8 @@ mod tests {
|
||||
) {
|
||||
let (tx_eq, rx_eq) = mpsc::channel();
|
||||
let (tx_ex, rx_ex) = mpsc::channel();
|
||||
let h = Harness::bootstrap(
|
||||
vec![
|
||||
let h = Harness::bootstrap(FlatGraph {
|
||||
nodes: vec![
|
||||
Box::new(Sma::new(2)), // 0
|
||||
Box::new(Sma::new(4)), // 1
|
||||
Box::new(Sub::new()), // 2
|
||||
@@ -231,7 +241,16 @@ mod tests {
|
||||
Box::new(Recorder::new(&[ScalarKind::F64], Firing::Any, tx_eq)), // 5 equity sink
|
||||
Box::new(Recorder::new(&[ScalarKind::F64], Firing::Any, tx_ex)), // 6 exposure sink
|
||||
],
|
||||
vec![SourceSpec {
|
||||
signatures: vec![
|
||||
Sma::builder().schema().clone(),
|
||||
Sma::builder().schema().clone(),
|
||||
Sub::builder().schema().clone(),
|
||||
Exposure::builder().schema().clone(),
|
||||
SimBroker::builder(0.0001).schema().clone(),
|
||||
f64_recorder_sig(),
|
||||
f64_recorder_sig(),
|
||||
],
|
||||
sources: vec![SourceSpec {
|
||||
kind: ScalarKind::F64,
|
||||
targets: vec![
|
||||
Target { node: 0, slot: 0 },
|
||||
@@ -239,7 +258,7 @@ mod tests {
|
||||
Target { node: 4, slot: 1 }, // price into the broker
|
||||
],
|
||||
}],
|
||||
vec![
|
||||
edges: vec![
|
||||
Edge { from: 0, to: 2, slot: 0, from_field: 0 },
|
||||
Edge { from: 1, to: 2, slot: 1, from_field: 0 },
|
||||
Edge { from: 2, to: 3, slot: 0, from_field: 0 },
|
||||
@@ -247,7 +266,7 @@ mod tests {
|
||||
Edge { from: 4, to: 5, slot: 0, from_field: 0 }, // equity -> sink 5
|
||||
Edge { from: 3, to: 6, slot: 0, from_field: 0 }, // exposure -> sink 6
|
||||
],
|
||||
)
|
||||
})
|
||||
.expect("valid signal-quality DAG");
|
||||
(h, rx_eq, rx_ex)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user