feat(aura-cli): composite definition renders as a typed signature
Refines #41's blueprint-definition render. The [param:<name>] marker nodes #41 added bloated the ASCII MACD graph (three extra nodes + fan-in edges); this moves the interface into the title line and folds names into the leaf labels that already exist: - title is the composite's typed signature, e.g. macd(fast:i64, slow:i64, signal:i64) -> (macd, signal, histogram) (param kinds from the aliased leaf's factory params via a new ScalarKind->lowercase kind_str; output NAMES only — output kinds need a pre-build factory interface, deferred to #43) - leaf labels fold aliased param names: [EMA] -> [EMA(fast)] - unnamed interior input slots render as ordered stubs: [Sub] -> [Sub(#A,#B)] (count/order derived render-side from c.edges() + c.input_roles(), no engine schema needed; arity-1 nodes get no stub) - outputs de-prefixed: [out:macd] -> [macd]; the [param:*] marker nodes are gone - two stale doc comments corrected: render_definition's [param:] prose and node.rs LeafFactory::label (the renderer went flat in 0017, so the cited ascii-dag cluster-garble rationale no longer applies — wide labels are safe; the factory label stays bare because alias names are composite-level) Render-only: no engine/ParamAlias/param_space change. compiled_view_golden byte-identical (C23 guard — verified untouched in the diff); aura run --macd deterministic and unchanged (total_pips 0.1637945563898923, 3 sign flips). The title format change repinned five .matches("name:")->.matches("name(") definition-count asserts (sma_cross/outer/inner/dup/macd); the stub separator is "," (no space) to match the [Sub(#A,#B)] surface. Verification (orchestrator-run): cargo build/test/clippy --workspace -D warnings all green; the redesigned MACD render confirmed by eye. refs #41 #43
This commit is contained in:
@@ -14,7 +14,7 @@
|
||||
//! subgraph layout mis-centres wide sibling labels, the flat layout does not.
|
||||
|
||||
use ascii_dag::graph::{Graph, RenderMode};
|
||||
use aura_core::Node;
|
||||
use aura_core::{LeafFactory, Node, ScalarKind};
|
||||
use aura_engine::{Blueprint, BlueprintNode, Composite, Edge, SourceSpec};
|
||||
|
||||
/// Blueprint view: the authored structure (#38). A flat main graph wires the
|
||||
@@ -98,16 +98,98 @@ fn collect_distinct_composites(bp: &Blueprint) -> Vec<&Composite> {
|
||||
out
|
||||
}
|
||||
|
||||
/// Render one composite's interior as a flat graph: interior leaves as `[type]`,
|
||||
/// nested composites as opaque `[name]`, plus an `[in:<name>]` entry marker per
|
||||
/// input role (wired to its interior targets), a `[param:<name>]` marker per param
|
||||
/// alias (wired to the leaf it relabels), and an `[out:<name>]` marker per
|
||||
/// re-exported output field (wired from its producer). Prefixed `"<name>:\n"`.
|
||||
/// `ScalarKind` as a lowercase type string for a signature (`i64`/`f64`/`bool`/
|
||||
/// `timestamp`). The derived `Debug` gives PascalCase (`I64`), so this is explicit.
|
||||
fn kind_str(kind: ScalarKind) -> &'static str {
|
||||
match kind {
|
||||
ScalarKind::I64 => "i64",
|
||||
ScalarKind::F64 => "f64",
|
||||
ScalarKind::Bool => "bool",
|
||||
ScalarKind::Timestamp => "timestamp",
|
||||
}
|
||||
}
|
||||
|
||||
/// The composite's typed signature for the definition title:
|
||||
/// `name(p1:kind, …) -> (o1, …)`. Param kinds come from the aliased interior leaf's
|
||||
/// declared params; output **names only** (kinds need a pre-build factory interface,
|
||||
/// #43). An empty alias list renders `name()`. Total: a malformed alias falls back
|
||||
/// to `?` rather than panicking (compile is the validator, #41).
|
||||
fn signature(c: &Composite) -> String {
|
||||
let params: Vec<String> = c
|
||||
.params()
|
||||
.iter()
|
||||
.map(|a| {
|
||||
let kind = c
|
||||
.nodes()
|
||||
.get(a.node)
|
||||
.and_then(|n| match n {
|
||||
BlueprintNode::Leaf(f) => f.params().get(a.slot).map(|p| kind_str(p.kind)),
|
||||
BlueprintNode::Composite(_) => None,
|
||||
})
|
||||
.unwrap_or("?");
|
||||
format!("{}:{}", a.name, kind)
|
||||
})
|
||||
.collect();
|
||||
let outs: Vec<String> = c.output().iter().map(|of| of.name.clone()).collect();
|
||||
format!("{}({}) -> ({})", c.name(), params.join(", "), outs.join(", "))
|
||||
}
|
||||
|
||||
/// A leaf item's render label: `factory.label()` plus an optional `(...)` listing
|
||||
/// its aliased param names, then its input-slot stubs (`#A`, `#B`, … one per wired
|
||||
/// input slot, slot index → letter) when the leaf has more than one wired input
|
||||
/// 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
|
||||
/// `.slot` values targeting it across interior edges (`Edge.to == index`) and input
|
||||
/// roles (`Role.targets` with `node == index`).
|
||||
fn leaf_label(c: &Composite, index: usize, factory: &LeafFactory) -> String {
|
||||
let params: Vec<&str> = c
|
||||
.params()
|
||||
.iter()
|
||||
.filter(|a| a.node == index)
|
||||
.map(|a| a.name.as_str())
|
||||
.collect();
|
||||
|
||||
let mut slots: Vec<usize> = Vec::new();
|
||||
for e in c.edges() {
|
||||
if e.to == index && !slots.contains(&e.slot) {
|
||||
slots.push(e.slot);
|
||||
}
|
||||
}
|
||||
for role in c.input_roles() {
|
||||
for t in &role.targets {
|
||||
if t.node == index && !slots.contains(&t.slot) {
|
||||
slots.push(t.slot);
|
||||
}
|
||||
}
|
||||
}
|
||||
slots.sort_unstable();
|
||||
let stubs: Vec<String> = if slots.len() > 1 {
|
||||
slots.iter().map(|s| format!("#{}", (b'A' + *s as u8) as char)).collect()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
let parts: Vec<String> = match (params.is_empty(), stubs.is_empty()) {
|
||||
(true, true) => return factory.label(),
|
||||
(false, true) => vec![params.join(", ")],
|
||||
(true, false) => vec![stubs.join(",")],
|
||||
(false, false) => vec![params.join(", "), stubs.join(",")],
|
||||
};
|
||||
format!("{}({})", factory.label(), parts.join("; "))
|
||||
}
|
||||
|
||||
/// Render one composite's interior as a flat graph: interior leaves as
|
||||
/// `[type(param…; #slot…)]` (aliased param names + ordered input-slot stubs folded
|
||||
/// in via `leaf_label`), nested composites as opaque `[name]`, an `[in:<name>]`
|
||||
/// entry marker per input role (wired to its interior targets), and an `[<name>]`
|
||||
/// node per re-exported output field (wired from its producer). The title line is
|
||||
/// the composite's typed `signature` (`name(p:kind, …) -> (out, …)`); params live
|
||||
/// in the signature, not as marker nodes.
|
||||
fn render_definition(c: &Composite) -> String {
|
||||
let mut labels: Vec<String> = Vec::with_capacity(c.nodes().len());
|
||||
for inner in c.nodes() {
|
||||
for (i, inner) in c.nodes().iter().enumerate() {
|
||||
labels.push(match inner {
|
||||
BlueprintNode::Leaf(factory) => factory.label(),
|
||||
BlueprintNode::Leaf(factory) => leaf_label(c, i, factory),
|
||||
BlueprintNode::Composite(inner_c) => inner_c.name().to_string(),
|
||||
});
|
||||
}
|
||||
@@ -122,18 +204,13 @@ fn render_definition(c: &Composite) -> String {
|
||||
edges.push((in_id, t.node));
|
||||
}
|
||||
}
|
||||
for a in c.params() {
|
||||
let p_id = labels.len();
|
||||
labels.push(format!("param:{}", a.name));
|
||||
edges.push((p_id, a.node));
|
||||
}
|
||||
for of in c.output() {
|
||||
let out_id = labels.len();
|
||||
labels.push(format!("out:{}", of.name));
|
||||
labels.push(of.name.clone());
|
||||
edges.push((of.node, out_id));
|
||||
}
|
||||
|
||||
format!("{}:\n{}", c.name(), render_flat(&labels, &edges))
|
||||
format!("{}:\n{}", signature(c), render_flat(&labels, &edges))
|
||||
}
|
||||
|
||||
/// Compiled view: the flat post-inline graph (no clusters; boundaries dissolved,
|
||||
|
||||
+19
-13
@@ -390,8 +390,8 @@ mod tests {
|
||||
fn blueprint_view_defines_each_composite_once() {
|
||||
let out = graph::render_blueprint(&sample_blueprint());
|
||||
// the sma_cross body is defined exactly once, with its interior + ports
|
||||
assert_eq!(out.matches("sma_cross:").count(), 1, "definition not rendered once:\n{out}");
|
||||
for needle in ["[SMA]", "[Sub]", "[in:price]", "[out:cross]"] {
|
||||
assert_eq!(out.matches("sma_cross(").count(), 1, "definition not rendered once:\n{out}");
|
||||
for needle in ["[SMA]", "[Sub(#A,#B)]", "[in:price]", "[cross]"] {
|
||||
assert!(out.contains(needle), "missing {needle} in definition:\n{out}");
|
||||
}
|
||||
}
|
||||
@@ -425,8 +425,8 @@ mod tests {
|
||||
// outer shows the inner composite as an opaque node, and both get a definition
|
||||
assert!(out.contains("[outer]"), "missing opaque outer node:\n{out}");
|
||||
assert!(out.contains("[inner]"), "inner must be opaque inside outer's definition:\n{out}");
|
||||
assert_eq!(out.matches("outer:").count(), 1, "outer defined once:\n{out}");
|
||||
assert_eq!(out.matches("inner:").count(), 1, "inner defined once:\n{out}");
|
||||
assert_eq!(out.matches("outer(").count(), 1, "outer defined once:\n{out}");
|
||||
assert_eq!(out.matches("inner(").count(), 1, "inner defined once:\n{out}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -449,7 +449,7 @@ mod tests {
|
||||
);
|
||||
let out = graph::render_blueprint(&bp);
|
||||
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}");
|
||||
assert_eq!(out.matches("dup(").count(), 1, "body defined once:\n{out}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -503,17 +503,17 @@ mod tests {
|
||||
|
||||
where:
|
||||
|
||||
sma_cross:
|
||||
sma_cross() -> (cross):
|
||||
[in:price]
|
||||
┌───└───┐
|
||||
↓ ↓
|
||||
[SMA] [SMA]
|
||||
└───┌───┘
|
||||
↓
|
||||
[Sub]
|
||||
[Sub(#A,#B)]
|
||||
│
|
||||
↓
|
||||
[out:cross]
|
||||
[cross]
|
||||
|
||||
|
||||
"#;
|
||||
@@ -578,12 +578,18 @@ sma_cross:
|
||||
// defines its EMA-of-EMA interior once under `where:`.
|
||||
let out = graph::render_blueprint(&macd_blueprint());
|
||||
assert!(out.contains("[macd]"), "missing opaque macd node:\n{out}");
|
||||
assert_eq!(out.matches("macd:").count(), 1, "macd defined once:\n{out}");
|
||||
assert!(out.contains("[EMA]"), "macd interior must show EMA leaves:\n{out}");
|
||||
assert_eq!(out.matches("macd(").count(), 1, "macd defined once:\n{out}");
|
||||
assert!(
|
||||
out.contains("macd(fast:i64, slow:i64, signal:i64) -> (macd, signal, histogram)"),
|
||||
"typed signature line: {out}"
|
||||
);
|
||||
assert!(out.contains("[EMA(fast)]"), "fast EMA folds its param: {out}");
|
||||
assert!(out.contains("[EMA(slow)]"), "slow EMA folds its param: {out}");
|
||||
assert!(out.contains("[EMA(signal)]"), "signal EMA folds its param: {out}");
|
||||
assert!(out.contains("[Sub(#A,#B)]"), "Sub shows its two ordered inputs: {out}");
|
||||
assert!(out.contains("[in:price]"), "named MACD input role: {out}");
|
||||
assert!(out.contains("[param:fast]"), "aliased fast length: {out}");
|
||||
assert!(out.contains("[param:slow]"), "aliased slow length: {out}");
|
||||
assert!(out.contains("[param:signal]"), "aliased signal length: {out}");
|
||||
assert!(!out.contains("[param:"), "param marker nodes removed: {out}");
|
||||
assert!(!out.contains("[out:"), "output prefix dropped: {out}");
|
||||
}
|
||||
|
||||
/// E2E acceptance (#41 / spec 0019, the worked example): the real MACD strategy
|
||||
|
||||
@@ -91,14 +91,15 @@ impl LeafFactory {
|
||||
(self.build)(params)
|
||||
}
|
||||
/// The param-generic render label for the blueprint view (C22 "structure
|
||||
/// before"): just the node type, e.g. `SMA`. A value-empty recipe has no
|
||||
/// values to show; the tunable knobs are surfaced by `Blueprint::param_space`,
|
||||
/// not in the graph. The label stays the bare type because the `ascii-dag`
|
||||
/// renderer writes a label verbatim on one line (no wrapping) and overlaps two
|
||||
/// wide sibling boxes inside a cluster subgraph — appending the knob names
|
||||
/// (`SMA(length)`) would garble the blueprint view, and domain labels grow
|
||||
/// unboundedly wide. The compiled view labels the built node valued (`SMA(2)`)
|
||||
/// via `Node::label`, unaffected.
|
||||
/// before"): just the node type, e.g. `SMA`. A value-empty recipe has no values
|
||||
/// to show; the tunable knobs are surfaced by `Blueprint::param_space`. The
|
||||
/// factory label stays the bare type because alias / handle names are a
|
||||
/// *composite-level* concept — the renderer folds them into the leaf label at
|
||||
/// the composite boundary (`render_definition`'s `leaf_label`), where the
|
||||
/// `(node, slot)` → name mapping lives, not on the standalone factory. (Wide
|
||||
/// labels are safe: both `aura graph` views are flat since cycle 0017 — no
|
||||
/// cluster subgraph, so no sibling-overlap garble.) The compiled view labels the
|
||||
/// built node valued (`SMA(2)`) via `Node::label`, unaffected.
|
||||
pub fn label(&self) -> String {
|
||||
self.name.to_string()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user