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:
@@ -53,6 +53,10 @@ impl Node for Add {
|
||||
self.out[0] = Scalar::F64(a[0] + b[0]);
|
||||
Some(&self.out)
|
||||
}
|
||||
|
||||
fn label(&self) -> String {
|
||||
"Add".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -36,6 +36,10 @@ impl Node for Exposure {
|
||||
self.out[0] = Scalar::F64((w[0] / self.scale).clamp(-1.0, 1.0));
|
||||
Some(&self.out)
|
||||
}
|
||||
|
||||
fn label(&self) -> String {
|
||||
format!("Exposure({})", self.scale)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -63,6 +63,10 @@ impl Node for LinComb {
|
||||
self.out[0] = Scalar::F64(acc);
|
||||
Some(&self.out)
|
||||
}
|
||||
|
||||
fn label(&self) -> String {
|
||||
"LinComb".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -55,6 +55,10 @@ impl Node for Recorder {
|
||||
let _ = self.tx.send((ctx.now(), row));
|
||||
None
|
||||
}
|
||||
|
||||
fn label(&self) -> String {
|
||||
"Recorder".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -82,6 +82,10 @@ impl Node for SimBroker {
|
||||
self.out[0] = Scalar::F64(self.cum);
|
||||
Some(&self.out)
|
||||
}
|
||||
|
||||
fn label(&self) -> String {
|
||||
format!("SimBroker({})", self.pip_size)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -43,6 +43,10 @@ impl Node for Sma {
|
||||
self.out[0] = Scalar::F64(sum / self.length as f64);
|
||||
Some(&self.out)
|
||||
}
|
||||
|
||||
fn label(&self) -> String {
|
||||
format!("SMA({})", self.length)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -86,4 +90,23 @@ mod tests {
|
||||
inputs[0].push(Scalar::F64(9.0)).unwrap();
|
||||
assert_eq!(sma.eval(Ctx::new(&inputs, Timestamp(0))), Some([Scalar::F64(9.0)].as_slice()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn labels_carry_identifying_params() {
|
||||
use crate::{Add, Exposure, LinComb, Recorder, SimBroker, Sub};
|
||||
use aura_core::{Firing, ScalarKind};
|
||||
|
||||
// the load-bearing payoff: two SMAs disambiguate by window
|
||||
assert_eq!(Sma::new(2).label(), "SMA(2)");
|
||||
assert_eq!(Sma::new(4).label(), "SMA(4)");
|
||||
// param-carrying single nodes
|
||||
assert_eq!(Exposure::new(0.5).label(), "Exposure(0.5)");
|
||||
assert_eq!(SimBroker::new(0.0001).label(), "SimBroker(0.0001)");
|
||||
// bare-kind nodes (identity is not a mis-wiring axis here, per spec)
|
||||
assert_eq!(Sub::new().label(), "Sub");
|
||||
assert_eq!(Add::new().label(), "Add");
|
||||
assert_eq!(LinComb::new(vec![1.0, -1.0]).label(), "LinComb");
|
||||
let (tx, _rx) = std::sync::mpsc::channel();
|
||||
assert_eq!(Recorder::new(&[ScalarKind::F64], Firing::Any, tx).label(), "Recorder");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,10 @@ impl Node for Sub {
|
||||
self.out[0] = Scalar::F64(a[0] - b[0]);
|
||||
Some(&self.out)
|
||||
}
|
||||
|
||||
fn label(&self) -> String {
|
||||
"Sub".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
Reference in New Issue
Block a user