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:
2026-06-05 20:56:52 +02:00
parent b01821653e
commit 0a855c3943
14 changed files with 463 additions and 7 deletions
+26
View File
@@ -65,6 +65,17 @@ pub struct NodeSchema {
pub trait Node {
fn schema(&self) -> NodeSchema;
fn eval(&mut self, ctx: Ctx<'_>) -> Option<&[Scalar]>;
/// A one-line, **non-load-bearing** render label (C23): a debug symbol for
/// tracing / graph rendering (#13), never read by the run loop and never
/// part of wiring (which is by index). Overrides SHOULD carry the node's
/// identifying params so identical node types disambiguate (`SMA(2)` vs
/// `SMA(4)`). MUST be single-line (no `\n`): ascii-dag breaks box drawing on
/// a multiline label. The default is a placeholder; every shipped node
/// overrides it. Returns an owned `String` and takes `&self`, so `Node`
/// stays object-safe and `Box<dyn Node>::label()` dispatches.
fn label(&self) -> String {
"node".to_string()
}
}
#[cfg(test)]
@@ -79,4 +90,19 @@ mod tests {
assert_eq!(b.firing, Firing::Barrier(0));
assert_ne!(a.firing, b.firing);
}
#[test]
fn default_label_is_placeholder() {
// A node that does not override label() falls back to the placeholder.
struct Bare;
impl Node for Bare {
fn schema(&self) -> NodeSchema {
NodeSchema { inputs: vec![], output: vec![] }
}
fn eval(&mut self, _ctx: Ctx<'_>) -> Option<&[Scalar]> {
None
}
}
assert_eq!(Bare.label(), "node");
}
}