//! The harness — the closed root graph that runs (glossary: `harness`, C20) — //! and its deterministic run loop. A `Harness` is a bootstrapped, frozen root //! graph: a flat node array plus an index edge table, topologically ordered, //! driven cycle by cycle by a k-way merge of timestamped sources (C3/C4). It is //! the flat, monomorphized sharpening of RustAst's reference-counted, //! interior-mutable observer push graph: no reference counting, no interior //! mutability, no per-cycle allocation (C1/C7). Each node owns its input columns //! (the cycle-0002 shape), so `Ctx` borrows them read-only and additionally //! carries the cycle timestamp (`ctx.now()`, C4). A node's `eval` returns a //! borrowed record (`Option<&[Scalar]>`); each out-edge forwards one field of it //! (`Edge::from_field`) into a consumer slot, so the K fields are co-fresh (C6). //! //! Firing (C5/C6) gates re-evaluation. Two read clocks drive it, both stamped per //! input slot on every push: `fresh_at` (the `cycle_id` of the last push — //! freshness epoch, C5) and `last_ts` (the data timestamp of that push — barrier //! token, C6). A node fires when any `Firing::Any` input is fresh this cycle, or //! when a `Firing::Barrier` group has every member at the current timestamp; a //! node that does not fire pushes nothing, so consumers keep seeing the held //! value via `window[0]` (sample-and-hold falls out of the push model). The //! barrier token is the timestamp, not the `cycle_id`: under C4 four same-time //! 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}; /// 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 /// (there is no "bind whole record" mechanism). #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct Edge { pub from: usize, pub to: usize, pub slot: usize, pub from_field: usize, } /// An input slot the source value is forwarded into each cycle. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct Target { pub node: usize, pub slot: usize, } /// A declared source: the scalar kind it produces and the input slots each of /// its records is forwarded into. The multi-source generalization of cycle /// 0003's `(source_targets, source_kind)` pair — sources are k-way-merged by /// timestamp at ingestion (C3); there is no merge inside the graph. #[derive(Clone, Debug, PartialEq, Eq)] pub struct SourceSpec { pub kind: ScalarKind, pub targets: Vec, } /// A wiring fault caught once, at bootstrap — C7's "type check paid at wiring" /// generalized to the whole topology. #[derive(Debug, PartialEq, Eq)] pub enum BootstrapError { /// An edge or source-target connects mismatched scalar kinds. KindMismatch { producer: ScalarKind, consumer: ScalarKind }, /// A node or slot index in an edge or target is out of range. BadIndex, /// The wiring contains a directed cycle (only an explicit delay node may close /// a loop; that node does not exist yet). Cycle, } /// Per-input engine bookkeeping (invisible to nodes): the two read clocks of the /// firing machinery. `fresh_at` is the `cycle_id` of the last push into this slot /// (freshness epoch, C5: fresh this cycle iff `fresh_at == cycle_id`); `last_ts` /// is the data timestamp of that push (barrier token, C6: a barrier group is /// complete iff all members carry `last_ts == T`). A never-pushed slot keeps the /// cold sentinel `Timestamp(i64::MIN)`, which no real timestamp equals. struct SlotState { fresh_at: u64, last_ts: Timestamp, } struct NodeBox { node: Box, inputs: Vec, firing: Vec, slots: Vec, out_len: usize, } /// A bootstrapped, frozen root graph instance plus its deterministic run loop. pub struct Harness { nodes: Vec, topo: Vec, out_edges: Vec>, sources: Vec, } // Manual, node-opaque `Debug`: `Box` is not `Debug`, so the struct // cannot derive it. This summary form is enough for `Result::unwrap_err` (which // formats the `Ok` arm on a failed bootstrap-rejection assertion) without // printing node internals. impl core::fmt::Debug for Harness { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { f.debug_struct("Harness") .field("nodes", &self.nodes.len()) .field("topo", &self.topo) .field("out_edges", &self.out_edges) .field("sources", &self.sources) .finish() } } 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>, sources: Vec, edges: Vec, ) -> Result { 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 let mut boxes: Vec = Vec::with_capacity(n); for (nd, schema) in nodes.into_iter().zip(schemas.iter()) { let inputs: Vec = schema .inputs .iter() .map(|spec| AnyColumn::with_capacity(spec.kind, spec.lookback)) .collect(); let firing: Vec = schema.inputs.iter().map(|spec| spec.firing).collect(); let slots: Vec = schema .inputs .iter() .map(|_| SlotState { fresh_at: 0, last_ts: Timestamp(i64::MIN) }) .collect(); let out_len = schema.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 slot = s.inputs.get(t.slot).ok_or(BootstrapError::BadIndex)?; if slot.kind != src.kind { return Err(BootstrapError::KindMismatch { producer: src.kind, consumer: slot.kind, }); } } } // edges: indices in range, producer output field kind == consumer slot kind let mut out_edges: Vec> = 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 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 { return Err(BootstrapError::KindMismatch { producer: field.kind, consumer: slot.kind, }); } out_edges[e.from].push(e); } // Kahn topological sort; a leftover node means a cycle let mut indeg = vec![0usize; n]; for &e in &edges { indeg[e.to] += 1; } let mut queue: Vec = (0..n).filter(|&i| indeg[i] == 0).collect(); let mut topo: Vec = Vec::with_capacity(n); let mut head = 0; while head < queue.len() { let u = queue[head]; head += 1; topo.push(u); for e in &out_edges[u] { indeg[e.to] -= 1; if indeg[e.to] == 0 { queue.push(e.to); } } } if topo.len() != n { return Err(BootstrapError::Cycle); } Ok(Harness { nodes: boxes, topo, out_edges, sources, }) } /// Drive the sources, k-way-merged in timestamp order (ties by source index, /// C4). One stream per source, each ascending in timestamp (C3 ingestion /// precondition). Recording is a node-side concern: a recording node pushes /// its record to a destination it holds (out of graph) inside `eval`; the /// engine only routes in-graph edges and is oblivious to the side effect. /// Allocates nothing per cycle beyond the reused scratch buffer. pub fn run(&mut self, streams: Vec>) { assert_eq!( streams.len(), self.sources.len(), "run: one stream per source required (got {} streams for {} sources)", streams.len(), self.sources.len() ); // disjoint field borrows so the topo walk can read topo/out_edges/sources // while mutating nodes let Harness { nodes, topo, out_edges, sources } = self; let mut cursor: Vec = vec![0; streams.len()]; let mut cycle_id: u64 = 0; let mut scratch: Vec = Vec::new(); loop { // pick the live source head with the smallest (timestamp, source index) let mut pick: Option = None; for (s, stream) in streams.iter().enumerate() { if cursor[s] < stream.len() { match pick { None => pick = Some(s), Some(p) => { if stream[cursor[s]].0 < streams[p][cursor[p]].0 { pick = Some(s); } } } } } let s = match pick { Some(s) => s, None => break, // all streams exhausted }; let (ts, value) = streams[s][cursor[s]]; cursor[s] += 1; cycle_id += 1; // forward the source value into its target slots, stamping freshness for t in sources[s].targets.iter() { let nb = &mut nodes[t.node]; nb.inputs[t.slot].push(value).expect("source kind checked at wiring"); nb.slots[t.slot] = SlotState { fresh_at: cycle_id, last_ts: ts }; } // evaluate in topological order; gate by firing; forward Some outputs for &nidx in topo.iter() { let out_len = nodes[nidx].out_len; let fired = { let nb = &nodes[nidx]; fires(&nb.firing, &nb.slots, cycle_id, ts) }; if !fired { continue; // hold: no eval, no push } let result: Option<&[Scalar]> = { let nb = &mut nodes[nidx]; nb.node.eval(Ctx::new(&nb.inputs, ts)) }; if let Some(row) = result { debug_assert_eq!(row.len(), out_len, "node returned a row of the wrong width"); scratch.clear(); scratch.extend_from_slice(row); for e in out_edges[nidx].iter() { let nb = &mut nodes[e.to]; nb.inputs[e.slot] .push(scratch[e.from_field]) .expect("edge kind checked at wiring"); nb.slots[e.slot] = SlotState { fresh_at: cycle_id, last_ts: ts }; } } } } } } /// The firing predicate (C5/C6): does this node re-evaluate this cycle? A node /// fires when *any* of its input groups fires (OR). A `Firing::Any` input fires /// the node when it is fresh this cycle (`fresh_at == cycle_id`). A barrier group /// fires when >=1 member is fresh this cycle AND every member carries /// `last_ts == ts` — the ">=1 fresh" clause is the once-per-timestamp guard (a /// group completed in a prior cycle has no fresh member now, so it does not /// re-fire). fn fires(firing: &[Firing], slots: &[SlotState], cycle_id: u64, ts: Timestamp) -> bool { // mode A: any fire-on-any-fresh input that is fresh this cycle for (i, f) in firing.iter().enumerate() { if matches!(f, Firing::Any) && slots[i].fresh_at == cycle_id { return true; } } // mode B: each distinct barrier group fires when complete this cycle if let Some(max_group) = firing.iter().filter_map(group_id).max() { for g in 0..=max_group { let mut has_member = false; let mut any_fresh = false; let mut all_at_ts = true; for (i, f) in firing.iter().enumerate() { if group_id(f) == Some(g) { has_member = true; if slots[i].fresh_at == cycle_id { any_fresh = true; } if slots[i].last_ts != ts { all_at_ts = false; } } } if has_member && any_fresh && all_at_ts { return true; } } } false } /// The barrier group id of a firing policy, or `None` for mode A. fn group_id(f: &Firing) -> Option { match f { Firing::Any => None, Firing::Barrier(g) => Some(*g), } } #[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}; use aura_std::{Exposure, Sma, SimBroker, Sub}; use std::sync::mpsc; /// Build an f64 source stream from (timestamp, value) points. fn f64_stream(points: &[(i64, f64)]) -> Vec<(Timestamp, Scalar)> { points.iter().map(|&(t, v)| (Timestamp(t), Scalar::F64(v))).collect() } // --- firing-policy fixtures (test-local; not library nodes — C9: examples // for the engine's own tests, no speculative aura-std surface) --- /// Mode A as-of join: a 2-input f64 sum that fires whenever either input is /// fresh, holding the other. Warm-up returns None until both have a value. struct AsOfSum { out: [Scalar; 1], } impl Node for AsOfSum { fn schema(&self) -> NodeSchema { NodeSchema { inputs: vec![ InputSpec { kind: ScalarKind::F64, lookback: 1, firing: Firing::Any }, InputSpec { kind: ScalarKind::F64, lookback: 1, firing: Firing::Any }, ], output: vec![FieldSpec { name: "value", kind: ScalarKind::F64 }], } } 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) } } /// Mode B barrier join: a 2-input f64 sum that fires only when both inputs /// share the current cycle timestamp (both warm by construction when it fires). struct BarrierSum { out: [Scalar; 1], } impl Node for BarrierSum { fn schema(&self) -> NodeSchema { NodeSchema { inputs: vec![ InputSpec { kind: ScalarKind::F64, lookback: 1, firing: Firing::Barrier(0) }, InputSpec { kind: ScalarKind::F64, lookback: 1, firing: Firing::Barrier(0) }, ], output: vec![FieldSpec { name: "value", kind: ScalarKind::F64 }], } } 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) } } /// Mixed A+B: barrier pair (inputs 0,1 in group 0) plus an as-of input /// (input 2). Fires when the pair completes (holding input 2) OR when input 2 /// ticks (holding the pair) — the OR-combine. struct MixedSum { out: [Scalar; 1], } impl Node for MixedSum { fn schema(&self) -> 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 }, ], output: vec![FieldSpec { name: "value", kind: ScalarKind::F64 }], } } fn eval(&mut self, ctx: Ctx<'_>) -> Option<&[Scalar]> { let a = ctx.f64_in(0); let b = ctx.f64_in(1); let c = ctx.f64_in(2); if a.is_empty() || b.is_empty() || c.is_empty() { return None; } self.out[0] = Scalar::F64(a[0] + b[0] + c[0]); Some(&self.out) } } /// A neutral multi-field producer: five f64 inputs bundled into one 5-field /// record. No trading-domain logic — it proves the K > 1 output mechanism in /// isolation. The five inputs are a Barrier(0) group, so the node emits one /// complete bar only when all five share the cycle's timestamp. struct Ohlcv { out: [Scalar; 5], } impl Node for Ohlcv { fn schema(&self) -> 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) }, ], output: vec![ FieldSpec { name: "open", kind: ScalarKind::F64 }, FieldSpec { name: "high", kind: ScalarKind::F64 }, FieldSpec { name: "low", kind: ScalarKind::F64 }, FieldSpec { name: "close", kind: ScalarKind::F64 }, FieldSpec { name: "volume", kind: ScalarKind::F64 }, ], } } fn eval(&mut self, ctx: Ctx<'_>) -> Option<&[Scalar]> { for i in 0..5 { let w = ctx.f64_in(i); if w.is_empty() { return None; // not yet warmed } self.out[i] = Scalar::F64(w[0]); } Some(&self.out) // one 5-field record, all fields co-fresh } } /// A producer whose output record mixes kinds: field 0 is f64, field 1 is i64. /// Used only to prove the bootstrap kind check is per-field (field 0 would bind /// into an f64 slot; field 1 would not). Its `eval` never runs in these tests — /// bootstrap rejects the wiring first. struct TwoField { out: [Scalar; 2], } impl Node for TwoField { fn schema(&self) -> NodeSchema { NodeSchema { inputs: vec![InputSpec { kind: ScalarKind::F64, lookback: 1, firing: Firing::Any }], output: vec![ FieldSpec { name: "f", kind: ScalarKind::F64 }, FieldSpec { name: "i", kind: ScalarKind::I64 }, ], } } fn eval(&mut self, _ctx: Ctx<'_>) -> Option<&[Scalar]> { self.out[0] = Scalar::F64(0.0); self.out[1] = Scalar::I64(0); Some(&self.out) } } /// A recording node (test-local fixture; stands in for a downstream author's /// chart/registry sink). It declares typed input slots and holds an /// `mpsc::Sender`; on every fired cycle it reads the newest of each input plus /// `ctx.now()`, sends the timestamped record out of the graph, and returns /// `None` (pure consumer — C8). Read-back is via the channel, never `Rc`/ /// `RefCell`, so `aura-engine/src` stays free of the interior-mutability the /// purity invariant (C7) forbids. struct Recorder { kinds: Vec, firing: Firing, tx: mpsc::Sender<(Timestamp, Vec)>, } impl Recorder { fn new( kinds: &[ScalarKind], firing: Firing, tx: mpsc::Sender<(Timestamp, Vec)>, ) -> Self { Self { kinds: kinds.to_vec(), firing, tx } } } impl Node for Recorder { fn schema(&self) -> NodeSchema { NodeSchema { inputs: self .kinds .iter() .map(|&kind| InputSpec { kind, lookback: 1, firing: self.firing }) .collect(), output: vec![], // pure sink: no output port } } fn eval(&mut self, ctx: Ctx<'_>) -> Option<&[Scalar]> { let mut row = Vec::with_capacity(self.kinds.len()); for (i, &kind) in self.kinds.iter().enumerate() { let v = match kind { ScalarKind::I64 => { let w = ctx.i64_in(i); if w.is_empty() { return None; // not yet warmed } Scalar::I64(w[0]) } ScalarKind::F64 => { let w = ctx.f64_in(i); if w.is_empty() { return None; } Scalar::F64(w[0]) } ScalarKind::Bool => { let w = ctx.bool_in(i); if w.is_empty() { return None; } Scalar::Bool(w[0]) } ScalarKind::Timestamp => { let w = ctx.ts_in(i); if w.is_empty() { return None; } Scalar::Ts(w[0]) } }; row.push(v); } let _ = self.tx.send((ctx.now(), row)); // out-of-graph side effect None // records, forwards nothing } } /// A node that records AND forwards: it sends `(now, value)` out of the graph /// (sink side effect) and returns its value as a one-field output the engine /// forwards downstream (producer). Proves the C8 "both" role. struct TapForward { out: [Scalar; 1], tx: mpsc::Sender<(Timestamp, Vec)>, } impl Node for TapForward { fn schema(&self) -> NodeSchema { NodeSchema { inputs: vec![InputSpec { kind: ScalarKind::F64, lookback: 1, firing: Firing::Any }], output: vec![FieldSpec { name: "value", kind: ScalarKind::F64 }], } } fn eval(&mut self, ctx: Ctx<'_>) -> Option<&[Scalar]> { let w = ctx.f64_in(0); if w.is_empty() { return None; } let v = w[0]; let _ = self.tx.send((ctx.now(), vec![Scalar::F64(v)])); // sink side effect self.out[0] = Scalar::F64(v); Some(&self.out) // producer output: engine forwards it } } #[test] 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( vec![ Box::new(Sma::new(3)), Box::new(Recorder::new(&[ScalarKind::F64], Firing::Any, tx)), ], vec![SourceSpec { kind: ScalarKind::F64, targets: vec![Target { node: 0, slot: 0 }] }], vec![Edge { from: 0, to: 1, slot: 0, from_field: 0 }], ) .expect("valid"); h.run(vec![f64_stream(&[(1, 1.0), (2, 2.0), (3, 3.0), (4, 4.0), (5, 5.0)])]); let got: Vec<(Timestamp, Vec)> = rx.try_iter().collect(); // SMA(3) warms at cycle 3; the recorder captures only fired cycles, each // tagged with the cycle's timestamp (sparse — no None hold-rows). assert_eq!( got, vec![ (Timestamp(3), vec![Scalar::F64(2.0)]), (Timestamp(4), vec![Scalar::F64(3.0)]), (Timestamp(5), vec![Scalar::F64(4.0)]), ] ); } #[test] fn fan_out_join_dag_runs_deterministically() { // 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( 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![SourceSpec { kind: ScalarKind::F64, targets: vec![Target { node: 0, slot: 0 }, Target { node: 1, slot: 0 }], }], 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 }, ], ) .expect("valid DAG") }; let prices = f64_stream(&[(1, 10.0), (2, 12.0), (3, 14.0), (4, 16.0), (5, 18.0), (6, 20.0)]); let (tx, rx) = mpsc::channel(); let mut h = build(tx); h.run(vec![prices.clone()]); let out: Vec<(Timestamp, Vec)> = rx.try_iter().collect(); // Sub fires once SMA(4) is warm (cycle 4): 15-13, 17-15, 19-17 -> 2. assert_eq!( out, vec![ (Timestamp(4), vec![Scalar::F64(2.0)]), (Timestamp(5), vec![Scalar::F64(2.0)]), (Timestamp(6), vec![Scalar::F64(2.0)]), ] ); // determinism (C1): a second identical run drains a bit-identical stream. let (tx2, rx2) = mpsc::channel(); let mut h2 = build(tx2); h2.run(vec![prices]); let out2: Vec<(Timestamp, Vec)> = rx2.try_iter().collect(); assert_eq!(out2, out); } #[test] fn mode_a_as_of_fires_on_any_fresh_and_holds() { // 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( vec![ Box::new(AsOfSum { out: [Scalar::F64(0.0)] }), Box::new(Recorder::new(&[ScalarKind::F64], Firing::Any, tx)), ], vec![ SourceSpec { kind: ScalarKind::F64, targets: vec![Target { node: 0, slot: 0 }] }, SourceSpec { kind: ScalarKind::F64, targets: vec![Target { node: 0, slot: 1 }] }, ], vec![Edge { from: 0, to: 1, slot: 0, from_field: 0 }], ) .expect("valid") }; let s0 = f64_stream(&[(1, 10.0), (2, 20.0), (3, 30.0), (4, 40.0)]); let s1 = f64_stream(&[(2, 100.0), (4, 200.0)]); let (tx, rx) = mpsc::channel(); let mut h = build(tx); h.run(vec![s0.clone(), s1.clone()]); let out: Vec<(Timestamp, Vec)> = rx.try_iter().collect(); // holds s1=100 across t=3 and the t=4 s0-cycle; emits on every tick once warm. assert_eq!( out, vec![ (Timestamp(2), vec![Scalar::F64(120.0)]), (Timestamp(3), vec![Scalar::F64(130.0)]), (Timestamp(4), vec![Scalar::F64(140.0)]), (Timestamp(4), vec![Scalar::F64(240.0)]), ] ); let (tx2, rx2) = mpsc::channel(); let mut h2 = build(tx2); h2.run(vec![s0, s1]); let out2: Vec<(Timestamp, Vec)> = rx2.try_iter().collect(); assert_eq!(out2, out); // deterministic } #[test] 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( vec![ Box::new(BarrierSum { out: [Scalar::F64(0.0)] }), Box::new(Recorder::new(&[ScalarKind::F64], Firing::Any, tx)), ], vec![ SourceSpec { kind: ScalarKind::F64, targets: vec![Target { node: 0, slot: 0 }] }, SourceSpec { kind: ScalarKind::F64, targets: vec![Target { node: 0, slot: 1 }] }, ], vec![Edge { from: 0, to: 1, slot: 0, from_field: 0 }], ) .expect("valid") }; let s0 = f64_stream(&[(1, 10.0), (2, 20.0), (3, 30.0), (4, 40.0)]); let s1 = f64_stream(&[(2, 100.0), (4, 200.0)]); let (tx, rx) = mpsc::channel(); let mut h = build(tx); h.run(vec![s0.clone(), s1.clone()]); let out: Vec<(Timestamp, Vec)> = rx.try_iter().collect(); // records ONLY at t=2 and t=4 where both inputs share the timestamp. assert_eq!( out, vec![ (Timestamp(2), vec![Scalar::F64(120.0)]), (Timestamp(4), vec![Scalar::F64(240.0)]), ] ); let (tx2, rx2) = mpsc::channel(); let mut h2 = build(tx2); h2.run(vec![s0, s1]); let out2: Vec<(Timestamp, Vec)> = rx2.try_iter().collect(); assert_eq!(out2, out); // deterministic } #[test] fn within_source_diamond_rejoin_barrier_fires() { // One source fans out through SMA(2), SMA(4) that rejoin at a Barrier(0) // node; node 3 = Recorder taps the barrier. Every push in a cycle carries // 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( 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![SourceSpec { kind: ScalarKind::F64, targets: vec![Target { node: 0, slot: 0 }, Target { node: 1, slot: 0 }], }], 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 }, ], ) .expect("valid DAG") }; let prices = f64_stream(&[(1, 10.0), (2, 12.0), (3, 14.0), (4, 16.0), (5, 18.0), (6, 20.0)]); let (tx, rx) = mpsc::channel(); let mut h = build(tx); h.run(vec![prices.clone()]); let out: Vec<(Timestamp, Vec)> = rx.try_iter().collect(); // SMA(4) warms at cycle 4; from then both paths emit each cycle at the same // timestamp, so the barrier fires: SMA(2)+SMA(4) = 15+13, 17+15, 19+17. assert_eq!( out, vec![ (Timestamp(4), vec![Scalar::F64(28.0)]), (Timestamp(5), vec![Scalar::F64(32.0)]), (Timestamp(6), vec![Scalar::F64(36.0)]), ] ); let (tx2, rx2) = mpsc::channel(); let mut h2 = build(tx2); h2.run(vec![prices]); let out2: Vec<(Timestamp, Vec)> = rx2.try_iter().collect(); assert_eq!(out2, out); } #[test] 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( vec![ Box::new(MixedSum { out: [Scalar::F64(0.0)] }), Box::new(Recorder::new(&[ScalarKind::F64], Firing::Any, tx)), ], vec![ SourceSpec { kind: ScalarKind::F64, targets: vec![Target { node: 0, slot: 0 }] }, SourceSpec { kind: ScalarKind::F64, targets: vec![Target { node: 0, slot: 1 }] }, SourceSpec { kind: ScalarKind::F64, targets: vec![Target { node: 0, slot: 2 }] }, ], vec![Edge { from: 0, to: 1, slot: 0, from_field: 0 }], ) .expect("valid"); let s0 = f64_stream(&[(2, 20.0), (5, 50.0)]); // in0 (barrier) let s1 = f64_stream(&[(2, 200.0)]); // in1 (barrier) let s2 = f64_stream(&[(1, 1.0), (3, 3.0)]); // in2 (as-of) h.run(vec![s0, s1, s2]); let out: Vec<(Timestamp, Vec)> = rx.try_iter().collect(); // c3: barrier pair completes at t=2, holds c=1 -> 221. c4: as-of input // ticks at t=3, holds the pair -> 223. c1 filters; c2,c5 hold (no record). assert_eq!( out, vec![ (Timestamp(2), vec![Scalar::F64(221.0)]), (Timestamp(3), vec![Scalar::F64(223.0)]), ] ); } #[test] fn bootstrap_rejects_a_cycle() { // two SMA(1) nodes wired a -> b -> a let err = Harness::bootstrap( vec![Box::new(Sma::new(1)), Box::new(Sma::new(1))], vec![], vec![Edge { from: 0, to: 1, slot: 0, from_field: 0 }, Edge { from: 1, to: 0, slot: 0, from_field: 0 }], ) .unwrap_err(); assert_eq!(err, BootstrapError::Cycle); } #[test] fn bootstrap_rejects_a_kind_mismatch() { // SMA(1) declares an f64 input; an i64 source mismatches let err = Harness::bootstrap( vec![Box::new(Sma::new(1))], vec![SourceSpec { kind: ScalarKind::I64, targets: vec![Target { node: 0, slot: 0 }] }], vec![], ) .unwrap_err(); assert_eq!( err, BootstrapError::KindMismatch { producer: ScalarKind::I64, consumer: ScalarKind::F64 } ); } #[test] fn bootstrap_rejects_a_bad_index() { // 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( vec![Box::new(Sma::new(1)), Box::new(Sma::new(1))], vec![SourceSpec { kind: ScalarKind::F64, targets: vec![Target { node: 0, slot: 0 }] }], vec![Edge { from: 0, to: 9, slot: 0, from_field: 0 }], ) .unwrap_err(); assert_eq!(err, BootstrapError::BadIndex); } /// Build five timestamp-aligned f64 sources feeding Ohlcv's five barrier slots. fn ohlcv_streams() -> Vec> { vec![ f64_stream(&[(1, 10.0), (2, 20.0)]), // open f64_stream(&[(1, 15.0), (2, 25.0)]), // high f64_stream(&[(1, 8.0), (2, 19.0)]), // low f64_stream(&[(1, 12.0), (2, 22.0)]), // close f64_stream(&[(1, 100.0), (2, 200.0)]), // volume ] } fn ohlcv_sources() -> Vec { (0..5) .map(|slot| SourceSpec { kind: ScalarKind::F64, targets: vec![Target { node: 0, slot }], }) .collect() } #[test] fn ohlcv_bundles_five_field_record() { // node 0 = Ohlcv; five sources feed O/H/L/C/V; node 1 = a 5-input Recorder // 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( vec![ Box::new(Ohlcv { out: [Scalar::F64(0.0); 5] }), Box::new(Recorder::new( &[ScalarKind::F64, ScalarKind::F64, ScalarKind::F64, ScalarKind::F64, ScalarKind::F64], Firing::Any, tx, )), ], ohlcv_sources(), vec![ Edge { from: 0, to: 1, slot: 0, from_field: 0 }, // open Edge { from: 0, to: 1, slot: 1, from_field: 1 }, // high Edge { from: 0, to: 1, slot: 2, from_field: 2 }, // low Edge { from: 0, to: 1, slot: 3, from_field: 3 }, // close Edge { from: 0, to: 1, slot: 4, from_field: 4 }, // volume ], ) .expect("valid"); h.run(ohlcv_streams()); let out: Vec<(Timestamp, Vec)> = rx.try_iter().collect(); assert_eq!( out, vec![ (Timestamp(1), vec![ Scalar::F64(10.0), Scalar::F64(15.0), Scalar::F64(8.0), Scalar::F64(12.0), Scalar::F64(100.0), ]), (Timestamp(2), vec![ Scalar::F64(20.0), Scalar::F64(25.0), Scalar::F64(19.0), Scalar::F64(22.0), Scalar::F64(200.0), ]), ] ); } #[test] fn edge_binds_single_field_high_minus_low() { // [Ohlcv (0), Sub (1), Recorder (2)]; Sub binds high (field 1) and low // (field 2) of the Ohlcv record -> high - low; the Recorder taps Sub. // 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( vec![ Box::new(Ohlcv { out: [Scalar::F64(0.0); 5] }), Box::new(Sub::new()), Box::new(Recorder::new(&[ScalarKind::F64], Firing::Any, tx)), ], ohlcv_sources(), vec![ Edge { from: 0, to: 1, slot: 0, from_field: 1 }, // high Edge { from: 0, to: 1, slot: 1, from_field: 2 }, // low Edge { from: 1, to: 2, slot: 0, from_field: 0 }, // Sub -> Recorder ], ) .expect("valid DAG") }; let (tx, rx) = mpsc::channel(); let mut h = build(tx); h.run(ohlcv_streams()); let out: Vec<(Timestamp, Vec)> = rx.try_iter().collect(); // bar1: 15 - 8 = 7; bar2: 25 - 19 = 6 (each on the bar's fifth cycle). assert_eq!( out, vec![ (Timestamp(1), vec![Scalar::F64(7.0)]), (Timestamp(2), vec![Scalar::F64(6.0)]), ] ); let (tx2, rx2) = mpsc::channel(); let mut h2 = build(tx2); h2.run(ohlcv_streams()); let out2: Vec<(Timestamp, Vec)> = rx2.try_iter().collect(); assert_eq!(out2, out); } #[test] fn distinct_edges_read_distinct_fields() { // Same Ohlcv, a different consumer: Sub binds close (field 3) and open // (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( vec![ Box::new(Ohlcv { out: [Scalar::F64(0.0); 5] }), Box::new(Sub::new()), Box::new(Recorder::new(&[ScalarKind::F64], Firing::Any, tx)), ], ohlcv_sources(), vec![ Edge { from: 0, to: 1, slot: 0, from_field: 3 }, // close Edge { from: 0, to: 1, slot: 1, from_field: 0 }, // open Edge { from: 1, to: 2, slot: 0, from_field: 0 }, // Sub -> Recorder ], ) .expect("valid DAG"); h.run(ohlcv_streams()); let out: Vec<(Timestamp, Vec)> = rx.try_iter().collect(); // bar1: 12 - 10 = 2; bar2: 22 - 20 = 2. assert_eq!( out, vec![ (Timestamp(1), vec![Scalar::F64(2.0)]), (Timestamp(2), vec![Scalar::F64(2.0)]), ] ); } #[test] 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( vec![Box::new(Sma::new(1)), Box::new(Sma::new(1))], vec![SourceSpec { kind: ScalarKind::F64, targets: vec![Target { node: 0, slot: 0 }] }], vec![Edge { from: 0, to: 1, slot: 0, from_field: 9 }], ) .unwrap_err(); assert_eq!(err, BootstrapError::BadIndex); } #[test] fn bootstrap_rejects_per_field_kind_mismatch() { // 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( vec![Box::new(TwoField { out: [Scalar::F64(0.0), Scalar::I64(0)] }), Box::new(Sma::new(1))], vec![SourceSpec { kind: ScalarKind::F64, targets: vec![Target { node: 0, slot: 0 }] }], vec![Edge { from: 0, to: 1, slot: 0, from_field: 1 }], ) .unwrap_err(); assert_eq!( err, BootstrapError::KindMismatch { producer: ScalarKind::I64, consumer: ScalarKind::F64 } ); } #[test] fn multi_sink_records_distinct_interior_streams() { // Two recorders tap SMA(2) and SMA(4) in ONE run -> one run records many // 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( 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![SourceSpec { kind: ScalarKind::F64, targets: vec![Target { node: 0, slot: 0 }, Target { node: 1, slot: 0 }], }], vec![ Edge { from: 0, to: 2, slot: 0, from_field: 0 }, // SMA(2) -> recorder fast Edge { from: 1, to: 3, slot: 0, from_field: 0 }, // SMA(4) -> recorder slow ], ) .expect("valid DAG"); h.run(vec![f64_stream(&[(1, 10.0), (2, 12.0), (3, 14.0), (4, 16.0), (5, 18.0)])]); let fast: Vec<(Timestamp, Vec)> = rx_fast.try_iter().collect(); let slow: Vec<(Timestamp, Vec)> = rx_slow.try_iter().collect(); // SMA(2) warms at cycle 2, SMA(4) at cycle 4 — two different-rate streams. assert_eq!( fast, vec![ (Timestamp(2), vec![Scalar::F64(11.0)]), (Timestamp(3), vec![Scalar::F64(13.0)]), (Timestamp(4), vec![Scalar::F64(15.0)]), (Timestamp(5), vec![Scalar::F64(17.0)]), ] ); assert_eq!( slow, vec![ (Timestamp(4), vec![Scalar::F64(13.0)]), (Timestamp(5), vec![Scalar::F64(15.0)]), ] ); } #[test] fn recorder_taps_all_fields_of_a_record() { // 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( vec![ Box::new(Ohlcv { out: [Scalar::F64(0.0); 5] }), Box::new(Recorder::new( &[ScalarKind::F64, ScalarKind::F64, ScalarKind::F64, ScalarKind::F64, ScalarKind::F64], Firing::Any, tx, )), ], ohlcv_sources(), vec![ Edge { from: 0, to: 1, slot: 0, from_field: 0 }, Edge { from: 0, to: 1, slot: 1, from_field: 1 }, Edge { from: 0, to: 1, slot: 2, from_field: 2 }, Edge { from: 0, to: 1, slot: 3, from_field: 3 }, Edge { from: 0, to: 1, slot: 4, from_field: 4 }, ], ) .expect("valid"); h.run(vec![ f64_stream(&[(1, 10.0)]), f64_stream(&[(1, 15.0)]), f64_stream(&[(1, 8.0)]), f64_stream(&[(1, 12.0)]), f64_stream(&[(1, 100.0)]), ]); let out: Vec<(Timestamp, Vec)> = rx.try_iter().collect(); assert_eq!(out.len(), 1); assert_eq!(out[0].1.len(), 5); // all five fields recorded as one row assert_eq!( out, vec![(Timestamp(1), vec![ Scalar::F64(10.0), Scalar::F64(15.0), Scalar::F64(8.0), Scalar::F64(12.0), Scalar::F64(100.0), ])] ); } #[test] fn recorder_records_mixed_scalar_kinds() { // A recorder with i64 + f64 + bool + timestamp inputs records a four-field // mixed-kind row -> recording is not f64-only. Four sources tick once each // 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( vec![Box::new(Recorder::new( &[ScalarKind::I64, ScalarKind::F64, ScalarKind::Bool, ScalarKind::Timestamp], Firing::Any, tx, ))], vec![ SourceSpec { kind: ScalarKind::I64, targets: vec![Target { node: 0, slot: 0 }] }, SourceSpec { kind: ScalarKind::F64, targets: vec![Target { node: 0, slot: 1 }] }, SourceSpec { kind: ScalarKind::Bool, targets: vec![Target { node: 0, slot: 2 }] }, SourceSpec { kind: ScalarKind::Timestamp, targets: vec![Target { node: 0, slot: 3 }] }, ], vec![], ) .expect("valid"); h.run(vec![ vec![(Timestamp(1), Scalar::I64(7))], vec![(Timestamp(2), Scalar::F64(1.5))], vec![(Timestamp(3), Scalar::Bool(true))], vec![(Timestamp(4), Scalar::Ts(Timestamp(99)))], ]); let out: Vec<(Timestamp, Vec)> = rx.try_iter().collect(); assert_eq!( out, vec![(Timestamp(4), vec![ Scalar::I64(7), Scalar::F64(1.5), Scalar::Bool(true), Scalar::Ts(Timestamp(99)), ])] ); } #[test] fn node_is_producer_and_sink_at_once() { // TapForward records its input AND forwards it downstream; a second // Recorder taps the forwarded output. Both channels see the same stream -> // 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( vec![ Box::new(TapForward { out: [Scalar::F64(0.0)], tx: tx_tap }), Box::new(Recorder::new(&[ScalarKind::F64], Firing::Any, tx_down)), ], vec![SourceSpec { kind: ScalarKind::F64, targets: vec![Target { node: 0, slot: 0 }] }], vec![Edge { from: 0, to: 1, slot: 0, from_field: 0 }], ) .expect("valid"); h.run(vec![f64_stream(&[(1, 10.0), (2, 20.0), (3, 30.0)])]); let tapped: Vec<(Timestamp, Vec)> = rx_tap.try_iter().collect(); let downstream: Vec<(Timestamp, Vec)> = rx_down.try_iter().collect(); let expected = vec![ (Timestamp(1), vec![Scalar::F64(10.0)]), (Timestamp(2), vec![Scalar::F64(20.0)]), (Timestamp(3), vec![Scalar::F64(30.0)]), ]; assert_eq!(tapped, expected); // it recorded (sink side effect) assert_eq!(downstream, expected); // and forwarded (producer output) } #[test] fn recording_is_deterministic() { // Two fresh harnesses, two channels, identical input -> bit-identical // recorded streams (C1). let build = |tx| { Harness::bootstrap( vec![ Box::new(Sma::new(3)), Box::new(Recorder::new(&[ScalarKind::F64], Firing::Any, tx)), ], vec![SourceSpec { kind: ScalarKind::F64, targets: vec![Target { node: 0, slot: 0 }] }], vec![Edge { from: 0, to: 1, slot: 0, from_field: 0 }], ) .expect("valid") }; let prices = f64_stream(&[(1, 1.0), (2, 2.0), (3, 3.0), (4, 4.0), (5, 5.0)]); let (tx_a, rx_a) = mpsc::channel(); let mut a = build(tx_a); a.run(vec![prices.clone()]); let run_a: Vec<(Timestamp, Vec)> = rx_a.try_iter().collect(); let (tx_b, rx_b) = mpsc::channel(); let mut b = build(tx_b); b.run(vec![prices]); let run_b: Vec<(Timestamp, Vec)> = rx_b.try_iter().collect(); assert_eq!(run_a, run_b); assert!(!run_a.is_empty()); // and it actually recorded something } #[test] fn recorder_barrier_firing_records_only_on_coincidence() { // 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( vec![Box::new(Recorder::new( &[ScalarKind::F64, ScalarKind::F64], Firing::Barrier(0), tx, ))], vec![ SourceSpec { kind: ScalarKind::F64, targets: vec![Target { node: 0, slot: 0 }] }, SourceSpec { kind: ScalarKind::F64, targets: vec![Target { node: 0, slot: 1 }] }, ], vec![], ) .expect("valid"); let s0 = f64_stream(&[(1, 10.0), (2, 20.0), (3, 30.0), (4, 40.0)]); let s1 = f64_stream(&[(2, 100.0), (4, 200.0)]); h.run(vec![s0, s1]); let out: Vec<(Timestamp, Vec)> = rx.try_iter().collect(); // records ONLY at t=2 and t=4 (both inputs coincide); holds otherwise. assert_eq!( out, vec![ (Timestamp(2), vec![Scalar::F64(20.0), Scalar::F64(100.0)]), (Timestamp(4), vec![Scalar::F64(40.0), Scalar::F64(200.0)]), ] ); } #[test] fn recorder_any_firing_records_on_each_fresh() { // 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( vec![Box::new(Recorder::new( &[ScalarKind::F64, ScalarKind::F64], Firing::Any, tx, ))], vec![ SourceSpec { kind: ScalarKind::F64, targets: vec![Target { node: 0, slot: 0 }] }, SourceSpec { kind: ScalarKind::F64, targets: vec![Target { node: 0, slot: 1 }] }, ], vec![], ) .expect("valid"); let s0 = f64_stream(&[(1, 10.0), (2, 20.0), (3, 30.0), (4, 40.0)]); let s1 = f64_stream(&[(2, 100.0), (4, 200.0)]); h.run(vec![s0, s1]); let out: Vec<(Timestamp, Vec)> = rx.try_iter().collect(); // from t=2 on, records every cycle holding the stale input; two cycles fall // on t=4 (the s0 tick then the s1 tick). assert_eq!( out, vec![ (Timestamp(2), vec![Scalar::F64(20.0), Scalar::F64(100.0)]), (Timestamp(3), vec![Scalar::F64(30.0), Scalar::F64(100.0)]), (Timestamp(4), vec![Scalar::F64(40.0), Scalar::F64(100.0)]), (Timestamp(4), vec![Scalar::F64(40.0), Scalar::F64(200.0)]), ] ); } #[test] fn bootstrap_rejects_kind_mismatched_recorder_edge() { // TwoField output: field 0 f64, field 1 i64. Binding field 1 (i64) into a // 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( vec![ Box::new(TwoField { out: [Scalar::F64(0.0), Scalar::I64(0)] }), Box::new(Recorder::new(&[ScalarKind::F64], Firing::Any, tx)), ], vec![SourceSpec { kind: ScalarKind::F64, targets: vec![Target { node: 0, slot: 0 }] }], vec![Edge { from: 0, to: 1, slot: 0, from_field: 1 }], ) .unwrap_err(); assert_eq!( err, BootstrapError::KindMismatch { producer: ScalarKind::I64, consumer: ScalarKind::F64 } ); } // --- #3 stress matrix: M-producer x N-consumer x K-sink DAGs --- // The closed `Harness::bootstrap` + `run` substrate must carry arbitrary // node-level fan-out / fan-in / depth / width deterministically (C1) and // compute every recorded stream correctly. Earlier cycles proved *source* // fan-out, single chains and firing in isolation; these close the node // fan-out / deep-chain / wide-layer holes #3 names. No trading domain. #[test] fn node_fan_out_identical_taps_record_identical_streams() { // PROPERTY: one PRODUCING node read by several consumers via distinct // edges from the same `from` node feeds every consumer the identical, // 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( 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![SourceSpec { kind: ScalarKind::F64, targets: vec![Target { node: 0, slot: 0 }], }], vec![ Edge { from: 0, to: 1, slot: 0, from_field: 0 }, Edge { from: 0, to: 2, slot: 0, from_field: 0 }, Edge { from: 0, to: 3, slot: 0, from_field: 0 }, ], ) .expect("valid fan-out DAG") }; let prices = f64_stream(&[(1, 2.0), (2, 4.0), (3, 6.0), (4, 8.0), (5, 10.0)]); let (a1, ra) = mpsc::channel(); let (b1, rb) = mpsc::channel(); let (c1, rc) = mpsc::channel(); let mut h = build(a1, b1, c1); h.run(vec![prices.clone()]); let s1: Vec<(Timestamp, Vec)> = ra.try_iter().collect(); let s2: Vec<(Timestamp, Vec)> = rb.try_iter().collect(); let s3: Vec<(Timestamp, Vec)> = rc.try_iter().collect(); // SMA(3) warms at cycle 3: mean(2,4,6)=4, mean(4,6,8)=6, mean(6,8,10)=8. let expected = vec![ (Timestamp(3), vec![Scalar::F64(4.0)]), (Timestamp(4), vec![Scalar::F64(6.0)]), (Timestamp(5), vec![Scalar::F64(8.0)]), ]; assert_eq!(s1, expected); assert_eq!(s2, expected); // every tap sees the identical shared stream assert_eq!(s3, expected); // determinism (C1): a fresh harness drains bit-identical. let (a2, ra2) = mpsc::channel(); let (b2, rb2) = mpsc::channel(); let (c2, rc2) = mpsc::channel(); let mut h2 = build(a2, b2, c2); h2.run(vec![prices]); assert_eq!(ra2.try_iter().collect::>(), expected); assert_eq!(rb2.try_iter().collect::>(), expected); assert_eq!(rc2.try_iter().collect::>(), expected); } #[test] fn node_fan_out_divergent_consumers_each_compute_their_own() { // PROPERTY: one producer (SMA(2)) read by consumers that do DIFFERENT // things — recorded raw by one sink AND fed as input0 of a Sub whose // 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( vec![ Box::new(Sma::new(2)), // 0: shared producer Box::new(Sma::new(4)), // 1: second producer Box::new(Sub::new()), // 2: SMA(2) - SMA(4) 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![SourceSpec { kind: ScalarKind::F64, targets: vec![Target { node: 0, slot: 0 }, Target { node: 1, slot: 0 }], }], vec![ Edge { from: 0, to: 3, slot: 0, from_field: 0 }, // SMA(2) -> raw recorder Edge { from: 0, to: 2, slot: 0, from_field: 0 }, // SMA(2) -> Sub.in0 Edge { from: 1, to: 2, slot: 1, from_field: 0 }, // SMA(4) -> Sub.in1 Edge { from: 2, to: 4, slot: 0, from_field: 0 }, // Sub -> Sub recorder ], ) .expect("valid divergent-fan-out DAG") }; let prices = f64_stream(&[(1, 10.0), (2, 12.0), (3, 14.0), (4, 16.0), (5, 18.0), (6, 20.0)]); let (tr, rr) = mpsc::channel(); let (ts, rs) = mpsc::channel(); let mut h = build(tr, ts); h.run(vec![prices.clone()]); let raw: Vec<(Timestamp, Vec)> = rr.try_iter().collect(); let sub: Vec<(Timestamp, Vec)> = rs.try_iter().collect(); // SMA(2): 11,13,15,17,19 at cycles 2..6 (raw tap). let raw_expected = vec![ (Timestamp(2), vec![Scalar::F64(11.0)]), (Timestamp(3), vec![Scalar::F64(13.0)]), (Timestamp(4), vec![Scalar::F64(15.0)]), (Timestamp(5), vec![Scalar::F64(17.0)]), (Timestamp(6), vec![Scalar::F64(19.0)]), ]; // Sub warms once SMA(4) is warm (cycle 4): 15-13, 17-15, 19-17 -> 2. let sub_expected = vec![ (Timestamp(4), vec![Scalar::F64(2.0)]), (Timestamp(5), vec![Scalar::F64(2.0)]), (Timestamp(6), vec![Scalar::F64(2.0)]), ]; assert_eq!(raw, raw_expected); assert_eq!(sub, sub_expected); let (tr2, rr2) = mpsc::channel(); let (ts2, rs2) = mpsc::channel(); let mut h2 = build(tr2, ts2); h2.run(vec![prices]); assert_eq!(rr2.try_iter().collect::>(), raw_expected); // deterministic assert_eq!(rs2.try_iter().collect::>(), sub_expected); } #[test] fn node_fan_out_under_mixed_firing_each_consumer_records_per_policy() { // PROPERTY: one producer (SMA(2)) read simultaneously by an as-of // (Firing::Any) consumer and a Firing::Barrier consumer — each records // per ITS OWN firing policy off the SAME shared upstream value. The Any // 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( 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![ SourceSpec { kind: ScalarKind::F64, targets: vec![Target { node: 0, slot: 0 }] }, // A SourceSpec { kind: ScalarKind::F64, targets: vec![Target { node: 2, slot: 1 }] }, // B ], vec![ Edge { from: 0, to: 1, slot: 0, from_field: 0 }, // SMA(2) -> as-of recorder Edge { from: 0, to: 2, slot: 0, from_field: 0 }, // SMA(2) -> barrier.in0 Edge { from: 2, to: 3, slot: 0, from_field: 0 }, // barrier -> recorder ], ) .expect("valid mixed-firing fan-out DAG") }; let a = f64_stream(&[(1, 10.0), (2, 12.0), (3, 14.0), (4, 16.0)]); // drives SMA(2) let b = f64_stream(&[(2, 100.0), (4, 200.0)]); // barrier.in1 let (ta, rany) = mpsc::channel(); let (tb, rbar) = mpsc::channel(); let mut h = build(ta, tb); h.run(vec![a.clone(), b.clone()]); let any: Vec<(Timestamp, Vec)> = rany.try_iter().collect(); let bar: Vec<(Timestamp, Vec)> = rbar.try_iter().collect(); // As-of tap records every SMA(2) fire: 11@t2, 13@t3, 15@t4. let any_expected = vec![ (Timestamp(2), vec![Scalar::F64(11.0)]), (Timestamp(3), vec![Scalar::F64(13.0)]), (Timestamp(4), vec![Scalar::F64(15.0)]), ]; // Barrier fires only where held SMA output and src B share a timestamp: // t=2 (SMA held=11 + B=100 = 111), t=4 (SMA held=15 + B=200 = 215). let bar_expected = vec![ (Timestamp(2), vec![Scalar::F64(111.0)]), (Timestamp(4), vec![Scalar::F64(215.0)]), ]; assert_eq!(any, any_expected); assert_eq!(bar, bar_expected); let (ta2, rany2) = mpsc::channel(); let (tb2, rbar2) = mpsc::channel(); let mut h2 = build(ta2, tb2); h2.run(vec![a, b]); assert_eq!(rany2.try_iter().collect::>(), any_expected); // deterministic assert_eq!(rbar2.try_iter().collect::>(), bar_expected); } #[test] fn deep_transform_chain_propagates_end_to_end() { // PROPERTY: a linear chain of several transform nodes (SMA(2) -> SMA(2) // -> 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( 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![SourceSpec { kind: ScalarKind::F64, targets: vec![Target { node: 0, slot: 0 }], }], vec![ Edge { from: 0, to: 1, slot: 0, from_field: 0 }, Edge { from: 1, to: 2, slot: 0, from_field: 0 }, Edge { from: 2, to: 3, slot: 0, from_field: 0 }, ], ) .expect("valid deep chain") }; let prices = f64_stream(&[(1, 2.0), (2, 4.0), (3, 6.0), (4, 8.0), (5, 10.0)]); let (tx, rx) = mpsc::channel(); let mut h = build(tx); h.run(vec![prices.clone()]); let out: Vec<(Timestamp, Vec)> = rx.try_iter().collect(); // stage1 (c2..c5): 3,5,7,9. stage2 (c3..): 4,6,8. stage3 (c4..): 5,7. let expected = vec![ (Timestamp(4), vec![Scalar::F64(5.0)]), (Timestamp(5), vec![Scalar::F64(7.0)]), ]; assert_eq!(out, expected); let (tx2, rx2) = mpsc::channel(); let mut h2 = build(tx2); h2.run(vec![prices]); assert_eq!(rx2.try_iter().collect::>(), expected); // deterministic } #[test] fn wide_parallel_layer_multi_sink_records_each_stream() { // PROPERTY: one source fanned to several PARALLEL producers of different // params (SMA(2), SMA(3), SMA(4)), each recorded by its own sink in one // run, yields each parallel stream correctly — closes "wide layers // untested" and exercises multi-sink at width. let build = |t2, t3, t4| { Harness::bootstrap( vec![ Box::new(Sma::new(2)), // 0 Box::new(Sma::new(3)), // 1 Box::new(Sma::new(4)), // 2 Box::new(Recorder::new(&[ScalarKind::F64], Firing::Any, t2)), // 3 Box::new(Recorder::new(&[ScalarKind::F64], Firing::Any, t3)), // 4 Box::new(Recorder::new(&[ScalarKind::F64], Firing::Any, t4)), // 5 ], vec![SourceSpec { kind: ScalarKind::F64, targets: vec![ Target { node: 0, slot: 0 }, Target { node: 1, slot: 0 }, Target { node: 2, slot: 0 }, ], }], vec![ Edge { from: 0, to: 3, slot: 0, from_field: 0 }, Edge { from: 1, to: 4, slot: 0, from_field: 0 }, Edge { from: 2, to: 5, slot: 0, from_field: 0 }, ], ) .expect("valid wide layer") }; let prices = f64_stream(&[(1, 10.0), (2, 12.0), (3, 14.0), (4, 16.0), (5, 18.0)]); let (a, ra) = mpsc::channel(); let (b, rb) = mpsc::channel(); let (c, rc) = mpsc::channel(); let mut h = build(a, b, c); h.run(vec![prices.clone()]); let w2: Vec<(Timestamp, Vec)> = ra.try_iter().collect(); let w3: Vec<(Timestamp, Vec)> = rb.try_iter().collect(); let w4: Vec<(Timestamp, Vec)> = rc.try_iter().collect(); let e2 = vec![ (Timestamp(2), vec![Scalar::F64(11.0)]), (Timestamp(3), vec![Scalar::F64(13.0)]), (Timestamp(4), vec![Scalar::F64(15.0)]), (Timestamp(5), vec![Scalar::F64(17.0)]), ]; let e3 = vec![ (Timestamp(3), vec![Scalar::F64(12.0)]), (Timestamp(4), vec![Scalar::F64(14.0)]), (Timestamp(5), vec![Scalar::F64(16.0)]), ]; let e4 = vec![ (Timestamp(4), vec![Scalar::F64(13.0)]), (Timestamp(5), vec![Scalar::F64(15.0)]), ]; assert_eq!(w2, e2); assert_eq!(w3, e3); assert_eq!(w4, e4); let (a2, ra2) = mpsc::channel(); let (b2, rb2) = mpsc::channel(); let (c2, rc2) = mpsc::channel(); let mut h2 = build(a2, b2, c2); h2.run(vec![prices]); assert_eq!(ra2.try_iter().collect::>(), e2); // deterministic assert_eq!(rb2.try_iter().collect::>(), e3); assert_eq!(rc2.try_iter().collect::>(), e4); } #[test] fn milestone_end_to_end_mixed_dag_records_every_stream_deterministically() { // PROPERTY (#3 headline): a single richer DAG combining node fan-out // (SMA(2) -> raw recorder AND Sub) + source fan-out (source -> SMA(2), // SMA(4)) + fan-in (Sub) + multiple sinks of MIXED scalar kinds (two f64 // streams + one i64 stream) records every stream correctly AND is fully // 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( vec![ Box::new(Sma::new(2)), // 0: shared f64 producer Box::new(Sma::new(4)), // 1: second f64 producer Box::new(Sub::new()), // 2: SMA(2) - SMA(4) Box::new(Recorder::new(&[ScalarKind::F64], Firing::Any, t_raw)), // 3 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![ SourceSpec { kind: ScalarKind::F64, targets: vec![Target { node: 0, slot: 0 }, Target { node: 1, slot: 0 }], }, SourceSpec { kind: ScalarKind::I64, targets: vec![Target { node: 5, slot: 0 }] }, ], vec![ Edge { from: 0, to: 3, slot: 0, from_field: 0 }, // SMA(2) raw Edge { from: 0, to: 2, slot: 0, from_field: 0 }, // SMA(2) -> Sub.in0 Edge { from: 1, to: 2, slot: 1, from_field: 0 }, // SMA(4) -> Sub.in1 Edge { from: 2, to: 4, slot: 0, from_field: 0 }, // Sub -> recorder ], ) .expect("valid milestone DAG") }; let prices = f64_stream(&[(1, 10.0), (2, 12.0), (3, 14.0), (4, 16.0), (5, 18.0), (6, 20.0)]); let counts: Vec<(Timestamp, Scalar)> = vec![ (Timestamp(1), Scalar::I64(7)), (Timestamp(2), Scalar::I64(8)), (Timestamp(3), Scalar::I64(9)), ]; let (tr, rr) = mpsc::channel(); let (ts, rs) = mpsc::channel(); let (ti, ri) = mpsc::channel(); let mut h = build(tr, ts, ti); h.run(vec![prices.clone(), counts.clone()]); let raw: Vec<(Timestamp, Vec)> = rr.try_iter().collect(); let sub: Vec<(Timestamp, Vec)> = rs.try_iter().collect(); let i64s: Vec<(Timestamp, Vec)> = ri.try_iter().collect(); let raw_expected = vec![ (Timestamp(2), vec![Scalar::F64(11.0)]), (Timestamp(3), vec![Scalar::F64(13.0)]), (Timestamp(4), vec![Scalar::F64(15.0)]), (Timestamp(5), vec![Scalar::F64(17.0)]), (Timestamp(6), vec![Scalar::F64(19.0)]), ]; let sub_expected = vec![ (Timestamp(4), vec![Scalar::F64(2.0)]), (Timestamp(5), vec![Scalar::F64(2.0)]), (Timestamp(6), vec![Scalar::F64(2.0)]), ]; let i64_expected = vec![ (Timestamp(1), vec![Scalar::I64(7)]), (Timestamp(2), vec![Scalar::I64(8)]), (Timestamp(3), vec![Scalar::I64(9)]), ]; assert_eq!(raw, raw_expected); assert_eq!(sub, sub_expected); assert_eq!(i64s, i64_expected); let (tr2, rr2) = mpsc::channel(); let (ts2, rs2) = mpsc::channel(); let (ti2, ri2) = mpsc::channel(); let mut h2 = build(tr2, ts2, ti2); h2.run(vec![prices, counts]); assert_eq!(rr2.try_iter().collect::>(), raw_expected); // deterministic assert_eq!(rs2.try_iter().collect::>(), sub_expected); assert_eq!(ri2.try_iter().collect::>(), i64_expected); } #[test] fn signal_quality_loop_records_pip_equity() { let (tx_eq, rx_eq) = mpsc::channel(); let mut h = Harness::bootstrap( vec![ Box::new(Sma::new(2)), // 0 fast Box::new(Sma::new(4)), // 1 slow Box::new(Sub::new()), // 2 raw signal Box::new(Exposure::new(0.5)), // 3 exposure Box::new(SimBroker::new(0.0001)), // 4 pip equity Box::new(Recorder::new(&[ScalarKind::F64], Firing::Any, tx_eq)), // 5 sink ], vec![SourceSpec { kind: ScalarKind::F64, targets: vec![ Target { node: 0, slot: 0 }, // price -> SMA fast Target { node: 1, slot: 0 }, // price -> SMA slow Target { node: 4, slot: 1 }, // price -> broker price input ], }], vec![ Edge { from: 0, to: 2, slot: 0, from_field: 0 }, // fast -> Sub.0 Edge { from: 1, to: 2, slot: 1, from_field: 0 }, // slow -> Sub.1 Edge { from: 2, to: 3, slot: 0, from_field: 0 }, // signal -> Exposure Edge { from: 3, to: 4, slot: 0, from_field: 0 }, // exposure -> broker.0 Edge { from: 4, to: 5, slot: 0, from_field: 0 }, // equity -> recorder ], ) .expect("valid signal-quality harness"); h.run(vec![f64_stream(&[ (1, 1.0000), (2, 1.0010), (3, 1.0025), (4, 1.0020), (5, 1.0040), ])]); let equity: Vec<(Timestamp, Vec)> = rx_eq.try_iter().collect(); // broker fires every cycle (price fresh each tick): five records, ts 1..=5 assert_eq!(equity.len(), 5); assert_eq!( equity.iter().map(|(t, _)| t.0).collect::>(), vec![1, 2, 3, 4, 5] ); // flat until SMA(4) warms (cycle 4) and the held exposure meets the next // price move (cycle 5): the first four equities are exactly 0. for (_, row) in &equity[0..4] { assert_eq!(row, &vec![Scalar::F64(0.0)]); } // cycle 5: prev_exposure 0.00175 * (1.0040 - 1.0020) / 0.0001 = 0.035 pips let Scalar::F64(last) = equity[4].1[0] else { panic!("equity is f64"); }; assert!((last - 0.035).abs() < 1e-9, "final equity = {last}, want ~0.035"); } #[test] fn signal_quality_loop_is_deterministic() { let build = || { let (tx, rx) = mpsc::channel(); let mut h = Harness::bootstrap( vec![ Box::new(Sma::new(2)), Box::new(Sma::new(4)), Box::new(Sub::new()), Box::new(Exposure::new(0.5)), Box::new(SimBroker::new(0.0001)), Box::new(Recorder::new(&[ScalarKind::F64], Firing::Any, tx)), ], vec![SourceSpec { kind: ScalarKind::F64, targets: vec![ Target { node: 0, slot: 0 }, Target { node: 1, slot: 0 }, Target { node: 4, slot: 1 }, ], }], 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 }, Edge { from: 3, to: 4, slot: 0, from_field: 0 }, Edge { from: 4, to: 5, slot: 0, from_field: 0 }, ], ) .expect("valid harness"); h.run(vec![f64_stream(&[ (1, 1.0000), (2, 1.0010), (3, 1.0025), (4, 1.0020), (5, 1.0040), ])]); rx.try_iter().collect::)>>() }; assert_eq!(build(), build()); } }