11dfff860c
Per-cycle fieldtest of node-instance naming (#56), driven as a downstream consumer from the public interface only (ledger + spec + rustdoc + CLI; no crates/*/src read), three fixtures built and run from HEAD. Verdict: the core 0031 promise holds first-try. A consumer authors Sma::builder().named("fast"), inspects param_space() (sma_cross.fast.length), binds by name and runs; the default-name case (sma.length / exposure.scale, verbatim lowercase) and paramless-interchangeable-stays-legal both hold. 0 bugs, 3 working, 2 friction, 1 spec_gap. The one real gap (verified by the orchestrator against the fixture output): the spec's headline forcing function IndistinguishableFanIn does NOT reach an author on the canonical .with(...).bootstrap() flow. An un-named 2-SMA cross emits a literal DUPLICATE knob (sma_cross.sma.length x2); the binder resolves names before the compile fan-in check, so the author hits UnknownKnob / AmbiguousKnob("sma_cross.sma.length") — which point at the knob, not at the cure "name your nodes". IndistinguishableFanIn only surfaces via the positional compile_with_params path. Rejection still happens (no invalid blueprint runs), so it is an ergonomic/signposting gap, not a correctness bug. Routed to the backlog (relates to #58); 0031 stays audit-closed. Minor: FlatGraph/Harness lack Debug, so a bootstrap Result can't be {:?}-printed.
148 lines
5.5 KiB
Rust
148 lines
5.5 KiB
Rust
// Cycle-0031 fieldtest — scenario 1 (axis a): the named single-run authoring path.
|
|
//
|
|
// Promise probed (spec 0031 acceptance #1, #2, #4): a downstream author writes
|
|
// `Sma::builder().named("fast")`, inspects `param_space()` (now uniformly
|
|
// `<node>.<param>` at every level, including the root `exposure.scale`), binds
|
|
// `sma_cross.fast.length` BY NAME with NO ParamAlias and NO index counting, and
|
|
// runs. This is the exact pain the milestone-0030 fixture (mw_1) had to work
|
|
// around with two hand-counted ParamAlias overlays.
|
|
//
|
|
// Public interface only: API discovered from the ledger (C9/C23), spec 0031,
|
|
// and `cargo doc`. No crates/*/src was read.
|
|
//
|
|
// The harness: source -> sma_cross composite { Sma("fast"), Sma("slow"), Sub }
|
|
// -> Exposure -> SimBroker -> Recorder.
|
|
|
|
use std::sync::mpsc;
|
|
|
|
use aura_core::{Firing, Scalar, ScalarKind, Timestamp};
|
|
use aura_engine::{BlueprintNode, Composite, Edge, OutField, Role, Target};
|
|
use aura_std::{Exposure, Recorder, SimBroker, Sma, Sub};
|
|
|
|
/// The reusable inner 2/4-SMA-cross composite. The two SMA legs are now named
|
|
/// inline via `.named()` — the single act that (a) distinguishes the fan-in and
|
|
/// (b) qualifies the two param paths. No ParamAlias argument exists anymore
|
|
/// (Composite::new is 4-arg).
|
|
fn sma_cross() -> Composite {
|
|
Composite::new(
|
|
"sma_cross",
|
|
vec![
|
|
Sma::builder().named("fast").into(),
|
|
Sma::builder().named("slow").into(),
|
|
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: "price".into(),
|
|
targets: vec![
|
|
Target { node: 0, slot: 0 },
|
|
Target { node: 1, slot: 0 },
|
|
],
|
|
source: None,
|
|
}],
|
|
vec![OutField { node: 2, field: 0, name: "out".into() }],
|
|
)
|
|
}
|
|
|
|
/// The root harness over the named cross. The root-level Exposure node now also
|
|
/// carries a name (default "exposure"), so its knob surfaces as `exposure.scale`.
|
|
fn harness(tx: mpsc::Sender<(Timestamp, Vec<Scalar>)>) -> Composite {
|
|
Composite::new(
|
|
"harness",
|
|
vec![
|
|
BlueprintNode::Composite(sma_cross()),
|
|
Exposure::builder().into(),
|
|
SimBroker::builder(1e-4).into(),
|
|
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: 0, field: 0, name: "exposure".into() }],
|
|
)
|
|
}
|
|
|
|
fn synthetic_prices() -> Vec<(Timestamp, Scalar)> {
|
|
let prices = [
|
|
1.00, 1.01, 1.02, 1.03, 1.05, 1.08, 1.06, 1.04, 1.02, 1.01, 1.03, 1.07,
|
|
];
|
|
prices
|
|
.iter()
|
|
.enumerate()
|
|
.map(|(i, &p)| (Timestamp(60_000_000_000 * i as i64), Scalar::F64(p)))
|
|
.collect()
|
|
}
|
|
|
|
fn main() {
|
|
// --- 1. Inspect the param-space: are the names what the spec promises? -----
|
|
let (tx0, _rx0) = mpsc::channel();
|
|
let space = harness(tx0).param_space();
|
|
println!("param_space() knobs (name : kind):");
|
|
for p in &space {
|
|
println!(" {} : {:?}", p.name, p.kind);
|
|
}
|
|
let names: Vec<&str> = space.iter().map(|p| p.name.as_str()).collect();
|
|
// spec 0031 acceptance #1/#4: uniform <node>.<param>, distinct named legs.
|
|
assert!(
|
|
names.contains(&"sma_cross.fast.length"),
|
|
"expected sma_cross.fast.length, got {names:?}"
|
|
);
|
|
assert!(
|
|
names.contains(&"sma_cross.slow.length"),
|
|
"expected sma_cross.slow.length, got {names:?}"
|
|
);
|
|
assert!(
|
|
names.contains(&"exposure.scale"),
|
|
"expected root-level exposure.scale, got {names:?}"
|
|
);
|
|
|
|
// --- 2. Bind BY NAME, with no ParamAlias and no index counting. ------------
|
|
let (tx, rx) = mpsc::channel::<(Timestamp, Vec<Scalar>)>();
|
|
let mut h = harness(tx)
|
|
.with("sma_cross.fast.length", 2)
|
|
.with("sma_cross.slow.length", 4)
|
|
.with("exposure.scale", 0.5)
|
|
.bootstrap()
|
|
.expect("named harness binds by the uniform <node>.<param> names and bootstraps");
|
|
|
|
h.run(vec![synthetic_prices()]);
|
|
drop(h);
|
|
|
|
let rows: Vec<(Timestamp, Vec<Scalar>)> = rx.iter().collect();
|
|
println!("\nnamed-bound run — recorded rows: {}", rows.len());
|
|
for (ts, row) in &rows {
|
|
if let Scalar::F64(x) = row[0] {
|
|
println!(" ts={:>14} exposure={:+.4}", ts.0, x);
|
|
}
|
|
}
|
|
assert!(!rows.is_empty(), "named-bound harness must record a populated trace");
|
|
|
|
// --- 3. Determinism (C1): a disjoint second run is bit-identical. ----------
|
|
let (tx2, rx2) = mpsc::channel::<(Timestamp, Vec<Scalar>)>();
|
|
let mut h2 = harness(tx2)
|
|
.with("sma_cross.fast.length", 2)
|
|
.with("sma_cross.slow.length", 4)
|
|
.with("exposure.scale", 0.5)
|
|
.bootstrap()
|
|
.expect("second disjoint bootstrap");
|
|
h2.run(vec![synthetic_prices()]);
|
|
drop(h2);
|
|
let rows2: Vec<(Timestamp, Vec<Scalar>)> = rx2.iter().collect();
|
|
assert_eq!(rows, rows2, "two disjoint named runs must be bit-identical (C1)");
|
|
|
|
println!("\nOK: param-space uniform <node>.<param>, named bind ran, deterministic.");
|
|
}
|