Files
Aura/fieldtests/cycle-0006-substrate/c0006_1_custom_node_now.rs
T
Brummel 0bf15cb069 fieldtest: cycle-0006 sink recording — 5 examples, 6 findings
First fieldtest of the project. A standalone downstream-consumer crate
(fieldtests/cycle-0006-substrate/) path-depends on the engine crates and
exercises the post-0006 substrate from the public interface only (rustdoc +
specs + ledger, never crates/ source):

  1 custom recording node via the Node contract + Ctx::now()
  2 fan-out/fan-in DAG, 3-arg bootstrap, run() -> ()
  3 two interior streams recorded from one run (the cycle headline)
  4 byte-identical recorded output across two runs (C1 determinism)
  5 mis-wired recording edge rejected (KindMismatch) at bootstrap

Findings: 3 working (carry-on), 2 friction, 1 spec_gap.
  - friction: a nested standalone consumer crate fights the workspace
    resolver (needs an empty [workspace] table — Cargo's own hint).
  - friction: recorder boilerplate is hand-rewritten per author (C9
    deliberately ships no library sink).
  - spec_gap: the public surface never states output: vec![] is THE sink
    declaration, nor defines a Some return paired with empty output.

Spec at docs/specs/fieldtest-0006-substrate.md feeds the 0007 plan.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 15:36:50 +02:00

101 lines
3.4 KiB
Rust

//! 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");
}