Files
Aura/fieldtests/cycle-0031-node-naming/c0031_3_default_names.rs
T
Brummel 11dfff860c fieldtest: cycle 0031 node-naming — naming cure under-signposted on the by-name flow
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.
2026-06-11 12:18:29 +02:00

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.");
}