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:
@@ -52,6 +52,7 @@ impl BlueprintNode {
|
||||
/// interior edges (local indices), input roles (role `r` fans into the interior
|
||||
/// targets `input_roles[r]`), and the one exposed output port.
|
||||
pub struct Composite {
|
||||
name: String,
|
||||
nodes: Vec<BlueprintNode>,
|
||||
edges: Vec<Edge>,
|
||||
input_roles: Vec<Vec<Target>>,
|
||||
@@ -59,15 +60,39 @@ pub struct Composite {
|
||||
}
|
||||
|
||||
impl Composite {
|
||||
/// Build a composite from its interior items, interior edges (local indices),
|
||||
/// input roles, and output port.
|
||||
/// Build a composite from its authored name, interior items, interior edges
|
||||
/// (local indices), input roles, and output port. The `name` is a
|
||||
/// non-load-bearing render symbol (the cluster title for #13); it does not
|
||||
/// reach the compilat (the boundary dissolves at inline, C23).
|
||||
pub fn new(
|
||||
name: impl Into<String>,
|
||||
nodes: Vec<BlueprintNode>,
|
||||
edges: Vec<Edge>,
|
||||
input_roles: Vec<Vec<Target>>,
|
||||
output: OutPort,
|
||||
) -> Self {
|
||||
Self { nodes, edges, input_roles, output }
|
||||
Self { name: name.into(), nodes, edges, input_roles, output }
|
||||
}
|
||||
|
||||
/// The authored render name (cluster title, #13). Non-load-bearing.
|
||||
pub fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
/// The interior blueprint items (read-only graph-as-data, C9).
|
||||
pub fn nodes(&self) -> &[BlueprintNode] {
|
||||
&self.nodes
|
||||
}
|
||||
/// The interior edges (local indices).
|
||||
pub fn edges(&self) -> &[Edge] {
|
||||
&self.edges
|
||||
}
|
||||
/// The input roles: role `r` fans into `input_roles()[r]` interior targets.
|
||||
pub fn input_roles(&self) -> &[Vec<Target>] {
|
||||
&self.input_roles
|
||||
}
|
||||
/// The single exposed output port.
|
||||
pub fn output(&self) -> OutPort {
|
||||
self.output
|
||||
}
|
||||
|
||||
/// The derived interface the enclosing graph wires against: input role `r`'s
|
||||
@@ -119,6 +144,19 @@ impl Blueprint {
|
||||
Self { nodes, sources, edges }
|
||||
}
|
||||
|
||||
/// The top-level blueprint items (read-only graph-as-data, C9).
|
||||
pub fn nodes(&self) -> &[BlueprintNode] {
|
||||
&self.nodes
|
||||
}
|
||||
/// The declared sources.
|
||||
pub fn sources(&self) -> &[SourceSpec] {
|
||||
&self.sources
|
||||
}
|
||||
/// The top-level edges (blueprint-level indices).
|
||||
pub fn edges(&self) -> &[Edge] {
|
||||
&self.edges
|
||||
}
|
||||
|
||||
/// Lower to the flat compilat: inline every composite (recursive), offset
|
||||
/// interior indices, rewrite edges, and fan input roles out. The run loop and
|
||||
/// `bootstrap`'s data model are unchanged; the lowered compilat is wired by raw
|
||||
@@ -201,7 +239,9 @@ fn inline_composite(
|
||||
flat_nodes: &mut Vec<Box<dyn Node>>,
|
||||
flat_edges: &mut Vec<Edge>,
|
||||
) -> Result<ItemLowering, CompileError> {
|
||||
let Composite { nodes, edges, input_roles, output } = c;
|
||||
// `name` is the non-load-bearing render symbol (#13); it dissolves at inline
|
||||
// (C23 — the boundary does not reach the compilat), so it is not destructured.
|
||||
let Composite { name: _, nodes, edges, input_roles, output } = c;
|
||||
let item_count = nodes.len();
|
||||
|
||||
// the output port must name an in-range interior item (field range checked
|
||||
@@ -354,6 +394,7 @@ mod tests {
|
||||
// one interior node (Join2: 2 f64 inputs, 1 f64 output); two roles, each
|
||||
// feeding one interior slot; output port = the Join2 output field 0.
|
||||
let c = Composite::new(
|
||||
"c",
|
||||
vec![BlueprintNode::Leaf(Box::new(Join2 { out: [Scalar::F64(0.0)] }))],
|
||||
vec![],
|
||||
vec![
|
||||
@@ -431,6 +472,7 @@ mod tests {
|
||||
/// the SMA-cross shape.
|
||||
fn fan_composite() -> Composite {
|
||||
Composite::new(
|
||||
"fan",
|
||||
vec![pass1(), pass1(), join2()],
|
||||
vec![
|
||||
Edge { from: 0, to: 2, slot: 0, from_field: 0 },
|
||||
@@ -479,6 +521,7 @@ mod tests {
|
||||
// Pass1 slots; the inner Join2 lands at flat index 2.
|
||||
let inner = fan_composite();
|
||||
let outer = Composite::new(
|
||||
"outer",
|
||||
vec![BlueprintNode::Composite(inner)],
|
||||
vec![],
|
||||
vec![vec![Target { node: 0, slot: 0 }]],
|
||||
@@ -511,6 +554,7 @@ mod tests {
|
||||
fn bad_interior_index_rejected() {
|
||||
// interior edge references interior node 9, which does not exist
|
||||
let c = Composite::new(
|
||||
"c",
|
||||
vec![pass1()],
|
||||
vec![Edge { from: 0, to: 9, slot: 0, from_field: 0 }],
|
||||
vec![vec![Target { node: 0, slot: 0 }]],
|
||||
@@ -525,6 +569,7 @@ mod tests {
|
||||
fn role_kind_mismatch_rejected() {
|
||||
// role 0 fans into a Pass1 f64 slot AND a SinkI64 i64 slot -> mismatch
|
||||
let c = Composite::new(
|
||||
"c",
|
||||
vec![pass1(), BlueprintNode::Leaf(Box::new(SinkI64))],
|
||||
vec![],
|
||||
vec![vec![Target { node: 0, slot: 0 }, Target { node: 1, slot: 0 }]],
|
||||
@@ -539,6 +584,7 @@ mod tests {
|
||||
fn output_port_out_of_range_rejected() {
|
||||
// output names field 5 of a node whose output has one field
|
||||
let c = Composite::new(
|
||||
"c",
|
||||
vec![pass1()],
|
||||
vec![],
|
||||
vec![vec![Target { node: 0, slot: 0 }]],
|
||||
@@ -629,6 +675,7 @@ mod tests {
|
||||
/// output (the fast-minus-slow spread). Interior wired with raw local indices.
|
||||
fn sma_cross(fast: usize, slow: usize) -> Composite {
|
||||
Composite::new(
|
||||
"sma_cross",
|
||||
vec![Sma::new(fast).into(), Sma::new(slow).into(), Sub::new().into()],
|
||||
vec![
|
||||
Edge { from: 0, to: 2, slot: 0, from_field: 0 },
|
||||
|
||||
Reference in New Issue
Block a user