fieldtest: milestone construction-layer — 4 examples, 7 findings

End-to-end milestone fieldtest (closing gate) for the "Construction layer"
milestone (#12 composites + #13 aura graph render). Four real downstream tasks
authored against the PUBLIC interface only (ledger + doc-comments + re-exports
+ the aura CLI), under fieldtests/milestone-construction-layer/:
  mc_1 — author a named sma_cross composite, build a Blueprint, compile +
         bootstrap + run (12 populated equity rows);
  mc_2 — correct vs fast/slow-swapped cross: labels + graph-as-data differ
         observably (the headline mis-wire-visible property);
  mc_3 — composite-nested-in-composite inlines and runs;
  mc_4 — walk the built graph via the read-only accessors, no engine internals.

Roll-up: friction_found, NO bugs. The milestone delivers its core promise —
fractal authoring -> compile -> bootstrap -> run, introspection as graph-as-data,
and param-carrying labels that make a mis-wire readable. None of the findings
block the gate.

Findings filed to the forward queue (not fixed here):
- #28 (feature) — the headline friction: `aura graph` renders only the built-in
  sample and the render adapter is CLI-private, so a consumer can't render their
  OWN graph. Subsumes the reachability of #26 (the nested-render panic is
  structurally unreachable until the render is parameterizable). Likely next
  (consumer-project / World) milestone.
- #29 (idea) — aura-engine doesn't re-export the scalar vocabulary (ScalarKind
  etc.) a graph-builder needs; one-crate ergonomics tidy.
- #16 (comment) — `aura graph` arg policy (help / unknown-arg) should unify with
  the still-open `aura run` strictness question.

The fieldtester read no implementation source; all artefacts are the consumer
crate + the two live render captures (render_clustered.txt / render_compiled.txt,
byte-identical across runs).
This commit is contained in:
2026-06-05 22:38:39 +02:00
parent d97fadaafa
commit 7e195f66b7
9 changed files with 771 additions and 0 deletions
@@ -0,0 +1,246 @@
# Fieldtest — milestone "Construction layer" — 2026-06-05
**Status:** Draft — awaiting orchestrator triage
**Author:** fieldtester (dispatched by fieldtest skill, milestone-scope variant)
**Scope mode:** MILESTONE (closing functional gate), not per-cycle.
## Scope
The "Construction layer" milestone (cycles 0012 composites + 0013 render)
promises that a downstream researcher can **build a graph fractally from a Rust
builder** — author leaf nodes and reusable *named composites* (a sub-graph
exposing one output), nest them arbitrarily, and `compile()` / `bootstrap()` the
param-generic `Blueprint` down to the flat, deterministic runnable instance
(C9/C19/C23). They can **introspect** the built graph as data and **render** it
as an ASCII DAG (`aura graph` clustered authored view, `aura graph --compiled`
flat inlined view) so the structure — and any mis-wiring — is visible
(C14/C22, #13). The composite boundary dissolves at inline (C23); a
composite-authored graph runs bit-identical to the hand-wired flat equivalent
(C1).
This fieldtest derives four end-to-end scenarios top-down from that promise, one
per carrier axis (ad), and drives them from the **public interface only**: the
design ledger (`docs/design/INDEX.md`), the crate doc-comments (rendered via
`cargo doc --no-deps`), the public re-export lists, and the `aura` CLI. **No
`crates/*/src` was read.** The construction-layer API was discovered entirely
from the ledger + rustdoc. The artefact under test was built from HEAD
(`cargo build --workspace`; the consumer crate compiled against the current
path-dep'd engine crates; the CLI driven via `cargo run -p aura-cli`, so HEAD is
always what ran).
## Examples
### fieldtests/milestone-construction-layer/mc_1_composite_build_run.rs — author + compile + run a named composite
- A standalone consumer crate (own `[workspace]`, path-deps on the engine
crates — the shape a C16 project takes) authors an SMA(2)/SMA(4)-cross as a
reusable named `Composite` (`sma_cross`, one price role fanning into both
SMAs, Sub's output as the exposed port), wires it into a harness `Blueprint`
(`source → composite → Exposure(0.5) → SimBroker(1e-4) → Recorder`),
`bootstrap()`s, and `run()`s it over a synthetic price ramp.
- Fits the milestone: this is the headline authoring story — build a graph
fractally from a Rust builder via a named composite, then compile/bootstrap/run
it.
- Outcome: built? **yes, first try.** ran? yes. matched expected? **yes** — 12
recorded equity rows, non-degenerate (`equity_pips` ramps 0 → 11.5 → 6.5),
the composite-authored harness produces a populated trace.
### fieldtests/milestone-construction-layer/mc_2_miswire_render.rs — a fast/slow swap is observable in graph-as-data + labels
- Builds the `sma_cross` composite twice — correct (fast→Sub.slot0,
slow→Sub.slot1) and swapped — and asserts the per-node `label()`s carry the
disambiguating params (`SMA(2)` / `SMA(4)`) and that the interior edge table
**differs** between the two. The property under test is the one the render
relies on: if a swap produced identical graph-as-data, no renderer could
surface it.
- Fits the milestone: the headline promise is "the render visibly surfaces the
mistake." This verifies the consumer-side substrate of that promise (labels +
edge table), complementing the `aura graph` CLI render exercised below.
- Outcome: built? yes. ran? yes. matched expected? **yes** — labels read
`["SMA(2)", "SMA(4)", "Sub"]`; correct edges `[{0→2.s0},{1→2.s1}]` vs swapped
`[{1→2.s0},{0→2.s1}]` — observably different.
### fieldtests/milestone-construction-layer/mc_3_nested_composite.rs — composite-inside-composite inlines, runs
- Nests the `sma_cross` composite inside an outer `strategy` composite (which
maps it through `Exposure(0.5)`), wires `strategy → SimBroker → Recorder`,
`bootstrap()`s and `run()`s.
- Fits the milestone: "nest composites arbitrarily" + "the compiled/flat view
handles nesting." This verifies the recursive-inline run-path from the
consumer side (the known sharp edge #26 is about the *clustered render*, not
the run path — see findings).
- Outcome: built? yes. ran? yes. matched expected? **yes** — the nested
composite recursively inlined to a flat runnable instance; 12 recorded rows.
### fieldtests/milestone-construction-layer/mc_4_introspect_graph.rs — walk the graph-as-data via read-only accessors
- Builds a `Blueprint` (composite + Exposure) and walks it purely through the
public accessors: `Blueprint::nodes()/sources()/edges()` and
`Composite::name()/nodes()/edges()/input_roles()/output()`, plus `label()` on
each boxed leaf, recursing into the composite interior.
- Fits the milestone: "introspect the built graph as data" (C9/C14) — can a
consumer walk it without engine internals?
- Outcome: built? yes (after one import fix — see finding). ran? yes. matched
expected? **yes** — the full interior (3 items, 1 role fanning to 2 targets,
Sub's output port, 2 interior edges) is reachable through the public accessors
alone.
### Render artefacts (CLI, built-in sample) — render_clustered.txt / render_compiled.txt
- `aura graph` and `aura graph --compiled` run live (via `cargo run -p
aura-cli`), captured as artefacts. Both render the engine's **built-in sample
blueprint** (the same SMA-cross harness). Byte-identical across two runs (C1
for the render).
## Findings
### [working] The full composite authoring → compile → bootstrap → run thread is authorable from the public interface
- Examples: mc_1, mc_3.
- What happened: the entire construction-layer authoring story —
`Composite::new(name, nodes, edges, input_roles, output)`,
`BlueprintNode::from(node)` / `BlueprintNode::Composite(..)`,
`Blueprint::new(..).bootstrap()`, `Harness::run` — compiled on the **first
attempt** from rustdoc signatures + the ledger (C9/C19/C23) alone, for both a
single composite and a composite-in-composite. Both produced populated,
non-degenerate recorded traces.
- Why working: the milestone's central promise — build a graph fractally from a
Rust builder and compile/bootstrap/run it — is delivered, and the public
surface (ledger + rustdoc) is self-sufficient for a downstream author. The
recursive inline of a nested composite works end to end on the run path.
- Recommended action: carry-on.
### [working] The graph-as-data accessors let a consumer walk the built graph (incl. composite interiors) with no engine internals
- Example: mc_4.
- What happened: `Blueprint::{nodes,sources,edges}` and
`Composite::{name,nodes,edges,input_roles,output}` together expose the whole
graph as read-only data; a recursive walk reaches every leaf label, every
role fan-out, the output port, and every interior edge. `node.label()`
dispatches off the boxed leaf without importing the `Node` trait.
- Why working: C9 "the built graph is introspectable runtime data" and the #13
graph-as-data promise hold from the consumer side.
- Recommended action: carry-on.
### [working] Per-node labels disambiguate identical node types, and a mis-wire changes the graph-as-data
- Examples: mc_2, render_clustered.txt.
- What happened: `label()` overrides carry params (`SMA(2)` vs `SMA(4)`), so the
two SMAs are distinguishable; a fast/slow swap changes the interior edge
table. The `aura graph` clustered render shows both SMAs feeding `Sub` with
the labels present, so a reader can trace which leg goes where. Render is
byte-identical across runs (C1).
- Why working: the C8-refinement label mechanism does exactly what the milestone
needs — it makes a swap *readable*. This is the substrate of the headline
"render surfaces a mis-wiring" promise.
- Recommended action: carry-on.
### [friction] `aura graph` renders ONLY the built-in sample — a consumer cannot render the graph they authored
- Examples: mc_1/mc_2/mc_3 (authored blueprints) vs the CLI render artefacts.
- What happened: `aura graph` and `aura graph --compiled` are hard-wired to one
built-in sample blueprint. Any extra argument is silently ignored:
`aura graph --bogus`, `aura graph mc_1_composite_build_run.rs`, and even
`aura graph --help` all render the **same** built-in sample and exit 0. There
is no path (flag, file, stdin) to point the renderer at a consumer's own
`Blueprint`.
- Why friction: the milestone's headline payoff — "render the structure so any
**mis-wiring** is visible before/around a run" — is, at the CLI level, only
available for the engine's own sample. A downstream researcher who authored
`mc_2`'s *swapped* cross (the literal mis-wire the feature exists to catch)
cannot feed it to `aura graph`; they fall back to introspecting the graph-as-data
by hand (mc_2/mc_4) or writing their own ascii-dag adapter (which lives only in
`aura-cli`, not a reusable surface). The render-as-a-library affordance that
would let a consumer render their own graph is absent. Task completed (the
render exists and is correct), but the consumer is forced into a hand-rolled
workaround to apply it to their own work.
- Recommended action: plan — expose a render entry point a consumer can call on
their own `Blueprint` (a library `render(&Blueprint) -> String` in a face
crate, or `aura graph` accepting a project/blueprint selector). Pairs naturally
with the deferred `aura new` / experiment-builder surface.
### [friction] The construction surface (aura-engine) does not re-export the core scalar vocabulary a graph-builder needs
- Example: mc_4 (initial `use aura_engine::{… ScalarKind …}` failed to compile:
"no `ScalarKind` in the root … consider importing `aura_core::ScalarKind`").
- What happened: building a `Blueprint`/`SourceSpec`/`Recorder` requires
`ScalarKind` (and, for sources/recorders, `Scalar` / `Firing` / `Timestamp`),
but `aura-engine` — the crate that owns `Blueprint`, `Composite`, `Edge`,
`Target`, `SourceSpec` — re-exports none of them. `SourceSpec { kind:
ScalarKind, .. }` is an `aura-engine` struct whose own field type must be
imported from a *different* crate (`aura-core`). A downstream author must know
to reach across crate boundaries for the vocabulary the engine's own public
types demand.
- Why friction: it is a paper cut, not a blocker — the compiler error names the
right import — but it makes the "build a graph from a Rust builder" surface
more than one `use aura_engine::*`. The construction API and the scalar
vocabulary it consumes live in two crates with no convenience re-export.
- Recommended action: plan (tidy) — re-export the core scalar vocabulary
(`Scalar`, `ScalarKind`, `Firing`, `Timestamp`) from `aura-engine` (or a
prelude) so a graph-builder has a single import surface. Low priority.
### [spec_gap] `aura graph --help` / unknown args → render the sample, exit 0 (no help, args silently swallowed)
- Examples: render probes — `aura graph --help` and `aura graph --bogus` both
print the clustered sample and exit 0.
- What happened: the `graph` subcommand keys only on the presence of the exact
`--compiled` token; every other argument (including `--help`) falls through to
the clustered view. No usage/help text, no rejection of unknown flags.
- Why spec_gap: the milestone/CLI contract does not pin how `aura graph` handles
unknown or `--help` arguments; the binary chose the lenient "ignore and render
the default view" reading. An equally plausible reading rejects unknown flags
(exit 2) and prints help on `--help`. This re-confirms, on the new `graph`
subcommand, the same lenient-arg gap the walking-skeleton fieldtest recorded
for `aura run <junk>` and `aura --help`.
- Recommended action: ratify-or-tighten — decide a single CLI arg policy
(reject unknown flags + `--help`→stdout/exit 0) across **both** `run` and
`graph`, rather than per-subcommand. Low priority; carried/expanded from the
walking-skeleton finding.
### [spec_gap] The known nested-composite render panic (#26) is unreachable from the public CLI today — its consumer impact is latent, not live
- Example: mc_3 (authored a nested composite; could not feed it to `aura graph`).
- What happened: the carrier flags #26 — the clustered blueprint view has an
`unimplemented!` on nested composites — as a sharp edge to classify. As a
downstream consumer I **could not reach it**: `aura graph` renders only the
built-in single-level sample (see the friction above), so there is no public
surface that feeds a *nested* blueprint into the clustered renderer. The panic
is real in the code but **structurally unreachable** from the shipped CLI; the
run-path nesting (mc_3) works fine.
- Why spec_gap: the milestone promise says the render makes nesting visible, but
the only render entry point is the fixed sample, so "render a nested composite"
is not actually exercisable by a consumer — the gap and the latent panic are
two faces of the missing "render my own graph" affordance. Severity of #26 as a
*consumer-facing* bug is currently **low/latent** (no public trigger), but it
becomes **live** the moment the render is made parameterizable (the friction
above) — at which point a consumer rendering a nested clustered view would
panic.
- Recommended action: tighten the design ledger / sequence #26 — record that the
"render my own graph" affordance (the friction above) and the #26 clustered
nesting fix are **coupled**: shipping the former without the latter exposes the
panic to consumers. The audit's own note ("resolve #26 before the next
render-touching cycle") already points this way; this fieldtest confirms the
coupling empirically. Classify #26 as the blocker *for the parameterizable-render
cycle*, not for this milestone (which ships only the sample render).
## Recommendation summary
| Finding | Class | Action |
|---|---|---|
| Composite authoring → compile → bootstrap → run thread authorable, populated | working | carry-on |
| Graph-as-data accessors walk the built graph (incl. interiors) | working | carry-on |
| Labels disambiguate; a swap changes graph-as-data; render deterministic | working | carry-on |
| `aura graph` renders only the built-in sample (no render-my-own-graph) | friction | plan |
| aura-engine doesn't re-export the core scalar vocabulary | friction | plan (tidy) |
| `aura graph --help` / unknown args → render sample, exit 0 | spec_gap | ratify-or-tighten (unify CLI arg policy) |
| #26 nested-render panic unreachable from CLI today (latent, coupled to render-my-own-graph) | spec_gap | tighten ledger / sequence #26 with the parameterizable-render cycle |
## Milestone roll-up
**The "Construction layer" milestone DELIVERS its core promise.** A downstream
researcher can author leaf nodes and reusable named composites, nest them
arbitrarily, and `compile()`/`bootstrap()`/`run()` the param-generic `Blueprint`
down to a flat, deterministic, populated-trace instance (C9/C19/C23) — all from
the public interface, first try, including a composite-in-composite. The
graph-as-data accessors let a consumer walk the built graph without engine
internals, and the C8-refinement labels make a mis-wire readable in both the
graph-as-data and the `aura graph` render.
The gap is in the **render's reach**, not its correctness: `aura graph` is
hard-wired to one built-in sample, so the headline "render any mis-wiring before
a run" payoff is, at the CLI level, only demonstrated on the engine's own
example — a consumer cannot point it at the graph they authored, which is exactly
where a mis-wire would live. That single missing affordance ("render my own
graph") subsumes both the friction and the latent reachability of the #26
nested-render panic: making the render parameterizable is the next step, and #26
must land with it. **No bugs found** (the #26 panic is real in code but
structurally unreachable from the public surface today). Three `working`, two
`friction`, two `spec_gap` — none blocking the milestone gate.
+30
View File
@@ -0,0 +1,30 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "aura-core"
version = "0.1.0"
[[package]]
name = "aura-engine"
version = "0.1.0"
dependencies = [
"aura-core",
]
[[package]]
name = "aura-std"
version = "0.1.0"
dependencies = [
"aura-core",
]
[[package]]
name = "ms-construction-layer-fieldtest"
version = "0.0.0"
dependencies = [
"aura-core",
"aura-engine",
"aura-std",
]
@@ -0,0 +1,39 @@
# Milestone fieldtest consumer crate — Construction layer.
#
# A standalone downstream project (the shape a C16 research project takes):
# path-deps on the engine crates only. It drives the construction layer
# (Blueprint / Composite / compile / bootstrap / run) and the graph-as-data
# accessors from the PUBLIC interface only — no crates/*/src was read; the
# API was discovered from the design ledger (docs/design/INDEX.md) + rustdoc
# doc-comments + the public re-exports.
#
# Empty [workspace] table: this fixture crate is its OWN workspace root, so it
# is not pulled into the aura engine workspace.
[workspace]
[package]
name = "ms-construction-layer-fieldtest"
edition = "2024"
version = "0.0.0"
publish = false
[[bin]]
name = "composite_build_run"
path = "mc_1_composite_build_run.rs"
[[bin]]
name = "miswire_render"
path = "mc_2_miswire_render.rs"
[[bin]]
name = "nested_composite"
path = "mc_3_nested_composite.rs"
[[bin]]
name = "introspect_graph"
path = "mc_4_introspect_graph.rs"
[dependencies]
aura-core = { path = "../../crates/aura-core" }
aura-std = { path = "../../crates/aura-std" }
aura-engine = { path = "../../crates/aura-engine" }
@@ -0,0 +1,111 @@
// Milestone fieldtest — axis (a): author a small signal as a reusable NAMED
// Composite, build a Blueprint that uses it, compile() + bootstrap() + run(),
// and confirm the recorded trace is non-empty.
//
// Public interface only: Blueprint / Composite / BlueprintNode / Edge / Target /
// OutPort / SourceSpec from aura-engine; Sma / Sub / Exposure / SimBroker /
// Recorder from aura-std; ScalarKind / Scalar / Firing / Timestamp from
// aura-core. No crates/*/src was read.
//
// The downstream task: package an SMA(2)/SMA(4)-cross into a reusable
// `sma_cross` composite exposing one output (the cross spread), then wire that
// composite into a harness blueprint (source -> composite -> Exposure ->
// SimBroker -> Recorder), bootstrap it, and run it over a tiny synthetic price
// ramp. This is the milestone's headline authoring story: build a graph
// fractally from a Rust builder via a named composite.
use std::sync::mpsc;
use aura_core::{Firing, Scalar, ScalarKind, Timestamp};
use aura_engine::{Blueprint, BlueprintNode, Composite, Edge, OutPort, SourceSpec, Target};
use aura_std::{Exposure, Recorder, SimBroker, Sma, Sub};
fn main() {
// --- The reusable composite: a 2/4 SMA cross. ----------------------------
// Interior layout (local indices):
// 0: Sma(2) (fast) 1: Sma(4) (slow) 2: Sub (fast - slow)
// One input role (the price) fans into BOTH SMAs' slot 0.
// The exposed output port is Sub's single output field.
let sma_cross = Composite::new(
"sma_cross",
vec![
BlueprintNode::from(Sma::new(2)),
BlueprintNode::from(Sma::new(4)),
BlueprintNode::from(Sub::new()),
],
// interior edges (local indices): fast -> Sub.slot0, slow -> Sub.slot1
vec![
Edge { from: 0, to: 2, slot: 0, from_field: 0 },
Edge { from: 1, to: 2, slot: 1, from_field: 0 },
],
// input role 0 (price) fans into Sma(2).slot0 AND Sma(4).slot0
vec![vec![
Target { node: 0, slot: 0 },
Target { node: 1, slot: 0 },
]],
// single exposed output: Sub's output field 0
OutPort { node: 2, field: 0 },
);
// --- The harness blueprint that USES the composite. ----------------------
// Top-level items:
// 0: sma_cross (Composite) 1: Exposure(0.5) 2: SimBroker(1e-4)
// 3: Recorder over [F64] (taps the broker equity)
let (tx, rx) = mpsc::channel::<(Timestamp, Vec<Scalar>)>();
let blueprint = Blueprint::new(
vec![
BlueprintNode::Composite(sma_cross),
BlueprintNode::from(Exposure::new(0.5)),
BlueprintNode::from(SimBroker::new(1e-4)),
BlueprintNode::from(Recorder::new(&[ScalarKind::F64], Firing::Any, tx)),
],
// one f64 price source: feeds the composite (item 0, role 0) AND the
// broker's price leg (item 2, slot 1).
vec![SourceSpec {
kind: ScalarKind::F64,
targets: vec![
Target { node: 0, slot: 0 }, // composite price role
Target { node: 2, slot: 1 }, // broker price leg
],
}],
// top-level edges: composite-out -> Exposure -> broker.exposure(slot0)
// -> Recorder
vec![
Edge { from: 0, to: 1, slot: 0, from_field: 0 }, // sma_cross -> Exposure
Edge { from: 1, to: 2, slot: 0, from_field: 0 }, // Exposure -> SimBroker.exposure
Edge { from: 2, to: 3, slot: 0, from_field: 0 }, // SimBroker -> Recorder
],
);
// --- compile() + bootstrap() + run() -------------------------------------
let mut harness = blueprint
.bootstrap()
.expect("composite-authored blueprint should bootstrap");
// a synthetic upward price ramp then a dip, enough to warm SMA(4) and flip
let prices: Vec<f64> = vec![
1.00, 1.01, 1.02, 1.03, 1.05, 1.08, 1.06, 1.04, 1.02, 1.01, 1.03, 1.07,
];
let stream: Vec<(Timestamp, Scalar)> = prices
.iter()
.enumerate()
.map(|(i, &p)| (Timestamp(60_000_000_000 * i as i64), Scalar::F64(p)))
.collect();
harness.run(vec![stream]);
drop(harness); // drop the recorder's tx clone held inside
let rows: Vec<(Timestamp, Vec<Scalar>)> = rx.iter().collect();
println!("recorded equity rows: {}", rows.len());
for (ts, row) in &rows {
let v = match row[0] {
Scalar::F64(x) => x,
other => panic!("expected f64 equity, got {other:?}"),
};
println!(" ts={:>14} equity_pips={:.4}", ts.0, v);
}
assert!(
!rows.is_empty(),
"the composite-authored harness must record a populated equity trace"
);
println!("OK: composite authored, compiled, bootstrapped, ran, recorded.");
}
@@ -0,0 +1,90 @@
// Milestone fieldtest — axis (b): deliberately mis-wire a graph (swap the
// fast/slow SMA legs of the cross) and confirm the wiring reads back
// differently — the milestone's headline promise that a render visibly
// surfaces a mis-wiring.
//
// The headline render is the `aura graph` CLI (exercised separately, in the
// accompanying .sh-free flow: see the spec). HERE we test the property the
// render RELIES ON from the consumer side: that the labels + graph-as-data of a
// correctly-wired composite differ observably from a swapped one. If a swap
// produced an identical graph-as-data, no renderer could ever surface it.
//
// Public interface only: Composite / Blueprint accessors + Node::label() via
// the boxed node; ledger C8-refinement (label is the disambiguating symbol).
use aura_engine::{BlueprintNode, Composite, Edge, OutPort, Target};
// NB: `node.label()` on a `&Box<dyn Node>` leaf dispatches via the trait object
// without an explicit `use aura_core::Node` (the method is in scope through the
// boxed trait object). A consumer does not need to import the Node trait to read
// labels off a built graph.
use aura_std::{Sma, Sub};
/// Build an SMA-cross composite. `swap` flips which SMA feeds Sub's slot 0.
/// Correct: fast(SMA2) -> minuend(slot0), slow(SMA4) -> subtrahend(slot1).
/// Swapped: slow -> slot0, fast -> slot1 (the classic fast/slow mis-wire).
fn cross(swap: bool) -> Composite {
let (e_fast, e_slow) = if swap {
// slow(node 1) into slot0, fast(node 0) into slot1
(
Edge { from: 1, to: 2, slot: 0, from_field: 0 },
Edge { from: 0, to: 2, slot: 1, from_field: 0 },
)
} else {
(
Edge { from: 0, to: 2, slot: 0, from_field: 0 },
Edge { from: 1, to: 2, slot: 1, from_field: 0 },
)
};
Composite::new(
"sma_cross",
vec![
BlueprintNode::from(Sma::new(2)),
BlueprintNode::from(Sma::new(4)),
BlueprintNode::from(Sub::new()),
],
vec![e_fast, e_slow],
vec![vec![Target { node: 0, slot: 0 }, Target { node: 1, slot: 0 }]],
OutPort { node: 2, field: 0 },
)
}
fn node_labels(c: &Composite) -> Vec<String> {
c.nodes()
.iter()
.map(|n| match n {
BlueprintNode::Leaf(node) => node.label(),
BlueprintNode::Composite(inner) => format!("composite:{}", inner.name()),
})
.collect()
}
fn main() {
let correct = cross(false);
let swapped = cross(true);
// The per-node labels carry identifying params (SMA(2) vs SMA(4)) so two
// SMAs are distinguishable in a render.
println!("correct labels: {:?}", node_labels(&correct));
println!("swapped labels: {:?}", node_labels(&swapped));
// The mis-wire lives in the EDGE table; the graph-as-data must reflect it.
let ce: Vec<Edge> = correct.edges().to_vec();
let se: Vec<Edge> = swapped.edges().to_vec();
println!("correct interior edges: {ce:?}");
println!("swapped interior edges: {se:?}");
// Headline property: the two graphs-as-data differ. A renderer reading the
// edge table + labels can therefore make the swap visible.
assert_ne!(
ce, se,
"a fast/slow swap MUST change the graph-as-data, else no render could surface it"
);
// And the disambiguating labels are present (SMA(2)/SMA(4) distinguishable).
let labels = node_labels(&correct);
assert!(
labels.iter().any(|l| l.contains("SMA(2)")) && labels.iter().any(|l| l.contains("SMA(4)")),
"labels must carry SMA params so a swapped leg reads differently: {labels:?}"
);
println!("OK: a fast/slow swap is observable in graph-as-data + labels.");
}
@@ -0,0 +1,100 @@
// Milestone fieldtest — axis (c): nest a composite INSIDE another composite,
// build a Blueprint, compile() + bootstrap() + run(). The milestone promises
// composites "nest arbitrarily" and that the compiled/flat view handles
// nesting (#26 notes the CLUSTERED blueprint render has an `unimplemented!` on
// nested composites — but that path is only reachable through `aura graph` on
// the built-in sample, which a downstream consumer cannot point at their own
// graph; see the spec finding).
//
// Here we verify the run-path promise from the consumer side: a composite that
// CONTAINS a composite lowers (inlines, recursively) to a flat runnable
// instance that produces a populated trace.
//
// Public interface only.
use std::sync::mpsc;
use aura_core::{Firing, Scalar, ScalarKind, Timestamp};
use aura_engine::{Blueprint, BlueprintNode, Composite, Edge, OutPort, SourceSpec, Target};
use aura_std::{Exposure, Recorder, SimBroker, Sma, Sub};
/// Inner composite: the SMA(2)/SMA(4) cross (one price role -> spread output).
fn inner_cross() -> Composite {
Composite::new(
"sma_cross",
vec![
BlueprintNode::from(Sma::new(2)),
BlueprintNode::from(Sma::new(4)),
BlueprintNode::from(Sub::new()),
],
vec![
Edge { from: 0, to: 2, slot: 0, from_field: 0 },
Edge { from: 1, to: 2, slot: 1, from_field: 0 },
],
vec![vec![Target { node: 0, slot: 0 }, Target { node: 1, slot: 0 }]],
OutPort { node: 2, field: 0 },
)
}
/// Outer composite: wraps inner_cross then maps it through Exposure(0.5).
/// Interior: 0 = inner_cross (Composite), 1 = Exposure(0.5).
/// One price role fans into the inner composite's role 0.
/// Output: Exposure's output field.
fn strategy() -> Composite {
Composite::new(
"strategy",
vec![
BlueprintNode::Composite(inner_cross()),
BlueprintNode::from(Exposure::new(0.5)),
],
vec![Edge { from: 0, to: 1, slot: 0, from_field: 0 }], // cross -> Exposure
vec![vec![Target { node: 0, slot: 0 }]], // price -> inner role 0
OutPort { node: 1, field: 0 },
)
}
fn main() {
let (tx, rx) = mpsc::channel::<(Timestamp, Vec<Scalar>)>();
// Top-level: 0 = strategy (nested Composite), 1 = SimBroker, 2 = Recorder.
let blueprint = Blueprint::new(
vec![
BlueprintNode::Composite(strategy()),
BlueprintNode::from(SimBroker::new(1e-4)),
BlueprintNode::from(Recorder::new(&[ScalarKind::F64], Firing::Any, tx)),
],
vec![SourceSpec {
kind: ScalarKind::F64,
targets: vec![
Target { node: 0, slot: 0 }, // strategy price role
Target { node: 1, slot: 1 }, // broker price leg
],
}],
vec![
Edge { from: 0, to: 1, slot: 0, from_field: 0 }, // strategy -> broker.exposure
Edge { from: 1, to: 2, slot: 0, from_field: 0 }, // broker -> Recorder
],
);
let mut harness = blueprint
.bootstrap()
.expect("nested-composite blueprint should bootstrap");
let prices: Vec<f64> = vec![
1.00, 1.01, 1.02, 1.03, 1.05, 1.08, 1.06, 1.04, 1.02, 1.01, 1.03, 1.07,
];
let stream: Vec<(Timestamp, Scalar)> = prices
.iter()
.enumerate()
.map(|(i, &p)| (Timestamp(60_000_000_000 * i as i64), Scalar::F64(p)))
.collect();
harness.run(vec![stream]);
drop(harness);
let rows: Vec<(Timestamp, Vec<Scalar>)> = rx.iter().collect();
println!("nested-composite recorded rows: {}", rows.len());
assert!(
!rows.is_empty(),
"a composite-in-composite must inline to a flat runnable instance"
);
println!("OK: composite-inside-composite inlined, bootstrapped, ran, recorded.");
}
@@ -0,0 +1,112 @@
// Milestone fieldtest — axis (d): introspect a built Blueprint via the
// read-only graph-as-data accessors — can a consumer walk the graph as data
// WITHOUT reading engine internals?
//
// Accessors under test (all public, from rustdoc):
// Blueprint::nodes() / sources() / edges()
// Composite::name() / nodes() / edges() / input_roles() / output()
// BlueprintNode::{Leaf, Composite}; Node::label() on a boxed leaf
//
// Public interface only.
use aura_core::ScalarKind;
use aura_engine::{Blueprint, BlueprintNode, Composite, Edge, OutPort, SourceSpec, Target};
// NB (fieldtest finding): `aura_engine::ScalarKind` does NOT resolve — the
// construction surface (aura-engine) does not re-export the core scalar
// vocabulary (ScalarKind / Scalar / Firing) that a graph-builder needs. A
// downstream author building a Blueprint must reach into aura_core directly.
// Recorded as friction in the spec. `node.label()` below dispatches on the
// boxed leaf via the Node trait object without an explicit `use` of Node.
fn sma_cross() -> Composite {
Composite::new(
"sma_cross",
vec![
BlueprintNode::from(aura_std::Sma::new(2)),
BlueprintNode::from(aura_std::Sma::new(4)),
BlueprintNode::from(aura_std::Sub::new()),
],
vec![
Edge { from: 0, to: 2, slot: 0, from_field: 0 },
Edge { from: 1, to: 2, slot: 1, from_field: 0 },
],
vec![vec![Target { node: 0, slot: 0 }, Target { node: 1, slot: 0 }]],
OutPort { node: 2, field: 0 },
)
}
fn describe_node(prefix: &str, n: &BlueprintNode) {
match n {
BlueprintNode::Leaf(node) => {
let sch = node.schema();
println!(
"{prefix}LEAF {:<14} inputs={} output_fields={}",
node.label(),
sch.inputs.len(),
sch.output.len()
);
}
BlueprintNode::Composite(c) => {
println!(
"{prefix}COMPOSITE name={:?} interior_items={} roles={} out=({},{})",
c.name(),
c.nodes().len(),
c.input_roles().len(),
c.output().node,
c.output().field,
);
for inner in c.nodes() {
describe_node(&format!("{prefix} "), inner);
}
for (r, targets) in c.input_roles().iter().enumerate() {
println!("{prefix} role[{r}] fans into {targets:?}");
}
for e in c.edges() {
println!("{prefix} interior edge {e:?}");
}
}
}
}
fn main() {
let bp = Blueprint::new(
vec![
BlueprintNode::Composite(sma_cross()),
BlueprintNode::from(aura_std::Exposure::new(0.5)),
],
vec![SourceSpec {
kind: ScalarKind::F64,
targets: vec![Target { node: 0, slot: 0 }],
}],
vec![Edge { from: 0, to: 1, slot: 0, from_field: 0 }],
);
println!("== Blueprint graph-as-data walk ==");
println!("sources: {}", bp.sources().len());
for (i, s) in bp.sources().iter().enumerate() {
println!(" source[{i}] kind={:?} -> {:?}", s.kind, s.targets);
}
println!("top-level items: {}", bp.nodes().len());
for n in bp.nodes() {
describe_node(" ", n);
}
println!("top-level edges: {}", bp.edges().len());
for e in bp.edges() {
println!(" edge {e:?}");
}
// The walk must reach the interior of the composite purely through the
// public accessors — no engine internals.
let first = &bp.nodes()[0];
if let BlueprintNode::Composite(c) = first {
assert_eq!(c.name(), "sma_cross");
assert_eq!(c.nodes().len(), 3, "composite interior should have 3 items");
assert_eq!(c.input_roles().len(), 1, "one price role");
assert_eq!(c.input_roles()[0].len(), 2, "price fans into both SMAs");
let out = c.output();
assert_eq!((out.node, out.field), (2, 0), "Sub output is the port");
println!("OK: walked the composite interior via public accessors only.");
} else {
panic!("expected composite as first item");
}
}
@@ -0,0 +1,24 @@
[source:F64]
┌─────────└┐────────┐
│ │ │
╔═════╪══════════╪════╗ │
║ sma_cross │ ║ │
║ ↓ ↓ ║ │
║ [SMA(2)] [SMA(4)] ║ │
║ └──┌───────┘ ║ │
║ ↓ ┌─╫───┘
║ [Sub] │ ║
║ │ │ ║
╚════════╪══════════╪═╝
│ │
↓ │
[Exposure(0.5)] │
┌───────┘────────┐ │
↓ ↓─┘
[Recorder] [SimBroker(0.0001)]
[Recorder]
@@ -0,0 +1,19 @@
[source:F64]
┌────────└─┐──────┐
↓ ↓ │
[SMA(2)] [SMA(4)] │
└────┌─────┘ │
↓ ┌──────┘
[Sub] │
│ │
↓ └────┐
[Exposure(0.5)] │
┌──────┘─────────┐│
↓ ↓┘
[Recorder] [SimBroker(0.0001)]
┌─────┘
[Recorder]