// Milestone fieldtest — Topology-as-data (C24), Example 1. // // The headline round-trip the milestone promises: a researcher authors a small // SMA-cross bias signal with the Rust GraphBuilder, serializes the topology to // canonical JSON (blueprint_to_json), loads it back through the std vocabulary // resolver (blueprint_from_json + aura_std::std_vocabulary), and proves // (a) the canonical JSON round-trips byte-identically (serialize -> load -> // re-serialize == original), and // (b) the RELOADED blueprint runs BIT-IDENTICALLY to its Rust-built twin // (C1: the recorded bias trace matches value-for-value). // // Discovery: the GraphBuilder / blueprint_to_json / blueprint_from_json surface // was learned from `cargo doc` doc-comments + the C24 ledger entry + `aura graph // introspect --node ` (which gives each node's port and bind shapes). No // crates/*/src was read. // // Note on the run path: a recording sink (Recorder) is deliberately OUTSIDE the // serializable round-trippable set (C24 ledger: it captures an mpsc::Sender — // runtime identity, not param-generic data). So the *serializable* unit is the // pure signal sub-composite (price source -> SMAs -> Sub -> Bias -> expose // bias); to OBSERVE a run we nest that sub-composite under a tiny throwaway // harness wrapper that adds a Recorder. The same wrapper is applied to both the // original and the reloaded sub-composite, so the comparison is apples-to-apples. use std::sync::mpsc; use aura_core::{Firing, Scalar, ScalarKind, Timestamp}; use aura_engine::{ blueprint_from_json, blueprint_to_json, BlueprintNode, Composite, Edge, GraphBuilder, OutField, Role, Target, VecSource, }; use aura_std::{std_vocabulary, Bias, Recorder, Sma, Sub}; /// Author the serializable signal sub-composite via the Rust builder API. /// SMA(2) fast, SMA(4) slow, Sub = fast - slow, Bias over the spread, exposing /// the single `bias` field. The two SMA lengths are baked (`.bind`); Bias.scale /// is left as a tunable param (mirrors the canonical op-script form). fn author_signal() -> Composite { let mut gb = GraphBuilder::new("graph"); let price = gb.source_role("price", ScalarKind::F64); let fast = gb.add(Sma::builder().named("fast").bind("length", Scalar::I64(2))); let slow = gb.add(Sma::builder().named("slow").bind("length", Scalar::I64(4))); gb.feed(price, [fast.input("series"), slow.input("series")]); let sub = gb.add(Sub::builder().named("sub")); gb.connect(fast.output("value"), sub.input("lhs")); gb.connect(slow.output("value"), sub.input("rhs")); let bias = gb.add(Bias::builder().named("bias")); gb.connect(sub.output("value"), bias.input("signal")); gb.expose(bias.output("bias"), "bias"); gb.build().expect("signal composite should build") } /// Run a signal sub-composite by nesting it under a Recorder harness and /// recording the exposed `bias` each fired cycle. Returns (ts, bias) rows. fn run_signal(signal: Composite, prices: &[f64]) -> Vec<(i64, f64)> { let (tx, rx) = mpsc::channel::<(Timestamp, Vec)>(); let harness = Composite::new( "harness", vec![ BlueprintNode::Composite(signal), Recorder::builder(vec![ScalarKind::F64], Firing::Any, tx).into(), ], vec![Edge { from: 0, to: 1, slot: 0, from_field: 0 }], // bias -> recorder col 0 vec![Role { name: "price".into(), targets: vec![Target { node: 0, slot: 0 }], source: Some(ScalarKind::F64), }], vec![] as Vec, ); let stream: Vec<(Timestamp, Scalar)> = prices .iter() .enumerate() .map(|(i, &p)| (Timestamp(60_000_000_000 * i as i64), Scalar::F64(p))) .collect(); // bias.scale is the one tunable param left in the space; bind it at bootstrap. let mut h = harness .bootstrap_with_params(vec![Scalar::F64(1.0)]) .expect("harness should bootstrap"); h.run(vec![Box::new(VecSource::new(stream))]); drop(h); // release the recorder's tx clone rx.iter() .map(|(ts, row)| match row[0] { Scalar::F64(x) => (ts.0, x), other => panic!("expected f64 bias, got {other:?}"), }) .collect() } fn main() { let prices: Vec = vec![ 1.00, 1.01, 1.02, 1.03, 1.05, 1.08, 1.06, 1.04, 1.02, 1.01, 1.03, 1.07, ]; // --- (a) canonical JSON round-trip --------------------------------------- let orig = author_signal(); let json1 = blueprint_to_json(&orig).expect("serialize original"); std::fs::write("mt_1_rust_blueprint.json", &json1).expect("persist artifact"); let loaded = blueprint_from_json(&json1, &std_vocabulary).expect("load from json"); let json2 = blueprint_to_json(&loaded).expect("re-serialize loaded"); assert_eq!( json1, json2, "canonical JSON must round-trip byte-identically (serialize->load->re-serialize)" ); println!("(a) canonical JSON round-trip: IDENTICAL ({} bytes)", json1.len()); // --- (b) run identity: reloaded twin runs bit-identical to the original --- let orig_run = run_signal(author_signal(), &prices); let loaded_run = run_signal(loaded, &prices); assert_eq!( orig_run, loaded_run, "C1: the reloaded blueprint must run bit-identically to its Rust-built twin" ); println!( "(b) run identity: {} recorded bias rows, value-for-value IDENTICAL", orig_run.len() ); for (ts, b) in &loaded_run { println!(" ts={ts:>14} bias={b:+.6}"); } println!("PASS: author -> serialize -> load -> reproduce (C1 round-trip)."); }