feat(aura-cli): aura graph renders a wired graph as an ASCII DAG (#13)
Make a wired graph introspectable so a mis-wiring becomes visible. Two
selectable views, both authored from one built-in sample blueprint:
aura graph clustered blueprint view — composites drawn as named
ascii-dag cluster boxes (pre-inline, C9)
aura graph --compiled flat compilat view — composite boundaries dissolved
(C23); the graph the run loop actually runs
The payoff is intrinsic, param-carrying node labels: two SMAs read SMA(2) /
SMA(4), so a swapped fast/slow input reads back differently. This is realized
by a `label()` default method on the core Node trait — a non-load-bearing
render symbol the run loop never reads (wiring is by index), the symbol C23
already reserves citing #13. Recorded as a C8 refinement in the ledger.
Rejected the alternatives in brainstorm: a separate Describe trait (forces a
combined trait-object dance for a label the ledger calls non-load-bearing) and
extrinsic parallel labels (decoupled from the node, so an author can label the
slow SMA "fast" and mask the very bug the render exists to surface).
Mechanism:
- aura-core: Node::label() default method (additive, object-safe, single-line).
- aura-std: per-node overrides — SMA(n)/Exposure(s)/SimBroker(p) carry params;
Sub/Add/LinComb/Recorder are bare (their identity is not a mis-wiring axis).
- aura-engine: Composite gains an authored `name` (cluster title; dissolves at
inline) + read-only graph-as-data accessors on Blueprint/Composite. No
external dependency — the engine stays dependency-pure (C16); ascii-dag lives
only in aura-cli. The label-free index-wired compilat (C23) is unchanged.
- aura-cli: ascii-dag v0.9.1 + a graph.rs adapter (Vertical mode; labels
materialized into an owned Vec<String> that outlives the borrow-based Graph).
`aura run` is untouched (the sample duplication is dedup idea #14).
Tests: a concern-defining test (swapped sample renders != correct), structure
pins (cluster present in blueprint view, absent in compiled view), per-node
label disambiguation, and two frozen byte-goldens of the deterministic render.
The cycle-0012 bit-identical and run-sample non-regression tests stay green.
Verified by the orchestrator (not the agent report): cargo build --workspace
clean; cargo test --workspace all green; cargo clippy --workspace --all-targets
-D warnings clean; both views run live and render as expected.
closes #13
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
//! The `aura graph` ASCII-DAG adapter (#13): turns the engine's graph-as-data
|
||||
//! (C9) into an `ascii_dag::Graph` rendered to a `String`. Two views:
|
||||
//! `render_blueprint` draws composites as named cluster boxes (pre-inline);
|
||||
//! `render_compilat` draws the flat post-inline graph (boundaries dissolved,
|
||||
//! C23). Rendering reads structure + node `label()`s only — never `eval`.
|
||||
//!
|
||||
//! ascii-dag borrows its node/subgraph labels as `&'a str`, so each function
|
||||
//! first materializes the owned label `String`s (which outlive the `Graph`),
|
||||
//! then borrows into them. `RenderMode::Vertical` is mandatory: Horizontal
|
||||
//! collapses a fan-out onto one path.
|
||||
|
||||
use ascii_dag::graph::{Graph, RenderMode};
|
||||
use aura_core::Node;
|
||||
use aura_engine::{Blueprint, BlueprintNode, Edge, SourceSpec, Target};
|
||||
|
||||
/// How one top-level blueprint item maps into display nodes (blueprint view).
|
||||
enum ItemDisplay {
|
||||
/// A leaf is one display node at this id.
|
||||
Leaf(usize),
|
||||
/// A composite is a cluster of interior leaf display ids; its output port and
|
||||
/// input roles resolve edges crossing its boundary.
|
||||
Composite {
|
||||
interior_ids: Vec<usize>,
|
||||
output_interior: usize,
|
||||
input_roles: Vec<Vec<Target>>,
|
||||
},
|
||||
}
|
||||
|
||||
/// The display id an edge *from* this item originates at (its producer).
|
||||
fn producer_id(d: &ItemDisplay) -> usize {
|
||||
match d {
|
||||
ItemDisplay::Leaf(id) => *id,
|
||||
ItemDisplay::Composite { interior_ids, output_interior, .. } => interior_ids[*output_interior],
|
||||
}
|
||||
}
|
||||
|
||||
/// The display id(s) an edge *into* this item at `slot` reaches (its consumers).
|
||||
/// A composite input role fans into several interior targets.
|
||||
fn consumer_ids(d: &ItemDisplay, slot: usize) -> Vec<usize> {
|
||||
match d {
|
||||
ItemDisplay::Leaf(id) => vec![*id],
|
||||
ItemDisplay::Composite { interior_ids, input_roles, .. } => {
|
||||
input_roles[slot].iter().map(|t| interior_ids[t.node]).collect()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Blueprint view: composites become labelled cluster boxes (pre-inline, C9).
|
||||
pub fn render_blueprint(bp: &Blueprint) -> String {
|
||||
let mut labels: Vec<String> = Vec::new();
|
||||
let mut sg_names: Vec<String> = Vec::new();
|
||||
let mut memberships: Vec<(usize, Vec<usize>)> = Vec::new();
|
||||
let mut edges: Vec<(usize, usize)> = Vec::new();
|
||||
let mut item_display: Vec<ItemDisplay> = Vec::with_capacity(bp.nodes().len());
|
||||
|
||||
// pass 1: assign display ids + labels; open a subgraph per composite; record
|
||||
// interior edges (the interior ids are in hand here).
|
||||
for item in bp.nodes() {
|
||||
match item {
|
||||
BlueprintNode::Leaf(node) => {
|
||||
let id = labels.len();
|
||||
labels.push(node.label());
|
||||
item_display.push(ItemDisplay::Leaf(id));
|
||||
}
|
||||
BlueprintNode::Composite(c) => {
|
||||
let mut interior_ids = Vec::with_capacity(c.nodes().len());
|
||||
for inner in c.nodes() {
|
||||
match inner {
|
||||
BlueprintNode::Leaf(node) => {
|
||||
let id = labels.len();
|
||||
labels.push(node.label());
|
||||
interior_ids.push(id);
|
||||
}
|
||||
BlueprintNode::Composite(_) => unimplemented!(
|
||||
"cycle 0013 renders leaf-interior composites (the built-in \
|
||||
sample); nested-composite cluster rendering is a follow-up"
|
||||
),
|
||||
}
|
||||
}
|
||||
for e in c.edges() {
|
||||
edges.push((interior_ids[e.from], interior_ids[e.to]));
|
||||
}
|
||||
let sg = sg_names.len();
|
||||
sg_names.push(c.name().to_string());
|
||||
memberships.push((sg, interior_ids.clone()));
|
||||
item_display.push(ItemDisplay::Composite {
|
||||
interior_ids,
|
||||
output_interior: c.output().node,
|
||||
input_roles: c.input_roles().to_vec(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sources as producer display nodes (unnamed in the data model -> label by kind)
|
||||
let mut source_ids: Vec<usize> = Vec::with_capacity(bp.sources().len());
|
||||
for src in bp.sources() {
|
||||
let id = labels.len();
|
||||
labels.push(format!("source:{:?}", src.kind));
|
||||
source_ids.push(id);
|
||||
}
|
||||
|
||||
// top-level edges, resolved through composite boundaries
|
||||
for e in bp.edges() {
|
||||
let from = producer_id(&item_display[e.from]);
|
||||
for to in consumer_ids(&item_display[e.to], e.slot) {
|
||||
edges.push((from, to));
|
||||
}
|
||||
}
|
||||
// source -> target edges
|
||||
for (src, &sid) in bp.sources().iter().zip(&source_ids) {
|
||||
for t in &src.targets {
|
||||
for to in consumer_ids(&item_display[t.node], t.slot) {
|
||||
edges.push((sid, to));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// build the borrowed-label Graph (labels + sg_names are final & owned)
|
||||
let mut g = Graph::with_mode(RenderMode::Vertical);
|
||||
for (id, l) in labels.iter().enumerate() {
|
||||
g.add_node(id, l);
|
||||
}
|
||||
let sg_ids: Vec<usize> = sg_names.iter().map(|n| g.add_subgraph(n)).collect();
|
||||
for (sg, members) in &memberships {
|
||||
g.put_nodes(members).inside(sg_ids[*sg]).expect("valid subgraph placement");
|
||||
}
|
||||
for (from, to) in edges {
|
||||
g.add_edge(from, to, None);
|
||||
}
|
||||
g.render()
|
||||
}
|
||||
|
||||
/// Compiled view: the flat post-inline graph (no clusters; boundaries dissolved,
|
||||
/// C23). Each `Box<dyn Node>` labels itself; node display id = node index.
|
||||
pub fn render_compilat(nodes: &[Box<dyn Node>], sources: &[SourceSpec], edges: &[Edge]) -> String {
|
||||
let mut labels: Vec<String> = nodes.iter().map(|n| n.label()).collect();
|
||||
let source_base = labels.len();
|
||||
for src in sources {
|
||||
labels.push(format!("source:{:?}", src.kind));
|
||||
}
|
||||
|
||||
let mut g = Graph::with_mode(RenderMode::Vertical);
|
||||
for (id, l) in labels.iter().enumerate() {
|
||||
g.add_node(id, l);
|
||||
}
|
||||
for e in edges {
|
||||
g.add_edge(e.from, e.to, None);
|
||||
}
|
||||
for (i, src) in sources.iter().enumerate() {
|
||||
for t in &src.targets {
|
||||
g.add_edge(source_base + i, t.node, None);
|
||||
}
|
||||
}
|
||||
g.render()
|
||||
}
|
||||
Reference in New Issue
Block a user