// Cycle-0031 fieldtest — scenario 3 (axis c): default names + the paramless // "interchangeable stays legal" corner + node-name visibility. // // Probes (spec 0031 §"Default name", §"Paramless interchangeable stays legal", // and the carrier's optional 3rd axis): // (a) a single un-named primitive emits `.` — `sma.length`; // (b) a root-level Exposure (un-named) emits `exposure.scale`; // (c) the default lowercasing is verbatim, no snake_case insertion // ("SimBroker" -> "simbroker", not "sim_broker"); // (d) two un-named PARAMLESS same-type siblings under a fan-in stay legal // (no IndistinguishableFanIn), as the spec preserves. // // Public interface only (ledger C9/C23 + spec 0031 + cargo doc). No src read. use std::sync::mpsc; use aura_core::{Firing, Scalar, ScalarKind, Timestamp}; use aura_engine::{Composite, Edge, OutField, Role, Target}; use aura_std::{Add, Exposure, Recorder, SimBroker, Sma, Sub}; /// A trivial harness: one un-named SMA feeding Exposure -> SimBroker -> Recorder. /// No composite nesting, so every param surfaces at the ROOT level — the case /// that exercises the "root segment is now present too" rule. fn single_unnamed_harness(tx: mpsc::Sender<(Timestamp, Vec)>) -> Composite { Composite::new( "harness", vec![ Sma::builder().into(), // un-named -> default "sma" Exposure::builder().into(), // un-named -> default "exposure" SimBroker::builder(1e-4).into(), // un-named -> default "simbroker" Recorder::builder(vec![ScalarKind::F64], Firing::Any, tx).into(), ], vec![ Edge { from: 0, to: 1, slot: 0, from_field: 0 }, Edge { from: 1, to: 2, slot: 0, from_field: 0 }, Edge { from: 2, to: 3, slot: 0, from_field: 0 }, ], vec![Role { name: "price".into(), targets: vec![ Target { node: 0, slot: 0 }, Target { node: 2, slot: 1 }, ], source: Some(ScalarKind::F64), }], vec![OutField { node: 1, field: 0, name: "exposure".into() }], ) } /// Two un-named PARAMLESS same-type siblings (Add has no params) under a fan-in /// (Sub). Spec: this stays LEGAL — interchangeable paramless legs do not fire /// IndistinguishableFanIn. Built as a self-contained ROOT (the single price /// source feeds both Adds' slots) so the role is bound and the only thing under /// test is the paramless same-name fan-in rule, not role-binding. fn paramless_fanin() -> Composite { Composite::new( "paramless", vec![ Add::builder().into(), // un-named -> "add" Add::builder().into(), // un-named -> "add" — same name, but paramless Sub::builder().into(), ], vec![ Edge { from: 0, to: 2, slot: 0, from_field: 0 }, Edge { from: 1, to: 2, slot: 1, from_field: 0 }, ], vec![Role { name: "x".into(), targets: vec![ Target { node: 0, slot: 0 }, Target { node: 0, slot: 1 }, Target { node: 1, slot: 0 }, Target { node: 1, slot: 1 }, ], // bound root source — keeps the role satisfied so the compile only // exercises the paramless fan-in rule. source: Some(ScalarKind::F64), }], vec![OutField { node: 2, field: 0, name: "out".into() }], ) } fn main() { // --- (a)(b)(c) default names appear in param_space at the root level. ------ let (tx0, _rx0) = mpsc::channel(); let space = single_unnamed_harness(tx0).param_space(); println!("default-name param_space():"); for p in &space { println!(" {} : {:?}", p.name, p.kind); } let names: Vec<&str> = space.iter().map(|p| p.name.as_str()).collect(); assert!(names.contains(&"sma.length"), "expected sma.length, got {names:?}"); assert!(names.contains(&"exposure.scale"), "expected exposure.scale, got {names:?}"); // SimBroker's spread arg is a constructor value, not a knob; if SimBroker has // a param it must surface lowercased verbatim. Record whatever it emits. let simbroker_knobs: Vec<&&str> = names.iter().filter(|n| n.starts_with("sim")).collect(); println!("simbroker-prefixed knobs (verbatim-lowercase check): {simbroker_knobs:?}"); assert!( !names.iter().any(|n| n.starts_with("sim_broker")), "default name must be verbatim-lowercased 'simbroker', never snake_cased 'sim_broker' ({names:?})" ); // Bind the default names and run, to prove they are real bindable knobs. let (tx, rx) = mpsc::channel::<(Timestamp, Vec)>(); let mut h = single_unnamed_harness(tx) .with("sma.length", 3) .with("exposure.scale", 1.0) .bootstrap() .expect("default-named knobs bind by their lowercased-type paths"); let prices: Vec<(Timestamp, Scalar)> = (0..10) .map(|i| (Timestamp(60_000_000_000 * i), Scalar::F64(1.0 + 0.02 * i as f64))) .collect(); h.run(vec![prices]); drop(h); let rows: Vec<_> = rx.iter().collect(); println!("\ndefault-named run recorded {} rows", rows.len()); assert!(!rows.is_empty()); // --- (d) paramless interchangeable same-name fan-in stays LEGAL. ---------- let compiled = paramless_fanin().compile_with_params(&[]); println!("\nparamless same-name fan-in compile ok: {:?}", compiled.is_ok()); assert!( compiled.is_ok(), "two un-named PARAMLESS same-type legs must stay legal (no IndistinguishableFanIn): err={:?}", compiled.err() ); println!("\nOK: default names = lowercased type label, root-level qualified, paramless legal."); }