03285d1b47
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.
240 lines
9.2 KiB
Rust
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.");
|
|
}
|