//! Fieldtest c0006 #1 — author a custom recording node from scratch. //! //! Axis: "Author a custom node via the public Node contract (schema / eval / //! Ctx, including Ctx::now())." //! //! A downstream author writes their own sink node — here a `Recorder` that holds //! the destination (an mpsc::Sender) as a field and, in eval, stamps each fired //! cycle's value with `ctx.now()` and pushes `(now, value)` out of the graph. //! It returns `None` (pure consumer, C8). This is the canonical recording-node //! shape the 0006 spec's "Concrete code shapes" section describes (`Recorder` is //! the spec's test-local fixture; a real author writes their ChartSink the same //! way). //! //! What this fixture proves a downstream consumer can do with ONLY the public //! surface: implement `Node` for their own type, read a typed input window, call //! `ctx.now()`, perform an out-of-graph side effect, and return `None`. use std::sync::mpsc::{self, Sender}; use aura_core::{Ctx, Firing, InputSpec, Node, NodeSchema, Scalar, ScalarKind, Timestamp}; use aura_engine::{Edge, Harness, SourceSpec, Target}; use aura_std::Sma; /// A pure-consumer recording node: one f64 input, no output, pushes /// `(ctx.now(), value)` to a channel the caller drains. struct Recorder { tx: Sender<(Timestamp, f64)>, } impl Recorder { fn new(tx: Sender<(Timestamp, f64)>) -> Self { Self { tx } } } impl Node for Recorder { fn schema(&self) -> NodeSchema { NodeSchema { inputs: vec![InputSpec { kind: ScalarKind::F64, lookback: 1, firing: Firing::Any, }], // Pure consumer (sink): no output port. output: vec![], } } fn eval(&mut self, ctx: Ctx<'_>) -> Option<&[Scalar]> { let w = ctx.f64_in(0); if w.is_empty() { return None; // not yet warmed } // The C2-causal stamp: the present cycle's timestamp, never the future. let _ = self.tx.send((ctx.now(), w[0])); None // pure sink — nothing forwarded into the graph } } fn f64_stream(pairs: &[(i64, f64)]) -> Vec<(Timestamp, Scalar)> { pairs .iter() .map(|&(t, v)| (Timestamp(t), Scalar::F64(v))) .collect() } fn main() { let (tx, rx) = mpsc::channel(); // source -> SMA(2) -> Recorder. Recorder stamps each fired SMA value with now(). let mut h = Harness::bootstrap( vec![ Box::new(Sma::new(2)), // node 0 Box::new(Recorder::new(tx)), // node 1 ], 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 DAG"); h.run(vec![f64_stream(&[(10, 10.0), (20, 12.0), (30, 14.0), (40, 16.0)])]); let recorded: Vec<(Timestamp, f64)> = rx.try_iter().collect(); // SMA(2) warms up after 2 samples, so it fires at t=20,30,40 with means // (10+12)/2=11, (12+14)/2=13, (14+16)/2=15. Each carries the cycle's now(). let expected = vec![ (Timestamp(20), 11.0), (Timestamp(30), 13.0), (Timestamp(40), 15.0), ]; println!("recorded = {recorded:?}"); println!("expected = {expected:?}"); assert_eq!(recorded, expected, "Recorder stream (timestamp via Ctx::now)"); println!("c0006_1 OK: custom node + Ctx::now() recorded a now-stamped stream"); }