Files
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

109 lines
3.5 KiB
Rust

//! Fieldtest c0006 #2 — multi-stage fan-out / fan-in DAG, bootstrapped + run.
//!
//! Axes: "Build a multi-stage fan-out / fan-in DAG (one source fanning into
//! SMA(2) and SMA(4), then a combiner)" and "Bootstrap and run a Harness (note
//! the new bootstrap arity and that run returns ())."
//!
//! source --> SMA(2) --\
//! \ Sub(fast - slow) --> Recorder
//! \-> SMA(4) --/
//!
//! A downstream author wires the classic spread DAG and records the combiner's
//! output. This fixture leans on the post-0006 surface: bootstrap takes exactly
//! THREE positional args (no observe), and run returns () — retention is the
//! recorder's channel, owned by the caller.
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, Sub};
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,
}],
output: vec![],
}
}
fn eval(&mut self, ctx: Ctx<'_>) -> Option<&[Scalar]> {
let w = ctx.f64_in(0);
if w.is_empty() {
return None;
}
let _ = self.tx.send((ctx.now(), w[0]));
None
}
}
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();
// nodes: 0 = SMA(2), 1 = SMA(4), 2 = Sub(0 - 1), 3 = Recorder taps Sub.
let mut h = Harness::bootstrap(
vec![
Box::new(Sma::new(2)),
Box::new(Sma::new(4)),
Box::new(Sub::new()),
Box::new(Recorder::new(tx)),
],
vec![SourceSpec {
kind: ScalarKind::F64,
// source fans out into both SMAs
targets: vec![Target { node: 0, slot: 0 }, Target { node: 1, slot: 0 }],
}],
vec![
Edge { from: 0, to: 2, slot: 0, from_field: 0 }, // SMA(2) -> Sub.in0
Edge { from: 1, to: 2, slot: 1, from_field: 0 }, // SMA(4) -> Sub.in1
Edge { from: 2, to: 3, slot: 0, from_field: 0 }, // Sub -> Recorder
],
)
.expect("valid DAG");
// ascending-in-price stream: SMA(2) leads SMA(4), spread stays constant once warm.
h.run(vec![f64_stream(&[
(1, 10.0),
(2, 12.0),
(3, 14.0),
(4, 16.0),
(5, 18.0),
(6, 20.0),
])]);
let recorded: Vec<(Timestamp, f64)> = rx.try_iter().collect();
// Sub fires once both SMAs are warm. SMA(4) warms at the 4th sample (t=4).
// t=4: SMA2=mean(16,14)=15, SMA4=mean(16,14,12,10)=13 -> 2
// t=5: SMA2=mean(18,16)=17, SMA4=mean(18,16,14,12)=15 -> 2
// t=6: SMA2=mean(20,18)=19, SMA4=mean(20,18,16,14)=17 -> 2
let expected = vec![
(Timestamp(4), 2.0),
(Timestamp(5), 2.0),
(Timestamp(6), 2.0),
];
println!("recorded = {recorded:?}");
println!("expected = {expected:?}");
assert_eq!(recorded, expected, "fan-out/fan-in spread DAG recorded output");
println!("c0006_2 OK: fan-out/fan-in DAG bootstrapped (3-arg) + run () + recorded");
}