feat: recording is a node role, not a type (multi-sink substrate)
Replace the engine's single observe: usize recording affordance with recording-by-node, so one run records many streams. A recording node reads its typed input windows + ctx.now() in eval and pushes the record to a destination it holds as a field (a channel, a chart handle) — an out-of-graph side effect. There is no Sink type, trait, or engine flag: a pure-consumer node returns None, and a node may record AND return a forwarded output in the same eval (the C8 "both" case). In-graph routing stays engine-owned data (the edge table); the escape out of the graph is the node's own side effect, and that boundary is the determinism / graph-as-data boundary. Engine surface shrinks: Ctx gains now: Timestamp + now() (C2-causal, the present cycle's timestamp); Harness loses the observe field, its observe >= n bootstrap check, and the per-cycle observed-row collection; bootstrap drops its 4th param; run returns (). Recorded streams are now sparse and timestamped (a record per fired cycle) instead of the dense Vec<Option<row>>. The Ctx::new signature change touched 9 call sites across three crates (not the 3 the spec estimated) — aura-std's node tests and the engine run loop were threaded too. The engine test suite migrated to a test-local Recorder fixture whose read-back is an mpsc channel, never Rc/RefCell, keeping aura-engine/src purity-clean (C7). Eight new proof tests cover the multi-sink headline, producer-and-sink, mixed-kind recording, all-fields tap, both recorder firing modes, determinism, and recorder-edge kind rejection. C8/C22 gain cycle-0006 realization notes. Gates: workspace test 45 green (core 20, std 3, engine 22), clippy -D warnings clean, purity grep clean (only a comment names Rc/RefCell). closes #2
This commit is contained in:
@@ -48,7 +48,7 @@ impl Node for Sma {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use aura_core::AnyColumn;
|
||||
use aura_core::{AnyColumn, Timestamp};
|
||||
|
||||
#[test]
|
||||
fn sma_warms_up_then_tracks_the_window_mean() {
|
||||
@@ -67,7 +67,7 @@ mod tests {
|
||||
|
||||
for (v, want) in feed.iter().zip(expect) {
|
||||
inputs[0].push(Scalar::F64(*v)).unwrap();
|
||||
let got = sma.eval(Ctx::new(&inputs));
|
||||
let got = sma.eval(Ctx::new(&inputs, Timestamp(0)));
|
||||
match want {
|
||||
None => assert_eq!(got, None),
|
||||
Some(m) => assert_eq!(got, Some([Scalar::F64(m)].as_slice())),
|
||||
@@ -81,9 +81,9 @@ mod tests {
|
||||
let mut inputs = vec![AnyColumn::with_capacity(ScalarKind::F64, 1)];
|
||||
|
||||
inputs[0].push(Scalar::F64(7.0)).unwrap();
|
||||
assert_eq!(sma.eval(Ctx::new(&inputs)), Some([Scalar::F64(7.0)].as_slice()));
|
||||
assert_eq!(sma.eval(Ctx::new(&inputs, Timestamp(0))), Some([Scalar::F64(7.0)].as_slice()));
|
||||
|
||||
inputs[0].push(Scalar::F64(9.0)).unwrap();
|
||||
assert_eq!(sma.eval(Ctx::new(&inputs)), Some([Scalar::F64(9.0)].as_slice()));
|
||||
assert_eq!(sma.eval(Ctx::new(&inputs, Timestamp(0))), Some([Scalar::F64(9.0)].as_slice()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ impl Node for Sub {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use aura_core::AnyColumn;
|
||||
use aura_core::{AnyColumn, Timestamp};
|
||||
|
||||
#[test]
|
||||
fn sub_is_difference_once_both_inputs_present() {
|
||||
@@ -61,10 +61,10 @@ mod tests {
|
||||
|
||||
// only input 0 present -> None
|
||||
inputs[0].push(Scalar::F64(10.0)).unwrap();
|
||||
assert_eq!(sub.eval(Ctx::new(&inputs)), None);
|
||||
assert_eq!(sub.eval(Ctx::new(&inputs, Timestamp(0))), None);
|
||||
|
||||
// both present -> a - b
|
||||
inputs[1].push(Scalar::F64(4.0)).unwrap();
|
||||
assert_eq!(sub.eval(Ctx::new(&inputs)), Some([Scalar::F64(6.0)].as_slice()));
|
||||
assert_eq!(sub.eval(Ctx::new(&inputs, Timestamp(0))), Some([Scalar::F64(6.0)].as_slice()));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user