e84ad6d0d2
Session, LinComb, and CostSum — the three builders the roster scope doc
excludes — re-shape onto the args seam (refs #271): builder() is now
zero-arg and returns a pending recipe (Session: tz + open; LinComb:
arity; CostSum: n_costs), the full signature forms in make, and
configured(...) is the Rust-path convenience producing the identical
recipe (twin-pinned). Session's period_minutes becomes a real ParamSpec
bound by configured instead of a baked struct field — same behaviour,
now sweepable when left open. Session::new and SessionFrankfurt are
untouched.
All builder(n)-form call sites move mechanically to configured(n):
aura-runner member.rs (wrap_r), aura-composites (vol_stop/cost_graph),
aura-engine blueprint.rs + e2e tests, aura-ingest breakout example,
and the crates' own tests. aura-std drops its unused chrono/chrono-tz
deps (stale since the b39fd63 session move).
The roster is deliberately untouched here: rostering before the op
seam lands would expose unconfigured pending builders to add_node.
135 lines
8.0 KiB
Rust
135 lines
8.0 KiB
Rust
//! End-to-end coverage for the construction op-script (#157, cycle 0088): a
|
|
//! declarative, replayable by-identifier op-script (`replay` over a `Vec<Op>`)
|
|
//! builds a runnable blueprint through validated ops, reusing the engine's
|
|
//! existing gates at two cadences — eager (per-op) and holistic (finalize).
|
|
//!
|
|
//! The in-module tests in `construction.rs` reach into crate internals
|
|
//! (`crate::GraphBuilder`, `crate::CompileError`, `super::replay`). These tests
|
|
//! use only the **exported** surface — `aura_engine::{replay, Op, OpError,
|
|
//! GraphBuilder, Composite, FlatGraph, Scalar, ScalarKind}` plus the injected
|
|
//! `aura_vocabulary::std_vocabulary` — the exact seam the iteration-2 CLI (`aura graph
|
|
//! build`/`introspect`) and the World consume. A refactor that made `Op`,
|
|
//! `OpError`, `replay`, or a needed re-export (`ScalarKind`, `Scalar`)
|
|
//! unreachable across the crate boundary would break that consumer while leaving
|
|
//! every in-crate test green; that is the regression class this file pins, in
|
|
//! addition to the per-property invariants below.
|
|
|
|
use aura_engine::{replay, GraphBuilder, Op, OpError, Scalar, ScalarKind};
|
|
use aura_std::{Sma, Sub};
|
|
use aura_strategy::Bias;
|
|
use aura_vocabulary::std_vocabulary;
|
|
|
|
/// The flat root's open param point: fast.length(i64), slow.length(i64),
|
|
/// bias.scale(f64), in param-space traversal order. The same vector is applied to
|
|
/// both construction paths, so it cancels and the only thing under test is
|
|
/// construction identity. Deterministic — no time, no randomness.
|
|
fn params() -> [Scalar; 3] {
|
|
[Scalar::i64(2), Scalar::i64(4), Scalar::f64(0.5)]
|
|
}
|
|
|
|
/// The SMA-cross signal as a construction op-script: a bound `price` source, a
|
|
/// fast/slow SMA pair fed from it, their spread through `Sub`, a `Bias`, and the
|
|
/// `bias` field exposed at the boundary. The smallest script that exercises every
|
|
/// op kind (`Source`/`Add`/`Feed`/`Connect`/`Expose`) at once.
|
|
fn signal_ops() -> Vec<Op> {
|
|
vec![
|
|
Op::Source { role: "price".into(), kind: ScalarKind::F64 },
|
|
Op::Add { type_id: "SMA".into(), as_name: Some("fast".into()), args: vec![], bind: vec![] },
|
|
Op::Add { type_id: "SMA".into(), as_name: Some("slow".into()), args: vec![], bind: vec![] },
|
|
Op::Add { type_id: "Sub".into(), as_name: None, args: vec![], bind: vec![] },
|
|
Op::Add { type_id: "Bias".into(), as_name: None, args: vec![], bind: vec![] },
|
|
Op::Feed { role: "price".into(), into: vec!["fast.series".into(), "slow.series".into()] },
|
|
Op::Connect { from: "fast.value".into(), to: "sub.lhs".into() },
|
|
Op::Connect { from: "slow.value".into(), to: "sub.rhs".into() },
|
|
Op::Connect { from: "sub.value".into(), to: "bias.signal".into() },
|
|
Op::Expose { from: "bias.bias".into(), as_name: "bias".into() },
|
|
]
|
|
}
|
|
|
|
/// Property (C24 acceptance-1, through the public seam): an op-script replayed via
|
|
/// the exported `replay` + the injected `aura_vocabulary::std_vocabulary` compiles to a
|
|
/// `FlatGraph` whose index-wired topology (`edges`) and lowered bound sources
|
|
/// (`sources`) are **identical** to the same signal hand-wired on the exported
|
|
/// `GraphBuilder` surface and compiled with the same param point. The two
|
|
/// authoring surfaces are one construction semantics (C24) — proven entirely
|
|
/// through `aura_engine`/`aura_std` public exports (the CLI/World seam). The
|
|
/// non-empty `edges` guard keeps the equality from being the vacuous match of two
|
|
/// empty graphs.
|
|
#[test]
|
|
fn replayed_construction_compiles_identically_to_graphbuilder() {
|
|
// (a) op-script replay through the public construction surface.
|
|
let replay_flat = replay("root", signal_ops(), &std_vocabulary, &|_: &str| None)
|
|
.expect("op-script resolves")
|
|
.compile_with_params(¶ms())
|
|
.expect("replay compiles");
|
|
|
|
// (b) the GraphBuilder twin — identical topology, identical param point.
|
|
let mut g = GraphBuilder::new("root");
|
|
let fast = g.add(Sma::builder().named("fast"));
|
|
let slow = g.add(Sma::builder().named("slow"));
|
|
let sub = g.add(Sub::builder());
|
|
let bias = g.add(Bias::builder());
|
|
let price = g.source_role("price", ScalarKind::F64);
|
|
g.feed(price, [fast.input("series"), slow.input("series")]);
|
|
g.connect(fast.output("value"), sub.input("lhs"));
|
|
g.connect(slow.output("value"), sub.input("rhs"));
|
|
g.connect(sub.output("value"), bias.input("signal"));
|
|
g.expose(bias.output("bias"), "bias");
|
|
let builder_flat = g
|
|
.build()
|
|
.expect("root resolves")
|
|
.compile_with_params(¶ms())
|
|
.expect("twin compiles");
|
|
|
|
assert!(!replay_flat.edges.is_empty(), "the signal must wire a non-trivial topology");
|
|
assert_eq!(replay_flat.edges, builder_flat.edges, "construction topology diverged from the builder twin");
|
|
assert_eq!(replay_flat.sources, builder_flat.sources, "lowered bound sources diverged from the builder twin");
|
|
}
|
|
|
|
/// Property (eager fail-fast cadence, at the public boundary): an eagerly-decidable
|
|
/// fault — here an `Add` of a `type_id` outside the closed vocabulary — stops
|
|
/// `replay` AT the offending op and is reported by `(op_index, OpError)`, the
|
|
/// op_index being the position of the bad op (2 here), not the end of the script.
|
|
/// The earlier valid ops do not mask it and the later ops never run. This is the
|
|
/// by-index error attribution the CLI/World relies on to point an author at the
|
|
/// exact failing op, asserted through the exported `replay`/`Op`/`OpError`.
|
|
#[test]
|
|
fn replay_attributes_eager_fault_to_its_op_index() {
|
|
let ops = vec![
|
|
Op::Add { type_id: "SMA".into(), as_name: Some("fast".into()), args: vec![], bind: vec![] }, // 0 ok
|
|
Op::Add { type_id: "Sub".into(), as_name: None, args: vec![], bind: vec![] }, // 1 ok
|
|
Op::Add { type_id: "Nope".into(), as_name: None, args: vec![], bind: vec![] }, // 2 fails
|
|
Op::Add { type_id: "Bias".into(), as_name: None, args: vec![], bind: vec![] }, // 3 never runs
|
|
];
|
|
// `.err().unwrap()` (not `unwrap_err`): the Ok arm `Composite` holds build
|
|
// closures and is not `Debug`, which `unwrap_err`'s bound would require.
|
|
let err = replay("g", ops, &std_vocabulary, &|_: &str| None).err().unwrap();
|
|
assert_eq!(err, (2, OpError::UnknownNodeType("Nope".into())));
|
|
}
|
|
|
|
/// Property (holistic finalize cadence, at the public boundary): a fault that is
|
|
/// only decidable at end-of-document — input-slot totality (the 0-cover arm of
|
|
/// exactly-once wiring) — passes every per-op check and is caught by the implicit
|
|
/// finalize step, attributed to index `ops.len()` (6 here), strictly past any real
|
|
/// op index. Every op below applies cleanly; `sub.rhs` is left unwired, so only the
|
|
/// holistic `validate_wiring` gate at finalize can reject it, as the by-identifier
|
|
/// `OpError::UnconnectedPort { .. }`. Together with the
|
|
/// eager-cadence test, this pins the iteration's eager/holistic gate split as
|
|
/// observable error attribution across the crate boundary.
|
|
#[test]
|
|
fn replay_attributes_holistic_fault_to_finalize_step() {
|
|
let ops = vec![
|
|
Op::Source { role: "price".into(), kind: ScalarKind::F64 }, // 0
|
|
Op::Add { type_id: "SMA".into(), as_name: Some("fast".into()), args: vec![], bind: vec![] }, // 1
|
|
Op::Add { type_id: "SMA".into(), as_name: Some("slow".into()), args: vec![], bind: vec![] }, // 2
|
|
Op::Add { type_id: "Sub".into(), as_name: None, args: vec![], bind: vec![] }, // 3
|
|
Op::Feed { role: "price".into(), into: vec!["fast.series".into(), "slow.series".into()] }, // 4
|
|
Op::Connect { from: "fast.value".into(), to: "sub.lhs".into() }, // 5
|
|
// sub.rhs deliberately left unwired -> only finalize can decide totality.
|
|
];
|
|
let finalize_index = ops.len(); // 6 — the implicit finalize step
|
|
let err = replay("g", ops, &std_vocabulary, &|_: &str| None).err().unwrap();
|
|
assert!(matches!(&err, (i, OpError::UnconnectedPort { .. }) if *i == finalize_index),
|
|
"the holistic fault is still attributed to the finalize step, got {err:?}");
|
|
}
|