Files
Brummel 03285d1b47 fieldtest: milestone the-world param-space & sweep — green, delivers promise
Milestone-scope fieldtest (closing functional gate) for "The World —
parameter-space & sweep" (issues #30-#36). Three curated end-to-end scenarios
derived top-down from the milestone promise, driven from the public interface
only (ledger + specs + rustdoc; no crates/*/src read), built and run from HEAD
via a downstream consumer crate.

Delivery verdict: GREEN. The milestone delivers its promise.
- mw_1 single run bound by name: .with("sma_cross.fast", 2).bootstrap() binds
  and runs (12 equity rows); diagnostics precise and knob-named (UnknownKnob /
  AmbiguousKnob / KindMismatch).
- mw_2 named-axis sweep + compare: .axis(..).sweep(run) yields a 4-member,
  ordered (odometer), disjoint, non-constant SweepFamily; ranking from public
  fields. CLI (aura sweep / aura runs rank) delivers the same with no Rust.
- mw_3 structural-constant negative space (deliberate): SMA2-entry's
  definitional `2` has no honest expression today -> grounds #55. Out of
  milestone scope; not a blocker.

Findings: 0 bugs, 2 working, 3 friction, 1 spec_gap. Verified independently
(re-ran all three from HEAD; confirmed a plain SMA-cross emits sma_cross.length
twice and the shipped CLI sample carries the fast/slow ParamAlias overlays).

Friction (routed to the forward queue, none blocks the gate):
- the promised sma_cross.fast/.slow names are not free — they require
  author-added ParamAlias overlays, doubly mandatory because the canonical
  2-SMA cross also will not bootstrap without them (IndistinguishableFanIn);
- the sweep closure drops back to positional bind + manual name re-zip.

spec_gap -> #55: the wished-for structural-constant consumer code is recorded
in mw_3 as #55's acceptance evidence.

This commit is the milestone fieldtest itself; it closes no issue. Closing the
tracker milestone stays a deliberate manual act on this green gate.
2026-06-11 09:11:37 +02:00

240 lines
9.2 KiB
Rust

// Milestone fieldtest — scenario 1: a single concrete run, bound BY NAME.
//
// Promise probed: an author declares a strategy's tunable knobs in its schema
// (typed), inspects them via `param_space()`, and binds one concrete runnable
// instance by NAME — `.with("sma_cross.fast", 2).with("scale", 0.5).bootstrap()`.
//
// Public interface only: discovered from the ledger + specs 0015/0030 + rustdoc.
// No crates/*/src was read.
//
// The harness: source -> sma_cross composite { Sma, Sma, Sub } -> Exposure
// -> SimBroker -> Recorder. The two SMA lengths live INSIDE the
// `sma_cross` composite (so param_space() path-qualifies them as
// `sma_cross.fast` / `sma_cross.slow`), `scale` is a root-level Exposure knob
// (bare). The 0024 root model: the root graph IS a Composite all of whose roles
// are source-bound.
use std::sync::mpsc;
use aura_core::{Firing, Scalar, ScalarKind, Timestamp};
use aura_engine::{
BindError, BlueprintNode, Composite, Edge, OutField, ParamAlias, Role, Target,
};
use aura_std::{Exposure, Recorder, SimBroker, Sma, Sub};
/// Build the reusable inner 2/4-SMA-cross composite (value-empty recipes; the
/// two lengths are open knobs bound at bootstrap).
fn sma_cross() -> Composite {
Composite::new(
"sma_cross",
vec![
Sma::builder().into(),
Sma::builder().into(),
Sub::builder().into(),
],
// interior edges: fast -> Sub.slot0, slow -> Sub.slot1
vec![
Edge { from: 0, to: 2, slot: 0, from_field: 0 },
Edge { from: 1, to: 2, slot: 1, from_field: 0 },
],
// one OPEN input role (price) fanning into both SMAs' slot 0.
// source: None => open interior port, wired by the enclosing graph.
vec![Role {
name: "price".into(),
targets: vec![
Target { node: 0, slot: 0 },
Target { node: 1, slot: 0 },
],
// source: None => an OPEN interior port, wired by the enclosing graph
// (0024 root model: Role.source distinguishes open vs source-bound).
source: None,
}],
vec![], // no param aliases
vec![OutField { node: 2, field: 0, name: "out".into() }],
)
}
/// The SAME composite, but with C23 ParamAlias overlays so the two interior SMA
/// knobs surface as the DISTINCT names `fast` / `slow` the milestone promise
/// example assumes. This is the work the author must do by hand to make
/// `sma_cross.fast` real.
fn sma_cross_aliased() -> Composite {
Composite::new(
"sma_cross",
vec![
Sma::builder().into(),
Sma::builder().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,
}],
// ALIASES: rename Sma#0's length slot -> "fast", Sma#1's -> "slow".
vec![
ParamAlias { name: "fast".into(), node: 0, slot: 0 },
ParamAlias { name: "slow".into(), node: 1, slot: 0 },
],
vec![OutField { node: 2, field: 0, name: "out".into() }],
)
}
/// Harness over an arbitrary inner cross composite (plain or aliased).
fn harness_with(
inner: Composite,
tx: mpsc::Sender<(Timestamp, Vec<Scalar>)>,
) -> Composite {
Composite::new(
"harness",
vec![
BlueprintNode::Composite(inner),
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![],
vec![],
)
}
/// The root harness over the PLAIN (un-aliased) cross — its two SMA knobs both
/// surface as `sma_cross.length`.
fn harness(tx: mpsc::Sender<(Timestamp, Vec<Scalar>)>) -> Composite {
harness_with(sma_cross(), tx)
}
/// The root harness over the ALIASED cross — knobs surface as
/// `sma_cross.fast` / `sma_cross.slow`.
fn harness_aliased(tx: mpsc::Sender<(Timestamp, Vec<Scalar>)>) -> Composite {
harness_with(sma_cross_aliased(), tx)
}
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: does it tell me the knob names? ----------
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);
}
// --- 2a. FIRST ATTEMPT: bind by the milestone-promise names. --------------
// The carrier / spec 0030 worked example says these are `sma_cross.fast` /
// `sma_cross.slow`. Try it verbatim and record the diagnostic.
let (txp, _rxp) = mpsc::channel();
let promise_form: Result<_, BindError> = harness(txp)
.with("sma_cross.fast", 2)
.with("sma_cross.slow", 4)
.with("scale", 0.5)
.bootstrap();
println!("\n[2a] promise-form (sma_cross.fast/.slow) result: {:?}", promise_form.err());
// --- 2b. SECOND ATTEMPT: bind by the names param_space() ACTUALLY emits. ---
// Both interior SMA knobs surface as the IDENTICAL `sma_cross.length`.
let (txq, _rxq) = mpsc::channel();
let real_name_form: Result<_, BindError> = harness(txq)
.with("sma_cross.length", 2)
.with("scale", 0.5)
.bootstrap();
println!("[2b] real-emitted-name (sma_cross.length) result: {:?}", real_name_form.err());
// --- 2c. The FIX: add C23 ParamAlias overlays so fast/slow become real. ---
// With the aliases, param_space() emits the DISTINCT names the promise wants.
let (txal0, _rxal0) = mpsc::channel();
let aliased_space = harness_aliased(txal0).param_space();
println!("\n[2c] param_space() AFTER aliasing:");
for p in &aliased_space {
println!(" {} : {:?}", p.name, p.kind);
}
// Now the milestone-promise named bind works AND runs.
let (tx, rx) = mpsc::channel::<(Timestamp, Vec<Scalar>)>();
let mut h = harness_aliased(tx)
.with("sma_cross.fast", 2)
.with("sma_cross.slow", 4)
.with("scale", 0.5)
.bootstrap()
.expect("aliased harness binds by the promised names");
h.run(vec![synthetic_prices()]);
drop(h);
let rows: Vec<(Timestamp, Vec<Scalar>)> = rx.iter().collect();
println!("\n[2c] aliased named-bound run — recorded equity rows: {}", rows.len());
for (ts, row) in &rows {
if let Scalar::F64(x) = row[0] {
println!(" ts={:>14} equity_pips={:.4}", ts.0, x);
}
}
assert!(!rows.is_empty(), "named-bound harness must record a populated trace");
// --- 3. Probe diagnostics: a misspelled knob name (against aliased names). -
let (txa, _rxa) = mpsc::channel();
let wrong_name: Result<_, BindError> = harness_aliased(txa)
.with("sma_cross.fast", 2)
.with("sma_cross.slo", 4) // typo: "slo" not "slow"
.with("scale", 0.5)
.bootstrap();
println!("\n[3] wrong-name (sma_cross.slo) result: {:?}", wrong_name.err());
// --- 4. Probe diagnostics: the SHORT bare name (spec says UnknownKnob). ----
let (txb, _rxb) = mpsc::channel();
let bare: Result<_, BindError> = harness_aliased(txb)
.with("fast", 2) // unqualified — spec: UnknownKnob
.with("sma_cross.slow", 4)
.with("scale", 0.5)
.bootstrap();
println!("[4] bare-name (\"fast\") result: {:?}", bare.err());
// --- 5. Probe diagnostics: a wrong-KIND value. ----------------------------
let (txc, _rxc) = mpsc::channel();
let wrong_kind: Result<_, BindError> = harness_aliased(txc)
.with("sma_cross.fast", 2)
.with("sma_cross.slow", 4)
.with("scale", 1) // I64 literal into an F64 slot -> KindMismatch
.bootstrap();
println!("[5] wrong-kind (scale=1 into F64) result: {:?}", wrong_kind.err());
// --- 6. Probe: binding the AMBIGUOUS un-aliased name -> AmbiguousKnob. -----
let (txd, _rxd) = mpsc::channel();
let ambiguous: Result<_, BindError> = harness(txd) // PLAIN harness, both = sma_cross.length
.with("sma_cross.length", 2)
.with("scale", 0.5)
.bootstrap();
println!("[6] ambiguous (plain sma_cross.length) result: {:?}", ambiguous.err());
println!("\nOK: param-space inspected, named bind ran (aliased), diagnostics probed.");
}