tidy(aura-engine): re-export scalar vocab (#29) + drop duplicate Recorder fixtures (#14)

Two behaviour-preserving tidies from the post-0013 fieldtest/backlog.

#29: aura-engine re-exports Firing/Scalar/ScalarKind/Timestamp at the crate
root. A Blueprint builder needs ScalarKind for SourceSpec.kind and Scalar/
Firing/Timestamp for sources & Recorder columns, but aura-engine previously
re-exported only the wiring types — forcing a second aura-core import for the
vocabulary its own public structs (SourceSpec.kind, ...) demand. The fieldtest
flagged this paper cut (mc_4). One import surface now. Guarded by a new
reexport_tests module.

#14: the two near-identical #[cfg(test)] Recorder fixtures in report.rs and
harness.rs are deleted in favour of the shipped aura-std::Recorder (already a
dev-dependency). Constructor signature, schema, and try_iter() drain are
identical, so no call site changed — only the struct/impl deletion plus adding
Recorder to each file's existing aura_std import. report.rs additionally drops
four imports (Ctx/InputSpec/Node/NodeSchema) that only the deleted fixture
used. The separate TapForward fixture in harness.rs is untouched.

Verified: cargo build/test/clippy --workspace all green (49 aura-engine lib
tests incl. the new one); bit-identical, golden-snapshot, SMA-disambiguation,
and mixed-kind recording tests all stay green — behaviour preserved.

closes #29
closes #14
This commit is contained in:
2026-06-05 23:25:48 +02:00
parent 73938a4e4a
commit 561482c422
3 changed files with 22 additions and 119 deletions
+1 -73
View File
@@ -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<ScalarKind>,
firing: Firing,
tx: mpsc::Sender<(Timestamp, Vec<Scalar>)>,
}
impl Recorder {
fn new(
kinds: &[ScalarKind],
firing: Firing,
tx: mpsc::Sender<(Timestamp, Vec<Scalar>)>,
) -> 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.
+19
View File
@@ -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);
}
}
+2 -46
View File
@@ -199,8 +199,8 @@ pub fn f64_field(rows: &[(Timestamp, Vec<Scalar>)], 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<ScalarKind>,
firing: Firing,
tx: mpsc::Sender<(Timestamp, Vec<Scalar>)>,
}
impl Recorder {
fn new(
kinds: &[ScalarKind],
firing: Firing,
tx: mpsc::Sender<(Timestamp, Vec<Scalar>)>,
) -> 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.