feat(aura-engine, aura-vocabulary, aura-cli): the args channel end to end
The data plane reaches non-scalar structural config (closes #271, the final Data-authorability-boundary item). Op::Add gains args (string pairs, applied via try_args after resolve and before the bind loop — no pending builder ever enters a graph; an arg-bearing type without args refuses as MissingArg). OpError::BadArg carries the precise ArgOpError; the CLI renders it as prose naming the arg, its kind, and the kind's closed hint (exit 1, #175 content-fault class). Serde: PrimitiveData.args (skip-if-empty — args-free documents stay byte-identical, golden-pinned, so every existing content id is stable per C18) with a data-driven format version: the writer emits 1 for args-free documents and 2 when any primitive recursively carries args; the loader accepts 1..=2. The old v2-refusal pin flips deliberately to v3 (named contract change). Identity JSON keeps args — they are structural, and the Rust configured() path and the args op-script bridge to the same identity id (pinned). Roster: Session / LinComb / CostSum enter (33 -> 36, both count pins; the LinComb-rejection assertion flips to the resolved side). introspect --node shows arg rows (name, kind, hint) plus the pending note; OP_REFERENCE's add row teaches the args form. Registry comment de-staled (LinComb is now roster-reachable). Sequencing note: this commit lands op seam and roster together so no intermediate tree state exposes a pending builder.
This commit is contained in:
@@ -7,18 +7,29 @@
|
||||
//! closure and the declared schema are re-derived on load from the injected
|
||||
//! resolver. This is the inverse of the lossy render half (`model_to_json`); it
|
||||
//! carries no node logic (C17) and references a closed vocabulary (C24).
|
||||
//!
|
||||
//! Construction args (#271) are structural, id-bearing data — like `bound`
|
||||
//! values, unlike debug-symbol names — so `strip_debug_symbols` leaves
|
||||
//! `PrimitiveData.args` untouched.
|
||||
|
||||
use crate::blueprint::{BlueprintNode, Composite, Gang, OutField, Role};
|
||||
use crate::harness::Edge;
|
||||
use aura_core::{BoundParam, PrimitiveBuilder};
|
||||
use aura_core::{BoundParam, ConstructionArg, PrimitiveBuilder};
|
||||
|
||||
/// The format version the loader understands. Bumped only by a load-bearing
|
||||
/// (Tier-2) change; additive optional fields do not bump it (#156). Pre-ship
|
||||
/// dormancy (#61, 2026-07-10): while the project is unshipped every document
|
||||
/// lives in-repo and reader/writer change atomically, so the Tier-2 bump
|
||||
/// discipline activates at the first external ship — which consciously
|
||||
/// freezes v1 (gangs included).
|
||||
pub const BLUEPRINT_FORMAT_VERSION: u32 = 1;
|
||||
/// The CEILING format version this build's loader understands (and the
|
||||
/// writer may ever emit) — bumped only by a load-bearing (Tier-2) change;
|
||||
/// additive optional fields do not bump it (#156). Pre-ship dormancy (#61,
|
||||
/// 2026-07-10): while the project is unshipped every document lives in-repo
|
||||
/// and reader/writer change atomically, so the Tier-2 bump discipline
|
||||
/// activates at the first external ship.
|
||||
///
|
||||
/// #271 (data-driven version): the writer no longer emits a single fixed
|
||||
/// version. A document emits `1` when args-free (byte-identical to every
|
||||
/// pre-#271 document — content ids stable, C18) and `2` the moment any
|
||||
/// primitive, at any nesting depth, carries construction `args` — the
|
||||
/// must-understand signal a pre-#271 loader needs. The loader accepts the
|
||||
/// closed range `1..=BLUEPRINT_FORMAT_VERSION`.
|
||||
pub const BLUEPRINT_FORMAT_VERSION: u32 = 2;
|
||||
|
||||
/// Top-level envelope: the version is read before the payload is interpreted.
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
@@ -61,17 +72,33 @@ pub enum NodeData {
|
||||
}
|
||||
|
||||
/// A primitive node as data: its compiled-in type identity, optional instance
|
||||
/// name, and bound params. The schema + build closure are re-derived on load.
|
||||
/// name, its accepted construction args (#271), and bound params. The schema
|
||||
/// + build closure are re-derived on load.
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
pub struct PrimitiveData {
|
||||
#[serde(rename = "type")]
|
||||
pub type_id: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub name: Option<String>,
|
||||
/// The consumed `(name, value)` construction-arg pairs (#271) — the
|
||||
/// id-bearing, serialized twin of `bound`. Additive-optional: absent
|
||||
/// (empty) for every args-free primitive, so a document with no args
|
||||
/// anywhere stays byte-identical to its pre-#271 form.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub args: Vec<ArgData>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub bound: Vec<BoundParam>,
|
||||
}
|
||||
|
||||
/// One serialized construction-arg pair (#271) — the wire twin of
|
||||
/// [`aura_core::ConstructionArg`]: `value` is the accepted strict-form
|
||||
/// string, stored verbatim (never re-normalized).
|
||||
#[derive(Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct ArgData {
|
||||
pub name: String,
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
/// Serializer failure (typed, named).
|
||||
#[derive(Debug)]
|
||||
pub enum SerializeError {
|
||||
@@ -111,9 +138,19 @@ fn project_node(n: &BlueprintNode) -> NodeData {
|
||||
// construction once a multi-param node enters the vocabulary.
|
||||
let mut bound = b.bound_params().to_vec();
|
||||
bound.sort_by_key(|bp| bp.pos);
|
||||
// Construction args (#271) are declared in a fixed `ArgSpec` order
|
||||
// (not player-chosen like binds), so no re-canonicalization is
|
||||
// needed — `construction_args()` already returns them in that
|
||||
// deterministic order.
|
||||
let args: Vec<ArgData> = b
|
||||
.construction_args()
|
||||
.iter()
|
||||
.map(|ConstructionArg { name, value }| ArgData { name: name.clone(), value: value.clone() })
|
||||
.collect();
|
||||
NodeData::Primitive(PrimitiveData {
|
||||
type_id: b.label(),
|
||||
name: b.instance_name().map(str::to_string),
|
||||
args,
|
||||
bound,
|
||||
})
|
||||
}
|
||||
@@ -121,8 +158,32 @@ fn project_node(n: &BlueprintNode) -> NodeData {
|
||||
}
|
||||
}
|
||||
|
||||
/// Any primitive, at any nesting depth, carries construction args (#271) —
|
||||
/// the must-understand signal for the data-driven version (spec §Data-driven
|
||||
/// format version).
|
||||
fn has_args(b: &CompositeData) -> bool {
|
||||
b.nodes.iter().any(|n| match n {
|
||||
NodeData::Primitive(p) => !p.args.is_empty(),
|
||||
NodeData::Composite(c) => has_args(c),
|
||||
})
|
||||
}
|
||||
|
||||
/// The version THIS document must declare (#271): `1` for an args-free
|
||||
/// document (byte-identical to every pre-#271 document, C18) or `2` the
|
||||
/// moment any primitive anywhere carries args — never the fixed
|
||||
/// `BLUEPRINT_FORMAT_VERSION` ceiling.
|
||||
fn document_version(b: &CompositeData) -> u32 {
|
||||
if has_args(b) {
|
||||
2
|
||||
} else {
|
||||
1
|
||||
}
|
||||
}
|
||||
|
||||
fn build_doc(c: &Composite) -> BlueprintDoc {
|
||||
BlueprintDoc { format_version: BLUEPRINT_FORMAT_VERSION, blueprint: project(c) }
|
||||
let blueprint = project(c);
|
||||
let format_version = document_version(&blueprint);
|
||||
BlueprintDoc { format_version, blueprint }
|
||||
}
|
||||
|
||||
fn serialize_doc(doc: &BlueprintDoc) -> Result<String, SerializeError> {
|
||||
@@ -200,11 +261,16 @@ pub enum LoadError {
|
||||
/// addition needs finer granularity than a version bump.
|
||||
UnsupportedVersion { found: u32, supported: u32 },
|
||||
/// `resolve` returned `None` for a serialized `type_id` (outside the injected
|
||||
/// vocabulary — unknown node, or a construction-arg/sink node not in #155's set).
|
||||
/// vocabulary — unknown node, or a sink node not in #155's set).
|
||||
UnknownNodeType(String),
|
||||
/// The document's gangs section fails structural validation against its
|
||||
/// own nodes (a hand-edited or corrupted document).
|
||||
Gang(crate::blueprint::CompileError),
|
||||
/// A primitive's `args` (#271) failed `try_args`: unknown/duplicate/
|
||||
/// missing arg, a malformed value, OR (the pending-with-no-args refusal)
|
||||
/// an arg-bearing type serialized/hand-written with an empty `args` —
|
||||
/// a document naming an arg-bearing type without args must not load.
|
||||
BadArg(aura_core::ArgOpError),
|
||||
}
|
||||
|
||||
fn reconstruct(
|
||||
@@ -220,6 +286,15 @@ fn reconstruct(
|
||||
if let Some(n) = &p.name {
|
||||
b = b.named(n);
|
||||
}
|
||||
// Apply construction args (#271) BEFORE bound params — the
|
||||
// `try_args` seam. An arg-bearing type with no `args` in the
|
||||
// document refuses HERE as `MissingArg` (no pending builder
|
||||
// ever reaches `bind`/enters the built graph); a `Plain` type
|
||||
// has empty `p.args`, so this is `Ok(self)` unchanged for
|
||||
// every pre-#271 document.
|
||||
let args: Vec<(String, String)> =
|
||||
p.args.iter().map(|a| (a.name.clone(), a.value.clone())).collect();
|
||||
b = b.try_args(&args).map_err(LoadError::BadArg)?;
|
||||
// Re-apply bound params BY NAME. The effective param vector is
|
||||
// order-independent (each `bind` computes its slot against the
|
||||
// shrunk schema); ascending original `pos` is the canonical order.
|
||||
@@ -251,7 +326,9 @@ pub fn blueprint_from_json(
|
||||
resolve: &dyn Fn(&str) -> Option<PrimitiveBuilder>,
|
||||
) -> Result<Composite, LoadError> {
|
||||
let doc: BlueprintDoc = serde_json::from_str(data).map_err(LoadError::Json)?;
|
||||
if doc.format_version != BLUEPRINT_FORMAT_VERSION {
|
||||
// #271: the loader accepts the closed range 1..=BLUEPRINT_FORMAT_VERSION
|
||||
// (today 1..=2) — a data-driven ceiling, not a single fixed version.
|
||||
if !(1..=BLUEPRINT_FORMAT_VERSION).contains(&doc.format_version) {
|
||||
return Err(LoadError::UnsupportedVersion {
|
||||
found: doc.format_version,
|
||||
supported: BLUEPRINT_FORMAT_VERSION,
|
||||
@@ -266,7 +343,8 @@ mod tests {
|
||||
use crate::blueprint::{Composite, GangMember, OutField, Role};
|
||||
use crate::harness::{Edge, Target};
|
||||
use crate::VecSource; // crate-root re-export (blueprint.rs tests import it the same way, e.g. :923)
|
||||
use aura_core::{Scalar, ScalarKind, Timestamp};
|
||||
use aura_core::{ArgOpError, Scalar, ScalarKind, Timestamp};
|
||||
use aura_market::Session;
|
||||
use aura_std::{Recorder, Sma, Sub};
|
||||
use aura_strategy::Bias;
|
||||
use std::sync::mpsc;
|
||||
@@ -436,11 +514,14 @@ mod tests {
|
||||
assert!(matches!(err, LoadError::UnknownNodeType(t) if t == "NoSuchNode"));
|
||||
}
|
||||
|
||||
/// #271 flipped pin (named contract change, not a regression): `format_version: 2`
|
||||
/// used to be refused (v1 was the only understood version); now that v2 loads
|
||||
/// (the args-bearing tier), only a version PAST the new ceiling is unsupported.
|
||||
#[test]
|
||||
fn unsupported_version_fails_named() {
|
||||
let json = r#"{"format_version":2,"blueprint":{"name":"x","nodes":[]}}"#;
|
||||
let json = r#"{"format_version":3,"blueprint":{"name":"x","nodes":[]}}"#;
|
||||
let err = blueprint_from_json(json, &|t| aura_vocabulary::std_vocabulary(t)).err().unwrap();
|
||||
assert!(matches!(err, LoadError::UnsupportedVersion { found: 2, supported: 1 }));
|
||||
assert!(matches!(err, LoadError::UnsupportedVersion { found: 3, supported: 2 }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -813,4 +894,127 @@ mod tests {
|
||||
"the gang itself is identity-bearing"
|
||||
);
|
||||
}
|
||||
|
||||
// ---- construction args (#271) --------------------------------------
|
||||
|
||||
// A single-node composite around an arg-configured `Session` (arity-free —
|
||||
// a `trigger` input role feeds it, its `bars_since_open` output re-exports).
|
||||
fn session_arg_fixture(tz: chrono_tz::Tz) -> Composite {
|
||||
Composite::new(
|
||||
"sess",
|
||||
vec![Session::configured(9, 30, tz, 15).into()],
|
||||
vec![],
|
||||
vec![Role {
|
||||
name: "trigger".into(),
|
||||
targets: vec![Target { node: 0, slot: 0 }],
|
||||
source: Some(ScalarKind::F64),
|
||||
}],
|
||||
vec![OutField { node: 0, field: 0, name: "bars".into() }],
|
||||
)
|
||||
}
|
||||
|
||||
/// #271 acceptance (engine layer): an args-bearing composite serializes its
|
||||
/// construction-arg pairs, declares `format_version: 2` (the data-driven
|
||||
/// must-understand tier, spec §Data-driven format version), and round-trips
|
||||
/// byte-stably through load -> re-serialize.
|
||||
#[test]
|
||||
fn args_round_trip_preserves_pairs_and_version_2() {
|
||||
let c = session_arg_fixture(chrono_tz::Europe::Berlin);
|
||||
let json = blueprint_to_json(&c).expect("serializes");
|
||||
assert!(json.contains(r#""format_version":2"#), "args-bearing document is version 2: {json}");
|
||||
assert!(
|
||||
json.contains(r#""args":[{"name":"tz","value":"Europe/Berlin"},{"name":"open","value":"09:30"}]"#),
|
||||
"carries the verbatim accepted arg pairs: {json}"
|
||||
);
|
||||
let loaded = blueprint_from_json(&json, &fixture_resolver()).expect("loads");
|
||||
assert_eq!(
|
||||
blueprint_to_json(&loaded).expect("re-serializes"),
|
||||
json,
|
||||
"args round-trip byte-stably"
|
||||
);
|
||||
}
|
||||
|
||||
/// #271 acceptance (C18 stability): a document with NO args anywhere stays
|
||||
/// version 1 and byte-identical to its pre-#271 canonical form — the exact
|
||||
/// golden `signal_serializes_to_canonical_golden` already pins, re-asserted
|
||||
/// here under the #271-specific test name the plan names.
|
||||
#[test]
|
||||
fn args_free_document_serializes_byte_identical_and_version_1() {
|
||||
let signal = crate::test_fixtures::sink_free_sma_cross_signal();
|
||||
let json = blueprint_to_json(&signal).expect("serializes");
|
||||
assert!(json.starts_with(r#"{"format_version":1,"#), "args-free document stays version 1: {json}");
|
||||
assert!(!json.contains("\"args\""), "an args-free document emits no args key: {json}");
|
||||
let golden = r#"{"format_version":1,"blueprint":{"name":"sma_cross","nodes":[{"primitive":{"type":"SMA","name":"fast","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":2}}]}},{"primitive":{"type":"SMA","name":"slow"}},{"primitive":{"type":"Sub"}},{"primitive":{"type":"Bias"}}],"edges":[{"from":0,"to":2,"slot":0,"from_field":0},{"from":1,"to":2,"slot":1,"from_field":0},{"from":2,"to":3,"slot":0,"from_field":0}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0},{"node":1,"slot":0}]}],"output":[{"node":3,"field":0,"name":"bias"}]}}"#;
|
||||
assert_eq!(json, golden, "byte-identical to the pre-#271 canonical form (content ids stable, C18)");
|
||||
}
|
||||
|
||||
/// #271 (nested propagation): an args-bearing primitive nested INSIDE an
|
||||
/// outer composite still trips the outer document's version to 2 — the
|
||||
/// version walk recurses through `NodeData::Composite`, not just the
|
||||
/// top-level node list.
|
||||
#[test]
|
||||
fn nested_spliced_args_composite_is_version_2() {
|
||||
let inner = session_arg_fixture(chrono_tz::Europe::Berlin);
|
||||
let outer = Composite::new(
|
||||
"outer",
|
||||
vec![crate::blueprint::BlueprintNode::Composite(inner)],
|
||||
vec![],
|
||||
vec![Role {
|
||||
name: "trigger".into(),
|
||||
targets: vec![Target { node: 0, slot: 0 }],
|
||||
source: Some(ScalarKind::F64),
|
||||
}],
|
||||
vec![OutField { node: 0, field: 0, name: "bars".into() }],
|
||||
);
|
||||
let json = blueprint_to_json(&outer).expect("serializes");
|
||||
assert!(json.contains(r#""format_version":2"#), "a nested arg-bearing primitive still trips version 2: {json}");
|
||||
assert!(json.contains("\"args\""), "the nested primitive's args survive: {json}");
|
||||
}
|
||||
|
||||
/// #271 (identity is arg-bearing): construction args are structural,
|
||||
/// id-bearing data — like a bound value, unlike a debug-symbol name — so
|
||||
/// two builds differing ONLY in one accepted arg value never share an
|
||||
/// identity JSON, while renaming the composite (a pure debug symbol)
|
||||
/// still does not affect it.
|
||||
#[test]
|
||||
fn identity_json_retains_args() {
|
||||
let berlin = session_arg_fixture(chrono_tz::Europe::Berlin);
|
||||
let paris = session_arg_fixture(chrono_tz::Europe::Paris);
|
||||
assert_ne!(
|
||||
blueprint_identity_json(&berlin).expect("identity-serializes"),
|
||||
blueprint_identity_json(&paris).expect("identity-serializes"),
|
||||
"a differing construction-arg value survives the identity projection"
|
||||
);
|
||||
// renaming the composite (a debug symbol) does not affect identity.
|
||||
let renamed = Composite::new(
|
||||
"sess_renamed",
|
||||
vec![Session::configured(9, 30, chrono_tz::Europe::Berlin, 15).into()],
|
||||
vec![],
|
||||
vec![Role {
|
||||
name: "trigger".into(),
|
||||
targets: vec![Target { node: 0, slot: 0 }],
|
||||
source: Some(ScalarKind::F64),
|
||||
}],
|
||||
vec![OutField { node: 0, field: 0, name: "bars".into() }],
|
||||
);
|
||||
assert_eq!(
|
||||
blueprint_identity_json(&berlin).expect("identity-serializes"),
|
||||
blueprint_identity_json(&renamed).expect("identity-serializes"),
|
||||
"the composite's own debug name does not affect identity"
|
||||
);
|
||||
}
|
||||
|
||||
/// #271 (refuse, don't guess): a hand-written document naming an
|
||||
/// arg-bearing type (`Session`) with NO `args` key refuses on load as
|
||||
/// `LoadError::BadArg(ArgOpError::MissingArg(..))` — a pending builder
|
||||
/// must never silently enter the built graph.
|
||||
#[test]
|
||||
fn loading_arg_bearing_type_without_args_refuses() {
|
||||
let json = r#"{"format_version":1,"blueprint":{"name":"x","nodes":[{"primitive":{"type":"Session"}}]}}"#;
|
||||
let err = blueprint_from_json(json, &fixture_resolver()).err().unwrap();
|
||||
assert!(
|
||||
matches!(&err, LoadError::BadArg(ArgOpError::MissingArg(a)) if a == "tz"),
|
||||
"an arg-bearing type with no args refuses as MissingArg, got {err:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use aura_core::{BindOpError, NodeSchema, PrimitiveBuilder, Scalar, ScalarKind};
|
||||
use aura_core::{ArgOpError, BindOpError, NodeSchema, PrimitiveBuilder, Scalar, ScalarKind};
|
||||
|
||||
use crate::blueprint::{
|
||||
check_param_namespace_injective, edge_kind_check, validate_wiring, BlueprintNode, Composite,
|
||||
@@ -32,8 +32,14 @@ pub enum Op {
|
||||
Source { role: String, kind: ScalarKind },
|
||||
/// Reserve an open input role (wired by an enclosing graph).
|
||||
Input { role: String },
|
||||
/// Add a node of `type_id`, optionally named `as_name`, with `bind` params.
|
||||
Add { type_id: String, as_name: Option<String>, bind: Vec<(String, Scalar)> },
|
||||
/// Add a node of `type_id`, optionally named `as_name`, with construction
|
||||
/// `args` (the typed, closed channel — #271) applied BEFORE `bind` params.
|
||||
Add {
|
||||
type_id: String,
|
||||
as_name: Option<String>,
|
||||
args: Vec<(String, String)>,
|
||||
bind: Vec<(String, Scalar)>,
|
||||
},
|
||||
/// Fan a role into one or more `"node.slot"` consumers.
|
||||
Feed { role: String, into: Vec<String> },
|
||||
/// Wire `"node.field"` -> `"node.slot"`.
|
||||
@@ -91,6 +97,11 @@ pub enum OpError {
|
||||
/// blueprint non-acyclic (domain invariant 5 / ledger C9).
|
||||
WouldCycle { from: String, to: String },
|
||||
BadParam { node: String, err: BindOpError },
|
||||
/// A construction-arg fault (#271): an arg-bearing type's `args` failed
|
||||
/// `try_args` (unknown/duplicate/missing arg, or a malformed value) — or a
|
||||
/// `Plain` type was given args it does not declare. By-identifier, naming
|
||||
/// the offending node.
|
||||
BadArg { node: String, err: ArgOpError },
|
||||
/// Holistic totality fault, by-identifier: an interior input slot covered by
|
||||
/// no edge or role target (the finalize 0-cover arm).
|
||||
UnconnectedPort { node: String, slot: String },
|
||||
@@ -180,7 +191,7 @@ impl<'v> GraphSession<'v> {
|
||||
match op {
|
||||
Op::Source { role, kind } => self.add_role(role, Some(kind)),
|
||||
Op::Input { role } => self.add_role(role, None),
|
||||
Op::Add { type_id, as_name, bind } => self.add_node(type_id, as_name, bind),
|
||||
Op::Add { type_id, as_name, args, bind } => self.add_node(type_id, as_name, args, bind),
|
||||
Op::Feed { role, into } => self.feed(role, into),
|
||||
Op::Connect { from, to } => self.connect(from, to),
|
||||
Op::Expose { from, as_name } => self.expose(from, as_name),
|
||||
@@ -248,11 +259,17 @@ impl<'v> GraphSession<'v> {
|
||||
&mut self,
|
||||
type_id: String,
|
||||
as_name: Option<String>,
|
||||
args: Vec<(String, String)>,
|
||||
bind: Vec<(String, Scalar)>,
|
||||
) -> Result<(), OpError> {
|
||||
let mut builder =
|
||||
(self.vocab)(&type_id).ok_or_else(|| OpError::UnknownNodeType(type_id.clone()))?;
|
||||
let id = as_name.unwrap_or_else(|| builder.node_name());
|
||||
// Construction args apply BEFORE the bind loop (spec §aura-engine): a
|
||||
// pending arg-bearing builder with no args therefore refuses here as
|
||||
// `MissingArg` — no pending builder ever reaches `try_bind`/enters the
|
||||
// graph.
|
||||
builder = builder.try_args(&args).map_err(|err| OpError::BadArg { node: id.clone(), err })?;
|
||||
for (slot, value) in bind {
|
||||
builder = builder
|
||||
.try_bind(&slot, value)
|
||||
@@ -564,7 +581,7 @@ pub fn replay(
|
||||
mod tests {
|
||||
use super::{GraphSession, Op, OpError};
|
||||
use crate::blueprint::{BlueprintNode, Composite, Role};
|
||||
use aura_core::{BindOpError, Scalar, ScalarKind};
|
||||
use aura_core::{ArgKind, ArgOpError, BindOpError, Scalar, ScalarKind};
|
||||
use aura_vocabulary::std_vocabulary;
|
||||
|
||||
fn session(name: &str) -> GraphSession<'static> {
|
||||
@@ -575,7 +592,7 @@ mod tests {
|
||||
fn add_unknown_type_rejected_at_op() {
|
||||
let mut s = session("g");
|
||||
assert_eq!(
|
||||
s.apply(Op::Add { type_id: "Nope".into(), as_name: None, bind: vec![] }),
|
||||
s.apply(Op::Add { type_id: "Nope".into(), as_name: None, args: vec![], bind: vec![] }),
|
||||
Err(OpError::UnknownNodeType("Nope".into()))
|
||||
);
|
||||
}
|
||||
@@ -583,9 +600,9 @@ mod tests {
|
||||
#[test]
|
||||
fn add_duplicate_identifier_rejected() {
|
||||
let mut s = session("g");
|
||||
s.apply(Op::Add { type_id: "SMA".into(), as_name: Some("x".into()), bind: vec![] }).unwrap();
|
||||
s.apply(Op::Add { type_id: "SMA".into(), as_name: Some("x".into()), args: vec![], bind: vec![] }).unwrap();
|
||||
assert_eq!(
|
||||
s.apply(Op::Add { type_id: "SMA".into(), as_name: Some("x".into()), bind: vec![] }),
|
||||
s.apply(Op::Add { type_id: "SMA".into(), as_name: Some("x".into()), args: vec![], bind: vec![] }),
|
||||
Err(OpError::DuplicateIdentifier("x".into()))
|
||||
);
|
||||
}
|
||||
@@ -597,7 +614,7 @@ mod tests {
|
||||
s.apply(Op::Add {
|
||||
type_id: "SMA".into(),
|
||||
as_name: Some("fast".into()),
|
||||
bind: vec![("length".into(), Scalar::f64(2.0))], // wrong kind (i64 param)
|
||||
args: vec![], bind: vec![("length".into(), Scalar::f64(2.0))], // wrong kind (i64 param)
|
||||
}),
|
||||
Err(OpError::BadParam {
|
||||
node: "fast".into(),
|
||||
@@ -624,9 +641,9 @@ mod tests {
|
||||
fn scaffold() -> GraphSession<'static> {
|
||||
let mut s = session("g");
|
||||
s.apply(Op::Source { role: "price".into(), kind: ScalarKind::F64 }).unwrap();
|
||||
s.apply(Op::Add { type_id: "SMA".into(), as_name: Some("fast".into()), bind: vec![] }).unwrap();
|
||||
s.apply(Op::Add { type_id: "SMA".into(), as_name: Some("slow".into()), bind: vec![] }).unwrap();
|
||||
s.apply(Op::Add { type_id: "Sub".into(), as_name: None, bind: vec![] }).unwrap();
|
||||
s.apply(Op::Add { type_id: "SMA".into(), as_name: Some("fast".into()), args: vec![], bind: vec![] }).unwrap();
|
||||
s.apply(Op::Add { type_id: "SMA".into(), as_name: Some("slow".into()), args: vec![], bind: vec![] }).unwrap();
|
||||
s.apply(Op::Add { type_id: "Sub".into(), as_name: None, args: vec![], bind: vec![] }).unwrap();
|
||||
s
|
||||
}
|
||||
|
||||
@@ -653,8 +670,8 @@ mod tests {
|
||||
fn connect_kind_mismatch_rejected_at_op() {
|
||||
// Gt produces bool; Bias.signal consumes f64 -> eager kind mismatch
|
||||
let mut s = session("g");
|
||||
s.apply(Op::Add { type_id: "Gt".into(), as_name: None, bind: vec![] }).unwrap();
|
||||
s.apply(Op::Add { type_id: "Bias".into(), as_name: None, bind: vec![] }).unwrap();
|
||||
s.apply(Op::Add { type_id: "Gt".into(), as_name: None, args: vec![], bind: vec![] }).unwrap();
|
||||
s.apply(Op::Add { type_id: "Bias".into(), as_name: None, args: vec![], bind: vec![] }).unwrap();
|
||||
let r = s.apply(Op::Connect { from: "gt.value".into(), to: "bias.signal".into() });
|
||||
assert!(matches!(
|
||||
r,
|
||||
@@ -820,9 +837,9 @@ mod tests {
|
||||
use crate::harness::FlatTap;
|
||||
let ops = vec![
|
||||
Op::Source { role: "price".into(), kind: ScalarKind::F64 },
|
||||
Op::Add { type_id: "SMA".into(), as_name: Some("fast".into()), bind: vec![] },
|
||||
Op::Add { type_id: "SMA".into(), as_name: Some("slow".into()), bind: vec![] },
|
||||
Op::Add { type_id: "Sub".into(), as_name: None, bind: vec![] },
|
||||
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::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() },
|
||||
@@ -842,10 +859,10 @@ mod tests {
|
||||
fn gang_op_projects_the_param_space() {
|
||||
let ops = vec![
|
||||
Op::Source { role: "price".into(), kind: ScalarKind::F64 },
|
||||
Op::Add { type_id: "SMA".into(), as_name: Some("a".into()), bind: vec![] },
|
||||
Op::Add { type_id: "SMA".into(), as_name: Some("b".into()), bind: vec![] },
|
||||
Op::Add { type_id: "SMA".into(), as_name: Some("a".into()), args: vec![], bind: vec![] },
|
||||
Op::Add { type_id: "SMA".into(), as_name: Some("b".into()), args: vec![], bind: vec![] },
|
||||
Op::Gang { as_name: "length".into(), into: vec!["a.length".into(), "b.length".into()] },
|
||||
Op::Add { type_id: "Sub".into(), as_name: None, bind: vec![] },
|
||||
Op::Add { type_id: "Sub".into(), as_name: None, args: vec![], bind: vec![] },
|
||||
Op::Feed { role: "price".into(), into: vec!["a.series".into(), "b.series".into()] },
|
||||
Op::Connect { from: "a.value".into(), to: "sub.lhs".into() },
|
||||
Op::Connect { from: "b.value".into(), to: "sub.rhs".into() },
|
||||
@@ -863,7 +880,7 @@ mod tests {
|
||||
/// i64` session-index output, wires its `trigger` input through the ordinary
|
||||
/// edge convention, and builds to a finished `Composite`. Reachability is the
|
||||
/// whole ask — the node's tz-aware/DST session SEMANTICS are already pinned
|
||||
/// in `aura-std/src/session.rs` and are not re-pinned here. Today the preset
|
||||
/// in `aura-market/src/session.rs` and are not re-pinned here. Today the preset
|
||||
/// id is absent from the roster (the base `Session` builder takes structural
|
||||
/// construction args, so the zero-arg macro cannot host it), so the lookup
|
||||
/// returns `None` and the op-script's `Add` would fail `UnknownNodeType`.
|
||||
@@ -892,7 +909,7 @@ mod tests {
|
||||
Op::Add {
|
||||
type_id: "SessionFrankfurt".into(),
|
||||
as_name: Some("session".into()),
|
||||
bind: vec![("period_minutes".into(), Scalar::i64(15))],
|
||||
args: vec![], bind: vec![("period_minutes".into(), Scalar::i64(15))],
|
||||
},
|
||||
Op::Feed { role: "trigger".into(), into: vec!["session.trigger".into()] },
|
||||
Op::Expose { from: "session.bars_since_open".into(), as_name: "bars".into() },
|
||||
@@ -909,11 +926,11 @@ mod tests {
|
||||
// bound member: `b.length` was bound at Add, so it left the open param
|
||||
// list and is unknown to the gang gate, exactly as `try_bind` would see it.
|
||||
let mut s = session("g");
|
||||
s.apply(Op::Add { type_id: "SMA".into(), as_name: Some("a".into()), bind: vec![] }).unwrap();
|
||||
s.apply(Op::Add { type_id: "SMA".into(), as_name: Some("a".into()), args: vec![], bind: vec![] }).unwrap();
|
||||
s.apply(Op::Add {
|
||||
type_id: "SMA".into(),
|
||||
as_name: Some("b".into()),
|
||||
bind: vec![("length".into(), Scalar::i64(5))],
|
||||
args: vec![], bind: vec![("length".into(), Scalar::i64(5))],
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
@@ -923,8 +940,8 @@ mod tests {
|
||||
|
||||
// kind mismatch: SMA.length is i64, Bias.scale is f64.
|
||||
let mut s = session("g");
|
||||
s.apply(Op::Add { type_id: "SMA".into(), as_name: Some("a".into()), bind: vec![] }).unwrap();
|
||||
s.apply(Op::Add { type_id: "Bias".into(), as_name: Some("b".into()), bind: vec![] }).unwrap();
|
||||
s.apply(Op::Add { type_id: "SMA".into(), as_name: Some("a".into()), args: vec![], bind: vec![] }).unwrap();
|
||||
s.apply(Op::Add { type_id: "Bias".into(), as_name: Some("b".into()), args: vec![], bind: vec![] }).unwrap();
|
||||
let r = s.apply(Op::Gang { as_name: "g".into(), into: vec!["a.length".into(), "b.scale".into()] });
|
||||
assert!(
|
||||
matches!(r, Err(OpError::GangKindMismatch { expected: ScalarKind::I64, got: ScalarKind::F64, .. })),
|
||||
@@ -933,9 +950,9 @@ mod tests {
|
||||
|
||||
// doubly-ganged member: a.length is already claimed by an earlier gang.
|
||||
let mut s = session("g");
|
||||
s.apply(Op::Add { type_id: "SMA".into(), as_name: Some("a".into()), bind: vec![] }).unwrap();
|
||||
s.apply(Op::Add { type_id: "SMA".into(), as_name: Some("b".into()), bind: vec![] }).unwrap();
|
||||
s.apply(Op::Add { type_id: "SMA".into(), as_name: Some("c".into()), bind: vec![] }).unwrap();
|
||||
s.apply(Op::Add { type_id: "SMA".into(), as_name: Some("a".into()), args: vec![], bind: vec![] }).unwrap();
|
||||
s.apply(Op::Add { type_id: "SMA".into(), as_name: Some("b".into()), args: vec![], bind: vec![] }).unwrap();
|
||||
s.apply(Op::Add { type_id: "SMA".into(), as_name: Some("c".into()), args: vec![], bind: vec![] }).unwrap();
|
||||
s.apply(Op::Gang { as_name: "lo".into(), into: vec!["a.length".into(), "b.length".into()] }).unwrap();
|
||||
assert_eq!(
|
||||
s.apply(Op::Gang { as_name: "hi".into(), into: vec!["a.length".into(), "c.length".into()] }),
|
||||
@@ -944,7 +961,7 @@ mod tests {
|
||||
|
||||
// single member: a gang needs at least two.
|
||||
let mut s = session("g");
|
||||
s.apply(Op::Add { type_id: "SMA".into(), as_name: Some("a".into()), bind: vec![] }).unwrap();
|
||||
s.apply(Op::Add { type_id: "SMA".into(), as_name: Some("a".into()), args: vec![], bind: vec![] }).unwrap();
|
||||
assert_eq!(
|
||||
s.apply(Op::Gang { as_name: "solo".into(), into: vec!["a.length".into()] }),
|
||||
Err(OpError::GangArity { gang: "solo".into() })
|
||||
@@ -952,7 +969,7 @@ mod tests {
|
||||
|
||||
// unknown node: a member names a node that was never added.
|
||||
let mut s = session("g");
|
||||
s.apply(Op::Add { type_id: "SMA".into(), as_name: Some("a".into()), bind: vec![] }).unwrap();
|
||||
s.apply(Op::Add { type_id: "SMA".into(), as_name: Some("a".into()), args: vec![], bind: vec![] }).unwrap();
|
||||
assert_eq!(
|
||||
s.apply(Op::Gang { as_name: "g".into(), into: vec!["a.length".into(), "ghost.length".into()] }),
|
||||
Err(OpError::UnknownIdentifier("ghost".into()))
|
||||
@@ -1010,7 +1027,7 @@ mod tests {
|
||||
fn finish_accepts_an_open_input_role_and_compile_refuses_it() {
|
||||
let mut s = session("g");
|
||||
s.apply(Op::Input { role: "ext".into() }).unwrap();
|
||||
s.apply(Op::Add { type_id: "Sub".into(), as_name: Some("sub".into()), bind: vec![] }).unwrap();
|
||||
s.apply(Op::Add { type_id: "Sub".into(), as_name: Some("sub".into()), args: vec![], bind: vec![] }).unwrap();
|
||||
s.apply(Op::Feed { role: "ext".into(), into: vec!["sub.lhs".into(), "sub.rhs".into()] }).unwrap();
|
||||
s.apply(Op::Expose { from: "sub.value".into(), as_name: "out".into() }).unwrap();
|
||||
let c = s.finish().expect("finish() no longer gates root-role boundness");
|
||||
@@ -1030,8 +1047,8 @@ mod tests {
|
||||
fn finish_reports_role_kind_mismatch_by_identifier() {
|
||||
let mut s = session("g");
|
||||
s.apply(Op::Source { role: "price".into(), kind: ScalarKind::F64 }).unwrap();
|
||||
s.apply(Op::Add { type_id: "Sub".into(), as_name: Some("sub".into()), bind: vec![] }).unwrap();
|
||||
s.apply(Op::Add { type_id: "And".into(), as_name: Some("conj".into()), bind: vec![] }).unwrap();
|
||||
s.apply(Op::Add { type_id: "Sub".into(), as_name: Some("sub".into()), args: vec![], bind: vec![] }).unwrap();
|
||||
s.apply(Op::Add { type_id: "And".into(), as_name: Some("conj".into()), args: vec![], bind: vec![] }).unwrap();
|
||||
// price fans into sub.lhs (f64) AND conj.a (bool): differing slot kinds.
|
||||
s.apply(Op::Feed { role: "price".into(), into: vec!["sub.lhs".into(), "conj.a".into()] }).unwrap();
|
||||
let err = s.finish().err().unwrap();
|
||||
@@ -1065,8 +1082,8 @@ mod tests {
|
||||
// — only an acyclicity check can catch it.
|
||||
let ops = vec![
|
||||
Op::Source { role: "price".into(), kind: ScalarKind::F64 },
|
||||
Op::Add { type_id: "Sub".into(), as_name: Some("a".into()), bind: vec![] },
|
||||
Op::Add { type_id: "Sub".into(), as_name: Some("b".into()), bind: vec![] },
|
||||
Op::Add { type_id: "Sub".into(), as_name: Some("a".into()), args: vec![], bind: vec![] },
|
||||
Op::Add { type_id: "Sub".into(), as_name: Some("b".into()), args: vec![], bind: vec![] },
|
||||
Op::Feed { role: "price".into(), into: vec!["a.rhs".into(), "b.rhs".into()] },
|
||||
Op::Connect { from: "a.value".into(), to: "b.lhs".into() }, // op 4: a -> b
|
||||
Op::Connect { from: "b.value".into(), to: "a.lhs".into() }, // op 5: closes b -> a
|
||||
@@ -1097,7 +1114,7 @@ mod tests {
|
||||
// loop. Both slots covered, role bound — only an acyclicity check catches it.
|
||||
let ops = vec![
|
||||
Op::Source { role: "price".into(), kind: ScalarKind::F64 },
|
||||
Op::Add { type_id: "Sub".into(), as_name: Some("s".into()), bind: vec![] },
|
||||
Op::Add { type_id: "Sub".into(), as_name: Some("s".into()), args: vec![], bind: vec![] },
|
||||
Op::Feed { role: "price".into(), into: vec!["s.rhs".into()] },
|
||||
Op::Connect { from: "s.value".into(), to: "s.lhs".into() }, // self-edge
|
||||
Op::Expose { from: "s.value".into(), as_name: "out".into() },
|
||||
@@ -1111,9 +1128,9 @@ mod tests {
|
||||
#[test]
|
||||
fn replay_stops_at_first_failing_op_naming_index() {
|
||||
let ops = vec![
|
||||
Op::Add { type_id: "SMA".into(), as_name: Some("fast".into()), bind: vec![] },
|
||||
Op::Add { type_id: "Nope".into(), as_name: None, bind: vec![] }, // op 1 fails
|
||||
Op::Add { type_id: "Sub".into(), as_name: None, bind: vec![] },
|
||||
Op::Add { type_id: "SMA".into(), as_name: Some("fast".into()), args: vec![], bind: vec![] },
|
||||
Op::Add { type_id: "Nope".into(), as_name: None, args: vec![], bind: vec![] }, // op 1 fails
|
||||
Op::Add { type_id: "Sub".into(), as_name: None, args: vec![], bind: vec![] },
|
||||
];
|
||||
// `.err().unwrap()` rather than `.unwrap_err()`: the Ok arm `Composite` is
|
||||
// not `Debug` (it holds build closures), which `unwrap_err` would require.
|
||||
@@ -1141,10 +1158,10 @@ mod tests {
|
||||
// (a) op-script replay
|
||||
let ops = vec![
|
||||
Op::Source { role: "price".into(), kind: ScalarKind::F64 },
|
||||
Op::Add { type_id: "SMA".into(), as_name: Some("fast".into()), bind: vec![] },
|
||||
Op::Add { type_id: "SMA".into(), as_name: Some("slow".into()), bind: vec![] },
|
||||
Op::Add { type_id: "Sub".into(), as_name: None, bind: vec![] },
|
||||
Op::Add { type_id: "Bias".into(), as_name: None, bind: vec![] },
|
||||
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() },
|
||||
@@ -1189,10 +1206,10 @@ mod tests {
|
||||
// (a) op-script: the Task-4 fixture (two SMA + gang + Sub + expose).
|
||||
let ops = vec![
|
||||
Op::Source { role: "price".into(), kind: ScalarKind::F64 },
|
||||
Op::Add { type_id: "SMA".into(), as_name: Some("a".into()), bind: vec![] },
|
||||
Op::Add { type_id: "SMA".into(), as_name: Some("b".into()), bind: vec![] },
|
||||
Op::Add { type_id: "SMA".into(), as_name: Some("a".into()), args: vec![], bind: vec![] },
|
||||
Op::Add { type_id: "SMA".into(), as_name: Some("b".into()), args: vec![], bind: vec![] },
|
||||
Op::Gang { as_name: "length".into(), into: vec!["a.length".into(), "b.length".into()] },
|
||||
Op::Add { type_id: "Sub".into(), as_name: None, bind: vec![] },
|
||||
Op::Add { type_id: "Sub".into(), as_name: None, args: vec![], bind: vec![] },
|
||||
Op::Feed { role: "price".into(), into: vec!["a.series".into(), "b.series".into()] },
|
||||
Op::Connect { from: "a.value".into(), to: "sub.lhs".into() },
|
||||
Op::Connect { from: "b.value".into(), to: "sub.rhs".into() },
|
||||
@@ -1241,9 +1258,9 @@ mod tests {
|
||||
// (a) op-script: an SMA crossover with a tap on the raw spread (sub.value).
|
||||
let ops = vec![
|
||||
Op::Source { role: "price".into(), kind: ScalarKind::F64 },
|
||||
Op::Add { type_id: "SMA".into(), as_name: Some("fast".into()), bind: vec![] },
|
||||
Op::Add { type_id: "SMA".into(), as_name: Some("slow".into()), bind: vec![] },
|
||||
Op::Add { type_id: "Sub".into(), as_name: Some("sub".into()), bind: vec![] },
|
||||
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: Some("sub".into()), 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() },
|
||||
@@ -1277,6 +1294,71 @@ mod tests {
|
||||
assert_eq!(replay_flat.edges, built_flat.edges);
|
||||
}
|
||||
|
||||
// ---- construction args (#271) -------------------------------------
|
||||
|
||||
/// `add_node` applies `try_args` AFTER resolve and BEFORE the bind loop
|
||||
/// (spec §aura-engine): an arg-bearing type's `add` op, given a valid
|
||||
/// `args` set, configures the real signature and the subsequent `bind`
|
||||
/// sees the now-real `period_minutes` param — a data-only op-script
|
||||
/// reaches a `Session` for any IANA timezone/open, not just the
|
||||
/// `SessionFrankfurt` preset.
|
||||
#[test]
|
||||
fn add_op_with_args_builds_a_session() {
|
||||
let mut s = session("g");
|
||||
s.apply(Op::Source { role: "trigger".into(), kind: ScalarKind::F64 }).unwrap();
|
||||
assert_eq!(
|
||||
s.apply(Op::Add {
|
||||
type_id: "Session".into(),
|
||||
as_name: Some("ny".into()),
|
||||
args: vec![("tz".into(), "America/New_York".into()), ("open".into(), "09:30".into())],
|
||||
bind: vec![("period_minutes".into(), Scalar::i64(15))],
|
||||
}),
|
||||
Ok(()),
|
||||
);
|
||||
s.apply(Op::Feed { role: "trigger".into(), into: vec!["ny.trigger".into()] }).unwrap();
|
||||
s.apply(Op::Expose { from: "ny.bars_since_open".into(), as_name: "bars".into() }).unwrap();
|
||||
s.finish().expect("an args-configured Session finishes and builds");
|
||||
}
|
||||
|
||||
/// `Session` is arg-bearing (declares `tz`/`open`): an `add` op naming it
|
||||
/// with NO `args` refuses at the op as `BadArg(MissingArg(..))` — the
|
||||
/// pending builder never reaches the bind loop or the graph (spec
|
||||
/// §aura-engine: "a pending resolve with no args therefore fails as
|
||||
/// MissingArg").
|
||||
#[test]
|
||||
fn add_op_arg_bearing_type_without_args_refuses() {
|
||||
let mut s = session("g");
|
||||
assert_eq!(
|
||||
s.apply(Op::Add {
|
||||
type_id: "Session".into(),
|
||||
as_name: Some("ny".into()),
|
||||
args: vec![],
|
||||
bind: vec![],
|
||||
}),
|
||||
Err(OpError::BadArg { node: "ny".into(), err: ArgOpError::MissingArg("tz".into()) }),
|
||||
);
|
||||
}
|
||||
|
||||
/// A malformed arg value (strict-form refusal, spec §Closedness) surfaces
|
||||
/// as `BadArg(BadValue{..})`, naming the arg, its declared `ArgKind`, and
|
||||
/// the rejected string.
|
||||
#[test]
|
||||
fn add_op_bad_tz_refuses() {
|
||||
let mut s = session("g");
|
||||
assert_eq!(
|
||||
s.apply(Op::Add {
|
||||
type_id: "Session".into(),
|
||||
as_name: Some("ny".into()),
|
||||
args: vec![("tz".into(), "not_a_zone".into()), ("open".into(), "09:30".into())],
|
||||
bind: vec![],
|
||||
}),
|
||||
Err(OpError::BadArg {
|
||||
node: "ny".into(),
|
||||
err: ArgOpError::BadValue { arg: "tz".into(), kind: ArgKind::Tz, got: "not_a_zone".into() },
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// ---- Op::Use (#317) -----------------------------------------------
|
||||
|
||||
/// A small two-node fixture composite for the `Op::Use` tests (#317): an
|
||||
@@ -1412,7 +1494,7 @@ mod tests {
|
||||
fn use_op_duplicate_instance_identifier_rejected() {
|
||||
let subgraph = |_: &str| Some(use_fixture());
|
||||
let mut s = GraphSession::new("g", &std_vocabulary, &subgraph);
|
||||
s.apply(Op::Add { type_id: "SMA".into(), as_name: Some("gate".into()), bind: vec![] }).unwrap();
|
||||
s.apply(Op::Add { type_id: "SMA".into(), as_name: Some("gate".into()), args: vec![], bind: vec![] }).unwrap();
|
||||
assert_eq!(
|
||||
s.apply(Op::Use { ref_id: "fixture".into(), name: Some("gate".into()), bind: vec![] }),
|
||||
Err(OpError::DuplicateIdentifier("gate".into()))
|
||||
@@ -1448,13 +1530,13 @@ mod tests {
|
||||
fn use_gang_fixture() -> Composite {
|
||||
let ops = vec![
|
||||
Op::Input { role: "price".into() },
|
||||
Op::Add { type_id: "SMA".into(), as_name: Some("channel_hi".into()), bind: vec![] },
|
||||
Op::Add { type_id: "SMA".into(), as_name: Some("channel_lo".into()), bind: vec![] },
|
||||
Op::Add { type_id: "SMA".into(), as_name: Some("channel_hi".into()), args: vec![], bind: vec![] },
|
||||
Op::Add { type_id: "SMA".into(), as_name: Some("channel_lo".into()), args: vec![], bind: vec![] },
|
||||
Op::Gang {
|
||||
as_name: "channel_length".into(),
|
||||
into: vec!["channel_hi.length".into(), "channel_lo.length".into()],
|
||||
},
|
||||
Op::Add { type_id: "Sub".into(), as_name: None, bind: vec![] },
|
||||
Op::Add { type_id: "Sub".into(), as_name: None, args: vec![], bind: vec![] },
|
||||
Op::Feed {
|
||||
role: "price".into(),
|
||||
into: vec!["channel_hi.series".into(), "channel_lo.series".into()],
|
||||
@@ -1505,7 +1587,7 @@ mod tests {
|
||||
let mut s = GraphSession::new("g", &std_vocabulary, &subgraph);
|
||||
s.apply(Op::Use { ref_id: "fixture".into(), name: Some("gate".into()), bind: vec![] })
|
||||
.unwrap();
|
||||
s.apply(Op::Add { type_id: "SMA".into(), as_name: Some("solo".into()), bind: vec![] })
|
||||
s.apply(Op::Add { type_id: "SMA".into(), as_name: Some("solo".into()), args: vec![], bind: vec![] })
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
s.apply(Op::Gang {
|
||||
@@ -1527,7 +1609,7 @@ mod tests {
|
||||
fn use_op_connect_closing_a_cycle_through_an_instance_is_rejected_eagerly() {
|
||||
let subgraph = |_: &str| Some(use_fixture());
|
||||
let mut s = GraphSession::new("g", &std_vocabulary, &subgraph);
|
||||
s.apply(Op::Add { type_id: "Sub".into(), as_name: Some("s".into()), bind: vec![] }).unwrap();
|
||||
s.apply(Op::Add { type_id: "Sub".into(), as_name: Some("s".into()), args: vec![], bind: vec![] }).unwrap();
|
||||
s.apply(Op::Use { ref_id: "fixture".into(), name: Some("gate".into()), bind: vec![] }).unwrap();
|
||||
s.apply(Op::Connect { from: "s.value".into(), to: "gate.price".into() }).unwrap();
|
||||
assert_eq!(
|
||||
@@ -1595,7 +1677,7 @@ mod tests {
|
||||
Op::Add {
|
||||
type_id: "SMA".into(),
|
||||
as_name: Some("sma".into()),
|
||||
bind: vec![("length".into(), Scalar::i64(3))],
|
||||
args: vec![], bind: vec![("length".into(), Scalar::i64(3))],
|
||||
},
|
||||
Op::Feed { role: "x".into(), into: vec!["sma.series".into()] },
|
||||
Op::Expose { from: "sma.value".into(), as_name: "out".into() },
|
||||
|
||||
@@ -56,8 +56,8 @@ pub use blueprint::{
|
||||
GangMember, OutField, RandomBinder, ReopenError, Role, SweepBinder, Tap, TapWire,
|
||||
};
|
||||
pub use blueprint_serde::{
|
||||
blueprint_from_json, blueprint_identity_json, blueprint_to_json, BlueprintDoc, CompositeData,
|
||||
LoadError, NodeData, PrimitiveData, SerializeError, BLUEPRINT_FORMAT_VERSION,
|
||||
blueprint_from_json, blueprint_identity_json, blueprint_to_json, ArgData, BlueprintDoc,
|
||||
CompositeData, LoadError, NodeData, PrimitiveData, SerializeError, BLUEPRINT_FORMAT_VERSION,
|
||||
};
|
||||
pub use builder::{BuildError, GraphBuilder, InPort, NodeHandle, OutPort, RoleHandle};
|
||||
pub use construction::{replay, GraphSession, Op, OpError};
|
||||
@@ -88,7 +88,8 @@ pub use walkforward::{
|
||||
// (SourceSpec.kind is a ScalarKind; sources/Recorder columns are Scalar /
|
||||
// Firing / Timestamp) so a graph builder has one import surface, not two.
|
||||
pub use aura_core::{
|
||||
BindOpError, Firing, NodeSchema, ParamSpec, PortSpec, Scalar, ScalarKind, Timestamp,
|
||||
ArgKind, ArgOpError, BindOpError, Firing, NodeSchema, ParamSpec, PortSpec, Scalar, ScalarKind,
|
||||
Timestamp,
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
Reference in New Issue
Block a user