diff --git a/crates/aura-engine/src/harness.rs b/crates/aura-engine/src/harness.rs index 33231eb..2787875 100644 --- a/crates/aura-engine/src/harness.rs +++ b/crates/aura-engine/src/harness.rs @@ -337,7 +337,7 @@ mod tests { // 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 aura_std::{Exposure, Recorder, Sma, SimBroker, Sub}; use std::sync::mpsc; /// Build an f64 source stream from (timestamp, value) points. @@ -486,78 +486,6 @@ mod tests { } } - /// 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. diff --git a/crates/aura-engine/src/lib.rs b/crates/aura-engine/src/lib.rs index 16d3b85..45b9232 100644 --- a/crates/aura-engine/src/lib.rs +++ b/crates/aura-engine/src/lib.rs @@ -38,3 +38,22 @@ mod report; pub use blueprint::{Blueprint, BlueprintNode, CompileError, Composite, OutPort}; pub use harness::{BootstrapError, Edge, 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, Scalar, ScalarKind, Timestamp}; + +#[cfg(test)] +mod reexport_tests { + // #29: the core scalar vocabulary a Blueprint builder needs is reachable + // from aura-engine alone (crate::X == external aura_engine::X), so a + // downstream author does not add a second aura-core import for ScalarKind. + #[test] + fn core_scalar_vocabulary_is_reexported_from_crate_root() { + use crate::{Firing, Scalar, ScalarKind, Timestamp}; + let _k: ScalarKind = ScalarKind::F64; + let _f: Firing = Firing::Any; + let _s: Scalar = Scalar::F64(0.0); + let _t: Timestamp = Timestamp(0); + } +} diff --git a/crates/aura-engine/src/report.rs b/crates/aura-engine/src/report.rs index 973e2d7..253e9a0 100644 --- a/crates/aura-engine/src/report.rs +++ b/crates/aura-engine/src/report.rs @@ -199,8 +199,8 @@ pub fn f64_field(rows: &[(Timestamp, Vec)], field: usize) -> Vec<(Timest mod tests { use super::*; use crate::{Edge, Harness, SourceSpec, Target}; - use aura_core::{Ctx, Firing, InputSpec, Node, NodeSchema, ScalarKind}; - use aura_std::{Exposure, SimBroker, Sma, Sub}; + use aura_core::{Firing, ScalarKind}; + use aura_std::{Exposure, Recorder, SimBroker, Sma, Sub}; use std::sync::mpsc; /// Build an f64 source stream from (timestamp, value) points (mirrors the @@ -210,50 +210,6 @@ mod tests { points.iter().map(|&(t, v)| (Timestamp(t), Scalar::F64(v))).collect() } - /// A recording sink that sends `(now, row)` out of the graph each fired - /// cycle and returns `None` (pure consumer, C8). Re-declared here because - /// the identically-shaped fixture in `harness.rs` is private to that test - /// module. - 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![], - } - } - fn eval(&mut self, ctx: Ctx<'_>) -> Option<&[Scalar]> { - // this test records only f64 streams (one f64 column per sink) - let mut row = Vec::with_capacity(self.kinds.len()); - for i in 0..self.kinds.len() { - let w = ctx.f64_in(i); - if w.is_empty() { - return None; - } - row.push(Scalar::F64(w[0])); - } - let _ = self.tx.send((ctx.now(), row)); - None - } - } - /// Bootstrap the cycle-0007 signal-quality harness with TWO sinks: one on /// the SimBroker equity output (node 4 -> node 5) and one on the Exposure /// output (node 3 -> node 6). Returns the harness plus the two receivers.