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.
131 lines
5.7 KiB
Rust
131 lines
5.7 KiB
Rust
// 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 `<lowercased-type>.<param>` — `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<Scalar>)>) -> 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<Scalar>)>();
|
|
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.");
|
|
}
|