feat(aura-cli): unify blueprint main-graph render through one shared core
render_blueprint and render_definition were two divergent paths: the main graph drew bare factory.label() leaves and bare edges, the composite interior drew enriched leaves (params, slot stubs, output bindings). They now delegate to one shared render_graph over borrowed slices — the blueprint is the root composite, differing only at the borders (ParamNames::Factory vs Aliases, Entry unifying SourceSpec and Role, empty OutField list = no bindings). LeafFactory holds a Box<dyn Fn>, so it is not Clone — an owned root-composite adapter is impossible; the shared core takes borrowed slices instead. The main graph now shows what the strategy does: a multi-output producer's selected field surfaces as a consumer-side prefix ([histogram -> Exposure(scale)]), mirroring the := output-binding form. Edge labels are deliberately avoided — ascii-dag 0.9.1 silently drops an edge label it cannot place without collision (arena_render.rs can_place_label), unreliable in a dense graph. Deferred: top-level blueprint multi-input leaves render without #-slot stubs (stub_ctx None) — fan_in_identifiers is still composite-coupled via signature_of. refs #48
This commit is contained in:
+224
-88
@@ -17,9 +17,34 @@ use ascii_dag::graph::{Graph, RenderMode};
|
|||||||
use ascii_dag::render::colors::Palette;
|
use ascii_dag::render::colors::Palette;
|
||||||
use aura_core::{LeafFactory, Node, ScalarKind};
|
use aura_core::{LeafFactory, Node, ScalarKind};
|
||||||
use aura_engine::{
|
use aura_engine::{
|
||||||
aliases_on, signature_of, Blueprint, BlueprintNode, Composite, Edge, OutField, SourceSpec,
|
aliases_on, signature_of, Blueprint, BlueprintNode, Composite, Edge, OutField, ParamAlias,
|
||||||
|
SourceSpec, Target,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Where a leaf's param **names** come from in the shared leaf-label path — the one
|
||||||
|
/// border that differs between the two views (#48). A composite interior leaf draws
|
||||||
|
/// its names from the composite's `ParamAlias` overlay (filtered to this node,
|
||||||
|
/// keeping the existing `where:` form byte-for-byte); a top-level blueprint leaf has
|
||||||
|
/// no alias overlay, so its names come straight from the factory's declared params.
|
||||||
|
enum ParamNames<'a> {
|
||||||
|
/// Composite interior: the alias list, filtered by `node == index` (existing
|
||||||
|
/// `where:` behaviour — only aliased slots show, never factory defaults).
|
||||||
|
Aliases(&'a [ParamAlias]),
|
||||||
|
/// Top-level blueprint leaf: every factory-declared param name, in order.
|
||||||
|
Factory,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A CLI-local, *borrowed* render notion unifying a composite **input role** and a
|
||||||
|
/// blueprint **source** for the shared graph core (#48): both are an external entry
|
||||||
|
/// drawn as a marker node wired into its interior `targets`. The composite builds
|
||||||
|
/// these from `Role` (name = role name), the blueprint from `SourceSpec` (name =
|
||||||
|
/// `source:{kind}`) — no engine change, the list is assembled CLI-side and borrows
|
||||||
|
/// the engine's `Target` slices.
|
||||||
|
struct Entry<'a> {
|
||||||
|
name: String,
|
||||||
|
targets: &'a [Target],
|
||||||
|
}
|
||||||
|
|
||||||
/// Edge colouring for a rendered graph. `Plain` is monochrome — golden-stable and
|
/// Edge colouring for a rendered graph. `Plain` is monochrome — golden-stable and
|
||||||
/// pipe-safe (no escape codes when redirected to a file). `Ansi` emits per-edge
|
/// pipe-safe (no escape codes when redirected to a file). `Ansi` emits per-edge
|
||||||
/// ANSI colours so crossing edges stay traceable on an interactive terminal. The
|
/// ANSI colours so crossing edges stay traceable on an interactive terminal. The
|
||||||
@@ -34,38 +59,28 @@ pub enum Color {
|
|||||||
/// harness with each composite shown as a single opaque node; a `where:` section
|
/// harness with each composite shown as a single opaque node; a `where:` section
|
||||||
/// defines each distinct composite type once. Flat layout only (no subgraphs).
|
/// defines each distinct composite type once. Flat layout only (no subgraphs).
|
||||||
pub fn render_blueprint(bp: &Blueprint, color: Color) -> String {
|
pub fn render_blueprint(bp: &Blueprint, color: Color) -> String {
|
||||||
// pass 1: one main-graph node per top-level item (leaf -> bare-type label;
|
// the main graph is the shared graph core (`render_graph`) over the blueprint's
|
||||||
// composite -> its name, opaque) and one node per source.
|
// top-level (nodes, edges): leaves enriched exactly as `where:` interior leaves
|
||||||
let mut labels: Vec<String> = Vec::new();
|
// are (param names folded in, a `<field> →` prefix for any input fed by a
|
||||||
let mut item_ids: Vec<usize> = Vec::with_capacity(bp.nodes().len());
|
// multi-output producer — the headline: macd's `histogram` driving Exposure),
|
||||||
for item in bp.nodes() {
|
// composites opaque. The deltas vs a composite definition: param names come from
|
||||||
let id = labels.len();
|
// the factory (no alias overlay at the root); the entries are sources, not roles;
|
||||||
labels.push(match item {
|
// there is no output record (terminals are sinks) and no title.
|
||||||
BlueprintNode::Leaf(factory) => factory.label(),
|
let entries: Vec<Entry> = bp
|
||||||
BlueprintNode::Composite(c) => c.name().to_string(),
|
.sources()
|
||||||
});
|
.iter()
|
||||||
item_ids.push(id);
|
.map(|src| Entry { name: format!("source:{:?}", src.kind), targets: &src.targets })
|
||||||
}
|
.collect();
|
||||||
let mut source_ids: Vec<usize> = Vec::with_capacity(bp.sources().len());
|
let main = render_graph(
|
||||||
for src in bp.sources() {
|
bp.nodes(),
|
||||||
let id = labels.len();
|
bp.edges(),
|
||||||
labels.push(format!("source:{:?}", src.kind));
|
&entries,
|
||||||
source_ids.push(id);
|
&[], // no output bindings: a blueprint's terminals are sinks
|
||||||
}
|
ParamNames::Factory,
|
||||||
|
None, // no fan-in stub context at the root (deterministic no-op, #48)
|
||||||
// pass 2: edges — every endpoint is a single opaque node, so the slot that
|
None, // no title
|
||||||
// mattered for cluster fan-in is irrelevant here.
|
color,
|
||||||
let mut edges: Vec<(usize, usize)> = Vec::new();
|
);
|
||||||
for e in bp.edges() {
|
|
||||||
edges.push((item_ids[e.from], item_ids[e.to]));
|
|
||||||
}
|
|
||||||
for (src, &sid) in bp.sources().iter().zip(&source_ids) {
|
|
||||||
for t in &src.targets {
|
|
||||||
edges.push((sid, item_ids[t.node]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let main = render_flat(&labels, &edges, color);
|
|
||||||
|
|
||||||
// definitions: each distinct composite type, once, recursively.
|
// definitions: each distinct composite type, once, recursively.
|
||||||
let defs = collect_distinct_composites(bp);
|
let defs = collect_distinct_composites(bp);
|
||||||
@@ -76,6 +91,59 @@ pub fn render_blueprint(bp: &Blueprint, color: Color) -> String {
|
|||||||
format!("{main}\nwhere:\n\n{body}")
|
format!("{main}\nwhere:\n\n{body}")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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
|
||||||
|
/// [`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
|
||||||
|
/// interior targets. A `title` (the composite's typed signature) is prefixed when
|
||||||
|
/// `Some`. The two render-borders that differ — param-name source and fan-in stub
|
||||||
|
/// availability — are passed as `param_names` / `stub_ctx`.
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
fn render_graph(
|
||||||
|
nodes: &[BlueprintNode],
|
||||||
|
edges: &[Edge],
|
||||||
|
entries: &[Entry],
|
||||||
|
output: &[OutField],
|
||||||
|
param_names: ParamNames,
|
||||||
|
stub_ctx: Option<&Composite>,
|
||||||
|
title: Option<&str>,
|
||||||
|
color: Color,
|
||||||
|
) -> String {
|
||||||
|
let mut labels: Vec<String> = Vec::with_capacity(nodes.len());
|
||||||
|
for (i, item) in nodes.iter().enumerate() {
|
||||||
|
let base = match item {
|
||||||
|
BlueprintNode::Leaf(factory) => {
|
||||||
|
leaf_label(nodes, edges, entries, i, factory, ¶m_names, stub_ctx)
|
||||||
|
}
|
||||||
|
BlueprintNode::Composite(c) => c.name().to_string(),
|
||||||
|
};
|
||||||
|
labels.push(match output_binding(output, i) {
|
||||||
|
Some(prefix) => format!("{prefix}{base}"),
|
||||||
|
None => base,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut edge_pairs: Vec<(usize, usize)> = edges.iter().map(|e| (e.from, e.to)).collect();
|
||||||
|
for entry in entries {
|
||||||
|
let entry_id = labels.len();
|
||||||
|
labels.push(entry.name.clone());
|
||||||
|
for t in entry.targets {
|
||||||
|
edge_pairs.push((entry_id, t.node));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// outputs are folded onto their producers by output_binding above — no
|
||||||
|
// standalone output node and no producer→output edge (C23).
|
||||||
|
|
||||||
|
let flat = render_flat(&labels, &edge_pairs, color);
|
||||||
|
match title {
|
||||||
|
Some(t) => format!("{t}:\n\n{flat}"),
|
||||||
|
None => flat,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Build and render a flat (no-subgraph) ascii-dag graph from owned labels and
|
/// Build and render a flat (no-subgraph) ascii-dag graph from owned labels and
|
||||||
/// edge pairs — the same idiom `render_compilat` uses.
|
/// edge pairs — the same idiom `render_compilat` uses.
|
||||||
fn render_flat(labels: &[String], edges: &[(usize, usize)], color: Color) -> String {
|
fn render_flat(labels: &[String], edges: &[(usize, usize)], color: Color) -> String {
|
||||||
@@ -152,48 +220,122 @@ fn signature(c: &Composite) -> String {
|
|||||||
format!("{}({}) -> ({})", c.name(), params.join(", "), outs.join(", "))
|
format!("{}({}) -> ({})", c.name(), params.join(", "), outs.join(", "))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A leaf item's render label: `factory.label()` plus an optional `(...)` listing
|
/// The unified leaf-label both views render through (#48): `factory.label()`
|
||||||
/// its aliased param names, then its input-slot stubs (`#A`, `#B`, … one per wired
|
/// enriched in three value-empty ways (names, never values — C22 "structure
|
||||||
/// input slot, slot index → letter) when the leaf has more than one wired input
|
/// before"), folded into one `factory.label(...)` form:
|
||||||
/// slot. Params and stubs are `; `-separated when both present; either alone has no
|
///
|
||||||
/// separator; neither yields the bare label. A node's wired slots are the distinct
|
/// 1. A `<field> → ` consumer-side prefix per input slot fed by a **multi-output
|
||||||
/// `.slot` values targeting it across interior edges (`Edge.to == index`) and input
|
/// producer** (the headline — macd's `histogram` driving `Exposure`). Single-output
|
||||||
/// roles (`Role.targets` with `node == index`).
|
/// producers contribute nothing, so single-out graphs (sample sma_cross interior
|
||||||
fn leaf_label(c: &Composite, index: usize, factory: &LeafFactory) -> String {
|
/// and main graph) stay prefix-free. Drawn outside the `(...)`, like the
|
||||||
let params: Vec<&str> = c
|
/// `output_binding` consumer-prefix form, because ascii-dag 0.9.1 silently drops
|
||||||
.params()
|
/// an edge label it cannot place.
|
||||||
.iter()
|
/// 2. The leaf's param **names** in `(...)`. The source is [`ParamNames`]: the
|
||||||
.filter(|a| a.node == index)
|
/// composite path keeps using the alias overlay filtered to this node (so `where:`
|
||||||
.map(|a| a.name.as_str())
|
/// stays byte-identical — only aliased slots, never factory defaults); the
|
||||||
.collect();
|
/// blueprint-root path uses the factory's declared names.
|
||||||
|
/// 3. The leaf's input-slot stubs (`#Sf`, …) when it is a multi-input fan-in **and** a
|
||||||
|
/// composite context is available (`stub_ctx = Some`). At the blueprint root no
|
||||||
|
/// such context is threaded, so a top-level fan-in renders without stubs (a
|
||||||
|
/// deterministic no-op, #48) rather than duplicating the signature machinery.
|
||||||
|
///
|
||||||
|
/// Params and stubs are `; `-separated inside the `(...)` when both present; the
|
||||||
|
/// field prefix always sits outside the parens. The bare label results when none of
|
||||||
|
/// the three enrichments apply.
|
||||||
|
fn leaf_label(
|
||||||
|
nodes: &[BlueprintNode],
|
||||||
|
edges: &[Edge],
|
||||||
|
entries: &[Entry],
|
||||||
|
index: usize,
|
||||||
|
factory: &LeafFactory,
|
||||||
|
param_names: &ParamNames,
|
||||||
|
stub_ctx: Option<&Composite>,
|
||||||
|
) -> String {
|
||||||
|
let params: Vec<&str> = match param_names {
|
||||||
|
ParamNames::Aliases(aliases) => {
|
||||||
|
aliases.iter().filter(|a| a.node == index).map(|a| a.name.as_str()).collect()
|
||||||
|
}
|
||||||
|
ParamNames::Factory => factory.params().iter().map(|p| p.name.as_str()).collect(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// wired input slots: the distinct `.slot` values targeting this node across
|
||||||
|
// interior/root edges and external entries (roles or sources).
|
||||||
let mut slots: Vec<usize> = Vec::new();
|
let mut slots: Vec<usize> = Vec::new();
|
||||||
for e in c.edges() {
|
for e in edges {
|
||||||
if e.to == index && !slots.contains(&e.slot) {
|
if e.to == index && !slots.contains(&e.slot) {
|
||||||
slots.push(e.slot);
|
slots.push(e.slot);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for role in c.input_roles() {
|
for entry in entries {
|
||||||
for t in &role.targets {
|
for t in entry.targets {
|
||||||
if t.node == index && !slots.contains(&t.slot) {
|
if t.node == index && !slots.contains(&t.slot) {
|
||||||
slots.push(t.slot);
|
slots.push(t.slot);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
slots.sort_unstable();
|
slots.sort_unstable();
|
||||||
let stubs: Vec<String> = if slots.len() > 1 {
|
let stubs: Vec<String> = match stub_ctx {
|
||||||
fan_in_identifiers(c, index, &slots)
|
Some(c) if slots.len() > 1 => fan_in_identifiers(c, index, &slots),
|
||||||
} else {
|
_ => Vec::new(),
|
||||||
Vec::new()
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let parts: Vec<String> = match (params.is_empty(), stubs.is_empty()) {
|
let inner: String = match (params.is_empty(), stubs.is_empty()) {
|
||||||
(true, true) => return factory.label(),
|
(true, true) => factory.label(),
|
||||||
(false, true) => vec![params.join(", ")],
|
(false, true) => format!("{}({})", factory.label(), params.join(", ")),
|
||||||
(true, false) => vec![stubs.join(",")],
|
(true, false) => format!("{}({})", factory.label(), stubs.join(",")),
|
||||||
(false, false) => vec![params.join(", "), stubs.join(",")],
|
(false, false) => {
|
||||||
|
format!("{}({}; {})", factory.label(), params.join(", "), stubs.join(","))
|
||||||
|
}
|
||||||
};
|
};
|
||||||
format!("{}({})", factory.label(), parts.join("; "))
|
|
||||||
|
// field prefixes: for each edge feeding this leaf from a multi-output producer,
|
||||||
|
// a `<field> → ` consumer-side prefix, in slot order for determinism. A no-op for
|
||||||
|
// both current corpora's single-output interiors (so `where:` is byte-stable).
|
||||||
|
let mut fed: Vec<(usize, String)> = Vec::new();
|
||||||
|
for e in edges {
|
||||||
|
if e.to != index {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Some(field) = multi_output_field_name(nodes, e.from, e.from_field) {
|
||||||
|
fed.push((e.slot, field));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fed.sort_by_key(|(slot, _)| *slot);
|
||||||
|
if fed.is_empty() {
|
||||||
|
return inner;
|
||||||
|
}
|
||||||
|
// a per-field `<field> → ` stack, then the (possibly enriched) label.
|
||||||
|
let prefix = fed.iter().map(|(_, f)| format!("{f} \u{2192} ")).collect::<String>();
|
||||||
|
format!("{prefix}{inner}")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The driving field's *name* when producer `from` (in `nodes`) is a **multi-output**
|
||||||
|
/// producer feeding its field `from_field` into a consumer; `None` when single-output
|
||||||
|
/// (no prefix — keeps single-out graphs clean). Operates on a borrowed node slice, so
|
||||||
|
/// it serves both views uniformly (blueprint root and composite interior).
|
||||||
|
///
|
||||||
|
/// - Composite producer: multi-output iff `output().len() > 1`; the name is
|
||||||
|
/// `output()[from_field].name` (the headline — macd's `output()[2] == "histogram"`).
|
||||||
|
/// - Leaf producer: pre-build field names are unavailable (#43), so a leaf's output
|
||||||
|
/// arity is unknown here. Treat `from_field > 0` as the only structurally-certain
|
||||||
|
/// multi-output signal and fall back to the field **index** as a string; this case
|
||||||
|
/// is absent in both current corpora and must never panic.
|
||||||
|
fn multi_output_field_name(nodes: &[BlueprintNode], from: usize, from_field: usize) -> Option<String> {
|
||||||
|
match nodes.get(from)? {
|
||||||
|
BlueprintNode::Composite(c) => {
|
||||||
|
if c.output().len() > 1 {
|
||||||
|
Some(
|
||||||
|
c.output()
|
||||||
|
.get(from_field)
|
||||||
|
.map(|of| of.name.clone())
|
||||||
|
.unwrap_or_else(|| from_field.to_string()),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BlueprintNode::Leaf(_) => (from_field > 0).then(|| from_field.to_string()),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One `#…` identifier per wired slot of a fan-in leaf, in slot order.
|
/// One `#…` identifier per wired slot of a fan-in leaf, in slot order.
|
||||||
@@ -293,32 +435,26 @@ fn unique_prefix_from(sig: &str, base: usize, others: &[&String]) -> Option<Stri
|
|||||||
/// `signature` (`name(p:kind, …) -> (out, …)`); params live in the signature, not
|
/// `signature` (`name(p:kind, …) -> (out, …)`); params live in the signature, not
|
||||||
/// as marker nodes.
|
/// as marker nodes.
|
||||||
fn render_definition(c: &Composite, color: Color) -> String {
|
fn render_definition(c: &Composite, color: Color) -> String {
|
||||||
let mut labels: Vec<String> = Vec::with_capacity(c.nodes().len());
|
// the same shared graph core the main graph uses (#48); the composite-specific
|
||||||
for (i, inner) in c.nodes().iter().enumerate() {
|
// borders: param names from the alias overlay, entries from input roles, the
|
||||||
let base = match inner {
|
// output record folded as bindings, fan-in stub context available, and the typed
|
||||||
BlueprintNode::Leaf(factory) => leaf_label(c, i, factory),
|
// signature as title.
|
||||||
BlueprintNode::Composite(inner_c) => inner_c.name().to_string(),
|
let entries: Vec<Entry> = c
|
||||||
};
|
.input_roles()
|
||||||
labels.push(match output_binding(c, i) {
|
.iter()
|
||||||
Some(prefix) => format!("{prefix}{base}"),
|
.map(|role| Entry { name: role.name.clone(), targets: &role.targets })
|
||||||
None => base,
|
.collect();
|
||||||
});
|
let title = signature(c);
|
||||||
}
|
render_graph(
|
||||||
let mut edges: Vec<(usize, usize)> = Vec::new();
|
c.nodes(),
|
||||||
for e in c.edges() {
|
c.edges(),
|
||||||
edges.push((e.from, e.to));
|
&entries,
|
||||||
}
|
c.output(),
|
||||||
for role in c.input_roles() {
|
ParamNames::Aliases(c.params()),
|
||||||
let in_id = labels.len();
|
Some(c),
|
||||||
labels.push(role.name.clone());
|
Some(&title),
|
||||||
for t in &role.targets {
|
color,
|
||||||
edges.push((in_id, t.node));
|
)
|
||||||
}
|
|
||||||
}
|
|
||||||
// outputs are folded onto their producers by output_binding above — no
|
|
||||||
// standalone output node and no producer→output edge (C23).
|
|
||||||
|
|
||||||
format!("{}:\n\n{}", signature(c), render_flat(&labels, &edges, color))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The `name := ` (single) or `(n1, n2) := ` (tuple) binding prefix for interior
|
/// The `name := ` (single) or `(n1, n2) := ` (tuple) binding prefix for interior
|
||||||
@@ -326,8 +462,8 @@ fn render_definition(c: &Composite, color: Color) -> String {
|
|||||||
/// are ordered by re-exported `field` index (ties keep author order). The producer
|
/// are ordered by re-exported `field` index (ties keep author order). The producer
|
||||||
/// label follows the prefix; no standalone output node or producer→output edge is
|
/// label follows the prefix; no standalone output node or producer→output edge is
|
||||||
/// emitted (the output *is* the producer, surfaced by name — C23).
|
/// emitted (the output *is* the producer, surfaced by name — C23).
|
||||||
fn output_binding(c: &Composite, i: usize) -> Option<String> {
|
fn output_binding(output: &[OutField], i: usize) -> Option<String> {
|
||||||
let mut outs: Vec<&OutField> = c.output().iter().filter(|of| of.node == i).collect();
|
let mut outs: Vec<&OutField> = output.iter().filter(|of| of.node == i).collect();
|
||||||
if outs.is_empty() {
|
if outs.is_empty() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -387,8 +387,10 @@ mod tests {
|
|||||||
let out = graph::render_blueprint(&sample_blueprint(), graph::Color::Plain);
|
let out = graph::render_blueprint(&sample_blueprint(), graph::Color::Plain);
|
||||||
// the composite is a single opaque main-graph node, not an expanded cluster
|
// the composite is a single opaque main-graph node, not an expanded cluster
|
||||||
assert!(out.contains("[sma_cross]"), "missing opaque composite node:\n{out}");
|
assert!(out.contains("[sma_cross]"), "missing opaque composite node:\n{out}");
|
||||||
// top-level leaves render as their bare-type nodes
|
// top-level leaves render enriched (param names folded in, #48), exactly as
|
||||||
for needle in ["[Exposure]", "[SimBroker]", "[Recorder]"] {
|
// the `where:` interior leaves are — `Exposure` folds its `scale` param;
|
||||||
|
// paramless leaves (SimBroker, Recorder) stay bare.
|
||||||
|
for needle in ["[Exposure(scale)]", "[SimBroker]", "[Recorder]"] {
|
||||||
assert!(out.contains(needle), "missing {needle}:\n{out}");
|
assert!(out.contains(needle), "missing {needle}:\n{out}");
|
||||||
}
|
}
|
||||||
// a definitions section is present
|
// a definitions section is present
|
||||||
@@ -500,11 +502,11 @@ mod tests {
|
|||||||
┌───└─────┐
|
┌───└─────┐
|
||||||
↓ │
|
↓ │
|
||||||
[sma_cross] │
|
[sma_cross] │
|
||||||
└┐ │
|
│ │
|
||||||
↓ │
|
↓ └──┐
|
||||||
[Exposure] │
|
[Exposure(scale)] │
|
||||||
┌───┘────────│
|
┌──┘─────────┐ │
|
||||||
↓ ↓
|
↓ ↓──┘
|
||||||
[Recorder] [SimBroker]
|
[Recorder] [SimBroker]
|
||||||
┌─────┘
|
┌─────┘
|
||||||
↓
|
↓
|
||||||
|
|||||||
Reference in New Issue
Block a user