feat(aura): node signature lives in the blueprint; collapse Blueprint into Composite
Consolidate the node data structure so every node's signature (NodeSchema:
inputs/output/params) is declared once and exists in the blueprint pre-build,
and dissolve the special "root graph" type. Behaviour-preserving (C1).
Signature vs sizing
- NodeSchema is now the static signature only: InputSpec -> PortSpec{kind,firing},
with lookback removed. The signature is fully static per blueprint (input
kinds/firing, output fields, params); LinComb's variable arity is a builder arg,
not an injected param.
- The one param-dependent quantity, an input's buffer lookback (e.g. Sma's window =
its injected length), moves out of the signature to Node::lookbacks() -> Vec<usize>,
read only by bootstrap for sizing. Node::schema() is removed.
- LeafFactory -> PrimitiveBuilder, which carries the full NodeSchema. The built node
no longer re-declares it: closes the params-declared-twice drift (#36, the 8
per-node factory_params_match_built_node_schema lockstep tests are deleted — their
subject is now structurally impossible) and a value-empty recipe exposes its full
I/O interface pre-build (#43).
Root is just a bound composite
- struct Blueprint is deleted; its compile/bootstrap/param_space methods move onto
Composite. Role gains source: Option<ScalarKind> (None = open interior port,
Some = bound ingestion feed). A composite is runnable iff every root role is bound;
the "main graph" is no longer a category, only the fully-source-bound composite.
New error CompileError::UnboundRootRole for an open root role.
- BlueprintNode::signature() answers uniformly for both arms: Primitive returns the
builder's declared schema, Composite derives it from the interior (role kinds in,
OutField kinds out, aggregated params), pre-build, no build.
compile -> FlatGraph -> bootstrap
- compile validates structure pre-build via signature() (validate_wiring: range +
kind, returning the same variants as before, so an edge kind fault is now caught
before any build closure fires) and emits FlatGraph{nodes,signatures,sources,edges}.
- bootstrap consumes the FlatGraph: kinds/firing/output from the carried signatures,
buffer depth from node.lookbacks(). SourceSpec survives as the flat descriptor.
Renames: BlueprintNode::Leaf -> Primitive, LeafFactory -> PrimitiveBuilder.
Render (aura-cli/src/graph.rs) is migrated compile-only: it takes &Composite, maps
bound roles to the same source-entry shape, so both render goldens reproduce
byte-identical output (no re-capture needed). Render-fidelity tuning is the next cycle.
Verification (orchestrator-run, not agent-reported): cargo build --workspace green;
cargo test --workspace 150 passed / 0 failed; cargo clippy --workspace --all-targets
-D warnings clean. All pinned determinism/run-output tests pass with values unchanged;
no behavioural assertion was altered to go green. 5 new tests assert the signature is
pre-build and uniform, that compile rejects a kind mismatch without building (via a
panicking builder), UnboundRootRole, and lookbacks()/signature arity agreement.
Deferred to cycle-close audit (per plan): docs/design/INDEX.md and some aura-std
module docs still name the old Node::schema()/LeafFactory/BlueprintNode::Leaf/
Blueprint::param_space contracts; prose reconciliation is the architect's at audit.
closes #43 #36
This commit is contained in:
@@ -15,10 +15,10 @@
|
||||
|
||||
use ascii_dag::graph::{Graph, RenderMode};
|
||||
use ascii_dag::render::colors::Palette;
|
||||
use aura_core::{LeafFactory, Node, ScalarKind};
|
||||
use aura_core::{Node, PrimitiveBuilder, ScalarKind};
|
||||
use aura_engine::{
|
||||
aliases_on, signature_of, Blueprint, BlueprintNode, Composite, Edge, OutField, ParamAlias,
|
||||
SourceSpec, Target,
|
||||
aliases_on, signature_of, BlueprintNode, Composite, Edge, OutField, ParamAlias, SourceSpec,
|
||||
Target,
|
||||
};
|
||||
|
||||
/// Where a leaf's param **names** come from in the shared leaf-label path — the one
|
||||
@@ -58,18 +58,24 @@ pub enum Color {
|
||||
/// Blueprint view: the authored structure (#38). A flat main graph wires the
|
||||
/// harness with each composite shown as a single opaque node; a `where:` section
|
||||
/// defines each distinct composite type once. Flat layout only (no subgraphs).
|
||||
pub fn render_blueprint(bp: &Blueprint, color: Color) -> String {
|
||||
// the main graph is the shared graph core (`render_graph`) over the blueprint's
|
||||
// top-level (nodes, edges): leaves enriched exactly as `where:` interior leaves
|
||||
// are (param names folded in, a `<field> →` prefix for any input fed by a
|
||||
// multi-output producer — the headline: macd's `histogram` driving Exposure),
|
||||
// composites opaque. The deltas vs a composite definition: param names come from
|
||||
// the factory (no alias overlay at the root); the entries are sources, not roles;
|
||||
// there is no output record (terminals are sinks) and no title.
|
||||
pub fn render_blueprint(bp: &Composite, color: Color) -> String {
|
||||
// the main graph is the shared graph core (`render_graph`) over the root
|
||||
// composite's top-level (nodes, edges): leaves enriched exactly as `where:`
|
||||
// interior leaves are (param names folded in, a `<field> →` prefix for any input
|
||||
// fed by a multi-output producer — the headline: macd's `histogram` driving
|
||||
// Exposure), composites opaque. The deltas vs a composite definition: param names
|
||||
// come from the builder (no alias overlay at the root); the entries are the
|
||||
// root's source-bound roles, not interior roles; there is no output record
|
||||
// (terminals are sinks) and no title.
|
||||
let entries: Vec<Entry> = bp
|
||||
.sources()
|
||||
.input_roles()
|
||||
.iter()
|
||||
.map(|src| Entry { name: format!("source:{:?}", src.kind), targets: &src.targets })
|
||||
.filter_map(|role| {
|
||||
role.source.map(|kind| Entry {
|
||||
name: format!("source:{kind:?}"),
|
||||
targets: &role.targets,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
let main = render_graph(
|
||||
bp.nodes(),
|
||||
@@ -94,7 +100,7 @@ pub fn render_blueprint(bp: &Blueprint, color: Color) -> String {
|
||||
/// The shared graph core both views render through (#48): a flat ascii-dag of the
|
||||
/// computation DAG plus external-entry markers. `nodes`/`edges` are a borrowed
|
||||
/// (blueprint-root or composite-interior) graph slice — never an owned
|
||||
/// `Composite` (`LeafFactory` is not `Clone`). Each leaf gets the unified
|
||||
/// `Composite` (`PrimitiveBuilder` is not `Clone`). Each leaf gets the unified
|
||||
/// [`leaf_label`]; each composite is opaque (`[name]`); a producing node carrying an
|
||||
/// `OutField` re-export gets the `name := ` binding prefix folded on (empty list →
|
||||
/// no binding, the blueprint case); every [`Entry`] is drawn as a marker wired to its
|
||||
@@ -115,7 +121,7 @@ fn render_graph(
|
||||
let mut labels: Vec<String> = Vec::with_capacity(nodes.len());
|
||||
for (i, item) in nodes.iter().enumerate() {
|
||||
let base = match item {
|
||||
BlueprintNode::Leaf(factory) => {
|
||||
BlueprintNode::Primitive(factory) => {
|
||||
leaf_label(nodes, edges, entries, i, factory, ¶m_names, stub_ctx)
|
||||
}
|
||||
BlueprintNode::Composite(c) => c.name().to_string(),
|
||||
@@ -166,7 +172,7 @@ fn render_flat(labels: &[String], edges: &[(usize, usize)], color: Color) -> Str
|
||||
/// `name()` (the authoring type identity — same name implies same structure).
|
||||
/// Recurses into a composite's interior on first sight so nested composites get
|
||||
/// their own definition; a later same-name occurrence is skipped (deduped).
|
||||
fn collect_distinct_composites(bp: &Blueprint) -> Vec<&Composite> {
|
||||
fn collect_distinct_composites(bp: &Composite) -> Vec<&Composite> {
|
||||
fn walk<'a>(items: &'a [BlueprintNode], seen: &mut Vec<&'a str>, out: &mut Vec<&'a Composite>) {
|
||||
for item in items {
|
||||
if let BlueprintNode::Composite(c) = item
|
||||
@@ -209,7 +215,7 @@ fn signature(c: &Composite) -> String {
|
||||
.nodes()
|
||||
.get(a.node)
|
||||
.and_then(|n| match n {
|
||||
BlueprintNode::Leaf(f) => f.params().get(a.slot).map(|p| kind_str(p.kind)),
|
||||
BlueprintNode::Primitive(f) => f.params().get(a.slot).map(|p| kind_str(p.kind)),
|
||||
BlueprintNode::Composite(_) => None,
|
||||
})
|
||||
.unwrap_or("?");
|
||||
@@ -247,7 +253,7 @@ fn leaf_label(
|
||||
edges: &[Edge],
|
||||
entries: &[Entry],
|
||||
index: usize,
|
||||
factory: &LeafFactory,
|
||||
factory: &PrimitiveBuilder,
|
||||
param_names: &ParamNames,
|
||||
stub_ctx: Option<&Composite>,
|
||||
) -> String {
|
||||
@@ -334,7 +340,7 @@ fn multi_output_field_name(nodes: &[BlueprintNode], from: usize, from_field: usi
|
||||
None
|
||||
}
|
||||
}
|
||||
BlueprintNode::Leaf(_) => (from_field > 0).then(|| from_field.to_string()),
|
||||
BlueprintNode::Primitive(_) => (from_field > 0).then(|| from_field.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -396,7 +402,7 @@ fn slot_source(c: &Composite, index: usize, slot: usize) -> (String, usize, bool
|
||||
/// rendered identifier never goes below.
|
||||
fn signature_base_len(c: &Composite, node: usize) -> usize {
|
||||
match &c.nodes()[node] {
|
||||
BlueprintNode::Leaf(_) => 1 + aliases_on(c.params(), node).count(),
|
||||
BlueprintNode::Primitive(_) => 1 + aliases_on(c.params(), node).count(),
|
||||
BlueprintNode::Composite(_) => 1,
|
||||
}
|
||||
}
|
||||
|
||||
+129
-87
@@ -10,8 +10,8 @@ mod graph;
|
||||
|
||||
use aura_core::{Firing, Scalar, ScalarKind, Timestamp};
|
||||
use aura_engine::{
|
||||
f64_field, summarize, Blueprint, BlueprintNode, Composite, Edge, Harness, OutField,
|
||||
ParamAlias, Role, RunManifest, RunReport, SourceSpec, Target,
|
||||
f64_field, summarize, BlueprintNode, Composite, Edge, FlatGraph, Harness, OutField, ParamAlias,
|
||||
Role, RunManifest, RunReport, SourceSpec, Target,
|
||||
};
|
||||
use aura_std::{Ema, Exposure, Recorder, SimBroker, Sma, Sub};
|
||||
use std::io::IsTerminal;
|
||||
@@ -50,8 +50,13 @@ fn sample_harness() -> (
|
||||
) {
|
||||
let (tx_eq, rx_eq) = mpsc::channel();
|
||||
let (tx_ex, rx_ex) = mpsc::channel();
|
||||
let h = Harness::bootstrap(
|
||||
vec![
|
||||
let f64_recorder_sig = || aura_engine::NodeSchema {
|
||||
inputs: vec![aura_engine::PortSpec { kind: ScalarKind::F64, firing: Firing::Any }],
|
||||
output: vec![],
|
||||
params: vec![],
|
||||
};
|
||||
let h = Harness::bootstrap(FlatGraph {
|
||||
nodes: vec![
|
||||
Box::new(Sma::new(2)), // 0 fast SMA
|
||||
Box::new(Sma::new(4)), // 1 slow SMA
|
||||
Box::new(Sub::new()), // 2 spread
|
||||
@@ -60,7 +65,16 @@ fn sample_harness() -> (
|
||||
Box::new(Recorder::new(&[ScalarKind::F64], Firing::Any, tx_eq)), // 5 equity sink
|
||||
Box::new(Recorder::new(&[ScalarKind::F64], Firing::Any, tx_ex)), // 6 exposure sink
|
||||
],
|
||||
vec![SourceSpec {
|
||||
signatures: vec![
|
||||
Sma::builder().schema().clone(),
|
||||
Sma::builder().schema().clone(),
|
||||
Sub::builder().schema().clone(),
|
||||
Exposure::builder().schema().clone(),
|
||||
SimBroker::builder(0.0001).schema().clone(),
|
||||
f64_recorder_sig(),
|
||||
f64_recorder_sig(),
|
||||
],
|
||||
sources: vec![SourceSpec {
|
||||
kind: ScalarKind::F64,
|
||||
targets: vec![
|
||||
Target { node: 0, slot: 0 },
|
||||
@@ -68,7 +82,7 @@ fn sample_harness() -> (
|
||||
Target { node: 4, slot: 1 }, // price into the broker's price slot
|
||||
],
|
||||
}],
|
||||
vec![
|
||||
edges: vec![
|
||||
Edge { from: 0, to: 2, slot: 0, from_field: 0 },
|
||||
Edge { from: 1, to: 2, slot: 1, from_field: 0 },
|
||||
Edge { from: 2, to: 3, slot: 0, from_field: 0 },
|
||||
@@ -76,7 +90,7 @@ fn sample_harness() -> (
|
||||
Edge { from: 4, to: 5, slot: 0, from_field: 0 }, // equity -> sink 5
|
||||
Edge { from: 3, to: 6, slot: 0, from_field: 0 }, // exposure -> sink 6
|
||||
],
|
||||
)
|
||||
})
|
||||
.expect("valid sample signal-quality DAG");
|
||||
(h, rx_eq, rx_ex)
|
||||
}
|
||||
@@ -122,7 +136,7 @@ fn run_sample() -> RunReport {
|
||||
fn sma_cross(name: &str) -> Composite {
|
||||
Composite::new(
|
||||
name,
|
||||
vec![Sma::factory().into(), Sma::factory().into(), Sub::factory().into()],
|
||||
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 },
|
||||
@@ -130,6 +144,7 @@ fn sma_cross(name: &str) -> Composite {
|
||||
vec![Role {
|
||||
name: "price".into(),
|
||||
targets: vec![Target { node: 0, slot: 0 }, Target { node: 1, slot: 0 }],
|
||||
source: None,
|
||||
}],
|
||||
vec![
|
||||
ParamAlias { name: "fast".into(), node: 0, slot: 0 }, // fast SMA length
|
||||
@@ -143,35 +158,39 @@ fn sma_cross(name: &str) -> Composite {
|
||||
/// exposure scale are injected at compile via the point vector (see
|
||||
/// `sample_point`). Recorders need a channel to construct; the receivers are
|
||||
/// dropped because the render never runs the graph.
|
||||
fn build_sample() -> Blueprint {
|
||||
fn build_sample() -> Composite {
|
||||
let (tx_eq, _rx_eq) = mpsc::channel();
|
||||
let (tx_ex, _rx_ex) = mpsc::channel();
|
||||
Blueprint::new(
|
||||
Composite::new(
|
||||
"sample",
|
||||
vec![
|
||||
BlueprintNode::Composite(sma_cross("sma_cross")),
|
||||
Exposure::factory().into(),
|
||||
SimBroker::factory(0.0001).into(),
|
||||
Recorder::factory(vec![ScalarKind::F64], Firing::Any, tx_eq).into(),
|
||||
Recorder::factory(vec![ScalarKind::F64], Firing::Any, tx_ex).into(),
|
||||
Exposure::builder().into(),
|
||||
SimBroker::builder(0.0001).into(),
|
||||
Recorder::builder(vec![ScalarKind::F64], Firing::Any, tx_eq).into(),
|
||||
Recorder::builder(vec![ScalarKind::F64], Firing::Any, tx_ex).into(),
|
||||
],
|
||||
vec![SourceSpec {
|
||||
kind: ScalarKind::F64,
|
||||
targets: vec![
|
||||
Target { node: 0, slot: 0 }, // price -> sma_cross role 0
|
||||
Target { node: 2, slot: 1 }, // price -> SimBroker price slot
|
||||
],
|
||||
}],
|
||||
vec![
|
||||
Edge { from: 0, to: 1, slot: 0, from_field: 0 }, // spread -> Exposure
|
||||
Edge { from: 1, to: 2, slot: 0, from_field: 0 }, // exposure -> broker slot 0
|
||||
Edge { from: 2, to: 3, slot: 0, from_field: 0 }, // equity -> sink
|
||||
Edge { from: 1, to: 4, slot: 0, from_field: 0 }, // exposure -> sink
|
||||
],
|
||||
vec![Role {
|
||||
name: "price".into(),
|
||||
targets: vec![
|
||||
Target { node: 0, slot: 0 }, // price -> sma_cross role 0
|
||||
Target { node: 2, slot: 1 }, // price -> SimBroker price slot
|
||||
],
|
||||
source: Some(ScalarKind::F64),
|
||||
}],
|
||||
vec![], // params: the interior sma_cross carries the aliases
|
||||
vec![], // output: the root ends in sinks, no re-export
|
||||
)
|
||||
}
|
||||
|
||||
/// The built-in sample rendered by `aura graph`.
|
||||
fn sample_blueprint() -> Blueprint {
|
||||
fn sample_blueprint() -> Composite {
|
||||
build_sample()
|
||||
}
|
||||
|
||||
@@ -195,11 +214,11 @@ fn macd(name: &str) -> Composite {
|
||||
Composite::new(
|
||||
name,
|
||||
vec![
|
||||
Ema::factory().into(), // 0 fast EMA
|
||||
Ema::factory().into(), // 1 slow EMA
|
||||
Sub::factory().into(), // 2 MACD line = fast − slow
|
||||
Ema::factory().into(), // 3 signal EMA of the MACD line
|
||||
Sub::factory().into(), // 4 histogram = MACD line − signal
|
||||
Ema::builder().into(), // 0 fast EMA
|
||||
Ema::builder().into(), // 1 slow EMA
|
||||
Sub::builder().into(), // 2 MACD line = fast − slow
|
||||
Ema::builder().into(), // 3 signal EMA of the MACD line
|
||||
Sub::builder().into(), // 4 histogram = MACD line − signal
|
||||
],
|
||||
vec![
|
||||
Edge { from: 0, to: 2, slot: 0, from_field: 0 }, // fast → line[0]
|
||||
@@ -214,6 +233,7 @@ fn macd(name: &str) -> Composite {
|
||||
Target { node: 0, slot: 0 }, // price → fast EMA
|
||||
Target { node: 1, slot: 0 }, // price → slow EMA
|
||||
],
|
||||
source: None,
|
||||
}],
|
||||
vec![
|
||||
ParamAlias { name: "fast".into(), node: 0, slot: 0 }, // fast EMA length
|
||||
@@ -234,34 +254,38 @@ fn macd(name: &str) -> Composite {
|
||||
fn macd_strategy_blueprint(
|
||||
tx_eq: mpsc::Sender<(Timestamp, Vec<Scalar>)>,
|
||||
tx_ex: mpsc::Sender<(Timestamp, Vec<Scalar>)>,
|
||||
) -> Blueprint {
|
||||
Blueprint::new(
|
||||
) -> Composite {
|
||||
Composite::new(
|
||||
"macd_strategy",
|
||||
vec![
|
||||
BlueprintNode::Composite(macd("macd")),
|
||||
Exposure::factory().into(),
|
||||
SimBroker::factory(0.0001).into(),
|
||||
Recorder::factory(vec![ScalarKind::F64], Firing::Any, tx_eq).into(),
|
||||
Recorder::factory(vec![ScalarKind::F64], Firing::Any, tx_ex).into(),
|
||||
Exposure::builder().into(),
|
||||
SimBroker::builder(0.0001).into(),
|
||||
Recorder::builder(vec![ScalarKind::F64], Firing::Any, tx_eq).into(),
|
||||
Recorder::builder(vec![ScalarKind::F64], Firing::Any, tx_ex).into(),
|
||||
],
|
||||
vec![SourceSpec {
|
||||
kind: ScalarKind::F64,
|
||||
targets: vec![
|
||||
Target { node: 0, slot: 0 }, // price → macd role 0
|
||||
Target { node: 2, slot: 1 }, // price → SimBroker price slot
|
||||
],
|
||||
}],
|
||||
vec![
|
||||
Edge { from: 0, to: 1, slot: 0, from_field: 2 }, // histogram → Exposure
|
||||
Edge { from: 1, to: 2, slot: 0, from_field: 0 }, // exposure → broker slot 0
|
||||
Edge { from: 2, to: 3, slot: 0, from_field: 0 }, // equity → sink
|
||||
Edge { from: 1, to: 4, slot: 0, from_field: 0 }, // exposure → sink
|
||||
],
|
||||
vec![Role {
|
||||
name: "price".into(),
|
||||
targets: vec![
|
||||
Target { node: 0, slot: 0 }, // price → macd role 0
|
||||
Target { node: 2, slot: 1 }, // price → SimBroker price slot
|
||||
],
|
||||
source: Some(ScalarKind::F64),
|
||||
}],
|
||||
vec![], // params: the interior macd carries the aliases
|
||||
vec![], // output: the root ends in sinks, no re-export
|
||||
)
|
||||
}
|
||||
|
||||
/// The MACD strategy blueprint for the structural render (receivers dropped, as
|
||||
/// the render never runs the graph).
|
||||
fn macd_blueprint() -> Blueprint {
|
||||
fn macd_blueprint() -> Composite {
|
||||
let (tx_eq, _rx_eq) = mpsc::channel();
|
||||
let (tx_ex, _rx_ex) = mpsc::channel();
|
||||
macd_strategy_blueprint(tx_eq, tx_ex)
|
||||
@@ -296,10 +320,10 @@ fn macd_prices() -> Vec<(Timestamp, Scalar)> {
|
||||
fn run_macd() -> RunReport {
|
||||
let (tx_eq, rx_eq) = mpsc::channel();
|
||||
let (tx_ex, rx_ex) = mpsc::channel();
|
||||
let (nodes, sources, edges) = macd_strategy_blueprint(tx_eq, tx_ex)
|
||||
let flat = macd_strategy_blueprint(tx_eq, tx_ex)
|
||||
.compile_with_params(&macd_point())
|
||||
.expect("valid macd blueprint");
|
||||
let mut h = Harness::bootstrap(nodes, sources, edges).expect("valid macd harness");
|
||||
let mut h = Harness::bootstrap(flat).expect("valid macd harness");
|
||||
|
||||
let prices = macd_prices();
|
||||
let window = (
|
||||
@@ -333,9 +357,9 @@ fn run_macd() -> RunReport {
|
||||
|
||||
/// Compile a blueprint under its point vector and render the flat post-inline
|
||||
/// (C23) view — the shared body of the `--compiled` paths.
|
||||
fn render_compiled(bp: Blueprint, point: &[Scalar], color: graph::Color) -> String {
|
||||
let (nodes, sources, edges) = bp.compile_with_params(point).expect("valid blueprint");
|
||||
graph::render_compilat(&nodes, &sources, &edges, color)
|
||||
fn render_compiled(bp: Composite, point: &[Scalar], color: graph::Color) -> String {
|
||||
let flat = bp.compile_with_params(point).expect("valid blueprint");
|
||||
graph::render_compilat(&flat.nodes, &flat.sources, &flat.edges, color)
|
||||
}
|
||||
|
||||
const USAGE: &str = "usage: aura run [--macd] | aura graph [--compiled | --macd [--compiled]]";
|
||||
@@ -415,25 +439,30 @@ mod tests {
|
||||
// structure only (no compile/validate), so a minimal fixture suffices.
|
||||
let inner = Composite::new(
|
||||
"inner",
|
||||
vec![Sma::factory().into()],
|
||||
vec![Sma::builder().into()],
|
||||
vec![],
|
||||
vec![Role { name: "price".into(), targets: vec![Target { node: 0, slot: 0 }] }],
|
||||
vec![Role { name: "price".into(), targets: vec![Target { node: 0, slot: 0 }], source: None }],
|
||||
vec![],
|
||||
vec![OutField { node: 0, field: 0, name: "out".into() }],
|
||||
);
|
||||
let outer = Composite::new(
|
||||
"outer",
|
||||
vec![BlueprintNode::Composite(inner), Sub::factory().into()],
|
||||
vec![BlueprintNode::Composite(inner), Sub::builder().into()],
|
||||
vec![Edge { from: 0, to: 1, slot: 0, from_field: 0 }],
|
||||
vec![Role { name: "price".into(), targets: vec![Target { node: 0, slot: 0 }] }],
|
||||
vec![Role { name: "price".into(), targets: vec![Target { node: 0, slot: 0 }], source: None }],
|
||||
vec![],
|
||||
vec![OutField { node: 1, field: 0, name: "out".into() }],
|
||||
);
|
||||
let bp = Blueprint::new(
|
||||
vec![BlueprintNode::Composite(outer)],
|
||||
vec![SourceSpec { kind: ScalarKind::F64, targets: vec![Target { node: 0, slot: 0 }] }],
|
||||
vec![],
|
||||
);
|
||||
let bp = Composite::new(
|
||||
"root",
|
||||
vec![BlueprintNode::Composite(outer)],
|
||||
vec![],
|
||||
vec![
|
||||
Role { name: "src".into(), targets: vec![Target { node: 0, slot: 0 }], source: Some(ScalarKind::F64) },
|
||||
],
|
||||
vec![], // params
|
||||
vec![], // output
|
||||
);
|
||||
let out = graph::render_blueprint(&bp, graph::Color::Plain); // must not panic (no unimplemented!)
|
||||
// outer shows the inner composite as an opaque node, and both get a definition
|
||||
assert!(out.contains("[outer]"), "missing opaque outer node:\n{out}");
|
||||
@@ -445,21 +474,23 @@ mod tests {
|
||||
#[test]
|
||||
fn reused_composite_defined_once() {
|
||||
// the same composite type used twice: two opaque nodes, one definition.
|
||||
let bp = Blueprint::new(
|
||||
vec![
|
||||
let bp = Composite::new(
|
||||
"root",
|
||||
vec![
|
||||
BlueprintNode::Composite(sma_cross("dup")),
|
||||
BlueprintNode::Composite(sma_cross("dup")),
|
||||
Exposure::factory().into(),
|
||||
Exposure::builder().into(),
|
||||
],
|
||||
vec![SourceSpec {
|
||||
kind: ScalarKind::F64,
|
||||
targets: vec![Target { node: 0, slot: 0 }, Target { node: 1, slot: 0 }],
|
||||
}],
|
||||
vec![
|
||||
vec![
|
||||
Edge { from: 0, to: 2, slot: 0, from_field: 0 },
|
||||
Edge { from: 1, to: 2, slot: 0, from_field: 0 },
|
||||
],
|
||||
);
|
||||
vec![
|
||||
Role { name: "src".into(), targets: vec![Target { node: 0, slot: 0 }, Target { node: 1, slot: 0 }], source: Some(ScalarKind::F64) },
|
||||
],
|
||||
vec![], // params
|
||||
vec![], // output
|
||||
);
|
||||
let out = graph::render_blueprint(&bp, graph::Color::Plain);
|
||||
assert_eq!(out.matches("[dup]").count(), 2, "two opaque uses expected:\n{out}");
|
||||
assert_eq!(out.matches("dup(").count(), 1, "body defined once:\n{out}");
|
||||
@@ -468,9 +499,8 @@ mod tests {
|
||||
#[test]
|
||||
fn compiled_view_dissolves_the_composite_boundary() {
|
||||
let bp = sample_blueprint();
|
||||
let (nodes, sources, edges) =
|
||||
bp.compile_with_params(&sample_point()).expect("valid sample");
|
||||
let out = graph::render_compilat(&nodes, &sources, &edges, graph::Color::Plain);
|
||||
let flat = bp.compile_with_params(&sample_point()).expect("valid sample");
|
||||
let out = graph::render_compilat(&flat.nodes, &flat.sources, &flat.edges, graph::Color::Plain);
|
||||
// node labels survive inlining...
|
||||
assert!(out.contains("SMA(2)") && out.contains("SMA(4)"), "labels lost:\n{out}");
|
||||
// ...but the composite cluster name does NOT (boundary dissolved, C23)
|
||||
@@ -483,12 +513,12 @@ mod tests {
|
||||
// The blueprint is now param-generic (identical for both orderings), so the
|
||||
// swap is observable only after the vector is injected — in the COMPILED
|
||||
// view, where the flat nodes carry their valued labels (SMA(2)/SMA(4)).
|
||||
let (cn, cs, ce) =
|
||||
let cflat =
|
||||
sample_blueprint().compile_with_params(&sample_point()).expect("valid sample");
|
||||
let correct = graph::render_compilat(&cn, &cs, &ce, graph::Color::Plain);
|
||||
let (sn, ss, se) =
|
||||
let correct = graph::render_compilat(&cflat.nodes, &cflat.sources, &cflat.edges, graph::Color::Plain);
|
||||
let sflat =
|
||||
sample_blueprint().compile_with_params(&swapped_point()).expect("valid sample");
|
||||
let swapped = graph::render_compilat(&sn, &ss, &se, graph::Color::Plain);
|
||||
let swapped = graph::render_compilat(&sflat.nodes, &sflat.sources, &sflat.edges, graph::Color::Plain);
|
||||
assert_ne!(correct, swapped, "a fast/slow SMA swap must change the compiled render");
|
||||
}
|
||||
|
||||
@@ -534,9 +564,8 @@ sma_cross(fast:i64, slow:i64) -> (cross):
|
||||
#[test]
|
||||
fn compiled_view_golden() {
|
||||
let bp = sample_blueprint();
|
||||
let (nodes, sources, edges) =
|
||||
bp.compile_with_params(&sample_point()).expect("valid sample");
|
||||
let out = graph::render_compilat(&nodes, &sources, &edges, graph::Color::Plain);
|
||||
let flat = bp.compile_with_params(&sample_point()).expect("valid sample");
|
||||
let out = graph::render_compilat(&flat.nodes, &flat.sources, &flat.edges, graph::Color::Plain);
|
||||
let expected = r#" [source:F64]
|
||||
┌────────└─┐──────┐
|
||||
↓ ↓ │
|
||||
@@ -615,13 +644,20 @@ sma_cross(fast:i64, slow:i64) -> (cross):
|
||||
// #price (role name) and #Es
|
||||
let c = Composite::new(
|
||||
"roles",
|
||||
vec![Ema::factory().into(), Sub::factory().into()],
|
||||
vec![Ema::builder().into(), Sub::builder().into()],
|
||||
vec![Edge { from: 0, to: 1, slot: 1, from_field: 0 }],
|
||||
vec![Role { name: "price".into(), targets: vec![Target { node: 1, slot: 0 }] }],
|
||||
vec![Role { name: "price".into(), targets: vec![Target { node: 1, slot: 0 }], source: None }],
|
||||
vec![ParamAlias { name: "slow".into(), node: 0, slot: 0 }],
|
||||
vec![OutField { node: 1, field: 0, name: "o".into() }],
|
||||
);
|
||||
let bp = Blueprint::new(vec![BlueprintNode::Composite(c)], vec![], vec![]);
|
||||
let bp = Composite::new(
|
||||
"root",
|
||||
vec![BlueprintNode::Composite(c)],
|
||||
vec![],
|
||||
vec![],
|
||||
vec![], // params
|
||||
vec![], // output
|
||||
);
|
||||
let out = graph::render_blueprint(&bp, graph::Color::Plain);
|
||||
assert!(out.contains("[o := Sub(#price,#Es)]"), "role name verbatim + source-derived, bound as output `o`: {out}");
|
||||
}
|
||||
@@ -634,13 +670,13 @@ sma_cross(fast:i64, slow:i64) -> (cross):
|
||||
let c = Composite::new(
|
||||
"nest",
|
||||
vec![
|
||||
Ema::factory().into(), // 0 fast
|
||||
Ema::factory().into(), // 1 slow
|
||||
Ema::factory().into(), // 2 up
|
||||
Ema::factory().into(), // 3 down
|
||||
Sub::factory().into(), // 4 = Sub(0,1)
|
||||
Sub::factory().into(), // 5 = Sub(2,3)
|
||||
Sub::factory().into(), // 6 = Sub(4,5) (the outer fan-in)
|
||||
Ema::builder().into(), // 0 fast
|
||||
Ema::builder().into(), // 1 slow
|
||||
Ema::builder().into(), // 2 up
|
||||
Ema::builder().into(), // 3 down
|
||||
Sub::builder().into(), // 4 = Sub(0,1)
|
||||
Sub::builder().into(), // 5 = Sub(2,3)
|
||||
Sub::builder().into(), // 6 = Sub(4,5) (the outer fan-in)
|
||||
],
|
||||
vec![
|
||||
Edge { from: 0, to: 4, slot: 0, from_field: 0 },
|
||||
@@ -657,8 +693,7 @@ sma_cross(fast:i64, slow:i64) -> (cross):
|
||||
Target { node: 1, slot: 0 },
|
||||
Target { node: 2, slot: 0 },
|
||||
Target { node: 3, slot: 0 },
|
||||
],
|
||||
}],
|
||||
], source: None, }],
|
||||
vec![
|
||||
ParamAlias { name: "fast".into(), node: 0, slot: 0 },
|
||||
ParamAlias { name: "slow".into(), node: 1, slot: 0 },
|
||||
@@ -667,7 +702,14 @@ sma_cross(fast:i64, slow:i64) -> (cross):
|
||||
],
|
||||
vec![OutField { node: 6, field: 0, name: "o".into() }],
|
||||
);
|
||||
let bp = Blueprint::new(vec![BlueprintNode::Composite(c)], vec![], vec![]);
|
||||
let bp = Composite::new(
|
||||
"root",
|
||||
vec![BlueprintNode::Composite(c)],
|
||||
vec![],
|
||||
vec![],
|
||||
vec![], // params
|
||||
vec![], // output
|
||||
);
|
||||
let out = graph::render_blueprint(&bp, graph::Color::Plain);
|
||||
assert!(out.contains("[o := Sub(#SEf,#SEu)]"), "outer Sub descends into inner Subs, bound as output `o`: {out}");
|
||||
assert!(out.contains("[Sub(#Ef,#Es)]"), "inner Sub uses EMA aliases: {out}");
|
||||
|
||||
Reference in New Issue
Block a user