feat(aura-engine): composite output is a named multi-field record

Replace a composite's single output port (OutPort { node, field }) with
an ordered, named record (Vec<OutField { node, field, name }>). A
composite can now re-export K interior fields as one output; a consumer
selects one via the existing Edge::from_field — the same field-wise
selector that already works for multi-output leaf producers (OHLCV).

Why: multi-line indicators (MACD, Bollinger, Stochastic, Ichimoku) could
not be authored as a composition and re-exported as a unit — only one
line escaped the boundary. The MACD PoC was forced to expose only the
histogram. This lifts the three `field == 0` / `from_field == 0` caps at
the composite boundary (inline_composite nested arm, rewrite_edge
composite arm), range-checking the index instead.

Boundary completion, not an invariant change:
- C8/C7/C4 untouched — one port, one row per eval, K base columns (a
  3-line MACD is identical in kind to OHLCV).
- C23 honoured — re-export names live at the blueprint boundary only and
  are dropped in the compilat (raw index wiring). compiled_view_golden
  stayed byte-identical (the regression guard): no name leaked into the
  flat graph.

The MACD PoC now re-exports macd / signal / histogram as a record; the
strategy still trades the histogram, selected via from_field: 2. The
render shows K [out:<name>] markers per multi-output composite (input
roles stay [in:<index>] — naming them is the dependent #41).

Output naming ships with the capability (OutField is born name-ready) so
#41 — composite param aliasing + input-role naming — does not reopen the
type.

Verification: cargo build/test/clippy --workspace -- -D warnings all
green; aura-engine 62 tests (+3 capability, +1 through-run E2E), aura-cli
unchanged-count green; compiled_view_golden byte-identical.

Known debt (out of scope, pre-existing): the non-workspace construction-
layer fieldtests (fieldtests/milestone-construction-layer/mc_1..mc_4)
carry their OutField sweep but still do not compile standalone — they use
the Sma::new / BlueprintNode::from(Sma) API retired in the cycle-0016
value-empty migration, never propagated to these fixtures. Tracked as a
follow-up.

closes #40
This commit is contained in:
2026-06-08 01:00:04 +02:00
parent 2c33a8d74f
commit 784e6c917a
8 changed files with 260 additions and 77 deletions
+7 -5
View File
@@ -100,8 +100,8 @@ fn collect_distinct_composites(bp: &Blueprint) -> Vec<&Composite> {
/// Render one composite's interior as a flat graph: interior leaves as `[type]`,
/// nested composites as opaque `[name]`, plus an `[in:k]` entry marker per input
/// role (wired to its interior targets) and an `[out]` marker (wired from the
/// output port). Prefixed `"<name>:\n"`.
/// role (wired to its interior targets) and an `[out:<name>]` marker per
/// re-exported output field (wired from its producer). Prefixed `"<name>:\n"`.
fn render_definition(c: &Composite) -> String {
let mut labels: Vec<String> = Vec::with_capacity(c.nodes().len());
for inner in c.nodes() {
@@ -121,9 +121,11 @@ fn render_definition(c: &Composite) -> String {
edges.push((in_id, t.node));
}
}
let out_id = labels.len();
labels.push("out".to_string());
edges.push((c.output().node, out_id));
for of in c.output() {
let out_id = labels.len();
labels.push(format!("out:{}", of.name));
edges.push((of.node, out_id));
}
format!("{}:\n{}", c.name(), render_flat(&labels, &edges))
}
+19 -14
View File
@@ -10,7 +10,7 @@ mod graph;
use aura_core::{Firing, Scalar, ScalarKind, Timestamp};
use aura_engine::{
f64_field, summarize, Blueprint, BlueprintNode, Composite, Edge, Harness, OutPort,
f64_field, summarize, Blueprint, BlueprintNode, Composite, Edge, Harness, OutField,
RunManifest, RunReport, SourceSpec, Target,
};
use aura_std::{Ema, Exposure, Recorder, SimBroker, Sma, Sub};
@@ -127,7 +127,7 @@ fn sma_cross(name: &str) -> Composite {
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 },
vec![OutField { node: 2, field: 0, name: "cross".into() }],
)
}
@@ -176,12 +176,13 @@ fn sample_point() -> Vec<Scalar> {
// --- MACD proof-of-concept (a richer, nested indicator + strategy) -----------
/// The MACD signal as a named composite: price → fast/slow `Ema` → the MACD line
/// (their spread) → a signal `Ema` of that line → the histogram (line signal),
/// which is the composite's single output — the line the strategy trades on. A
/// richer fixture than `sma_cross`: a nested EMA-of-EMA chain with interior
/// fan-out (the MACD line feeds *both* the signal EMA and the histogram). Three
/// `length` knobs (fast, slow, signal) are injected at compile in node order;
/// value-empty here.
/// (their spread) → a signal `Ema` of that line → the histogram (line signal).
/// The composite exposes all **three MACD lines** as a named output record
/// (`macd`, `signal`, `histogram`); the strategy trades the histogram by reading
/// `from_field: 2`. A richer fixture than `sma_cross`: a nested EMA-of-EMA chain
/// with interior fan-out (the MACD line feeds *both* the signal EMA and the
/// histogram). Three `length` knobs (fast, slow, signal) are injected at compile
/// in node order; value-empty here.
fn macd(name: &str) -> Composite {
Composite::new(
name,
@@ -203,7 +204,11 @@ fn macd(name: &str) -> Composite {
Target { node: 0, slot: 0 }, // price → fast EMA
Target { node: 1, slot: 0 }, // price → slow EMA
]],
OutPort { node: 4, field: 0 }, // the histogram
vec![
OutField { node: 2, field: 0, name: "macd".into() }, // the MACD line
OutField { node: 3, field: 0, name: "signal".into() }, // the signal line
OutField { node: 4, field: 0, name: "histogram".into() }, // the histogram
],
)
}
@@ -230,7 +235,7 @@ fn macd_strategy_blueprint(
],
}],
vec![
Edge { from: 0, to: 1, slot: 0, from_field: 0 }, // histogram → Exposure
Edge { from: 0, to: 1, slot: 0, from_field: 2 }, // histogram → Exposure
Edge { from: 1, to: 2, slot: 0, from_field: 0 }, // exposure → broker slot 0
Edge { from: 2, to: 3, slot: 0, from_field: 0 }, // equity → sink
Edge { from: 1, to: 4, slot: 0, from_field: 0 }, // exposure → sink
@@ -374,7 +379,7 @@ mod tests {
let out = graph::render_blueprint(&sample_blueprint());
// the sma_cross body is defined exactly once, with its interior + ports
assert_eq!(out.matches("sma_cross:").count(), 1, "definition not rendered once:\n{out}");
for needle in ["[SMA]", "[Sub]", "[in:0]", "[out]"] {
for needle in ["[SMA]", "[Sub]", "[in:0]", "[out:cross]"] {
assert!(out.contains(needle), "missing {needle} in definition:\n{out}");
}
}
@@ -388,14 +393,14 @@ mod tests {
vec![Sma::factory().into()],
vec![],
vec![vec![Target { node: 0, slot: 0 }]],
OutPort { node: 0, field: 0 },
vec![OutField { node: 0, field: 0, name: "out".into() }],
);
let outer = Composite::new(
"outer",
vec![BlueprintNode::Composite(inner), Sub::factory().into()],
vec![Edge { from: 0, to: 1, slot: 0, from_field: 0 }],
vec![vec![Target { node: 0, slot: 0 }]],
OutPort { node: 1, field: 0 },
vec![OutField { node: 1, field: 0, name: "out".into() }],
);
let bp = Blueprint::new(
vec![BlueprintNode::Composite(outer)],
@@ -494,7 +499,7 @@ sma_cross:
[Sub]
[out]
[out:cross]
"#;
+223 -47
View File
@@ -15,11 +15,15 @@ use aura_core::{LeafFactory, Node, ParamSpec, Scalar, ScalarKind};
use crate::harness::{BootstrapError, Edge, Harness, SourceSpec, Target};
/// Which interior `(node, output-field)` is a composite's single output port (C8).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct OutPort {
/// One re-exported field of a composite's output record: an interior
/// `(node, output-field)` surfaced at the boundary under `name`. `name` is a
/// non-load-bearing render/debug symbol (C23) — like `FieldSpec.name` and
/// `Composite.name`, it does not reach the compilat.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct OutField {
pub node: usize,
pub field: usize,
pub name: String,
}
/// A blueprint item: a leaf node or a nested composite. Both present a declared
@@ -45,7 +49,7 @@ pub struct Composite {
nodes: Vec<BlueprintNode>,
edges: Vec<Edge>,
input_roles: Vec<Vec<Target>>,
output: OutPort,
output: Vec<OutField>,
}
impl Composite {
@@ -58,7 +62,7 @@ impl Composite {
nodes: Vec<BlueprintNode>,
edges: Vec<Edge>,
input_roles: Vec<Vec<Target>>,
output: OutPort,
output: Vec<OutField>,
) -> Self {
Self { name: name.into(), nodes, edges, input_roles, output }
}
@@ -79,9 +83,10 @@ impl Composite {
pub fn input_roles(&self) -> &[Vec<Target>] {
&self.input_roles
}
/// The single exposed output port.
pub fn output(&self) -> OutPort {
self.output
/// The exposed output record: each entry re-exports one interior
/// `(node, output-field)` under a boundary name (C8 — one port, K columns).
pub fn output(&self) -> &[OutField] {
&self.output
}
}
@@ -243,9 +248,10 @@ fn collect_params(items: &[BlueprintNode], prefix: &str, out: &mut Vec<ParamSpec
enum ItemLowering {
/// A leaf lowered to exactly one flat node at this index.
Leaf { index: usize },
/// A composite lowered to its interior: its single output port is this flat
/// `(node, field)`, and input role `r` fans into `roles[r]` (flat targets).
Composite { output: (usize, usize), roles: Vec<Vec<Target>> },
/// A composite lowered to its interior: its output record is these flat
/// `(node, field)` producers (one per re-exported field, declared order), and
/// input role `r` fans into `roles[r]` (flat targets). Names dropped (C23).
Composite { output: Vec<(usize, usize)>, roles: Vec<Vec<Target>> },
}
/// Lower a list of blueprint items into the flat node array, appending interior
@@ -301,12 +307,6 @@ fn inline_composite(
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
// once the item's lowering is known)
if output.node >= item_count {
return Err(CompileError::OutputPortOutOfRange);
}
// recursively lower interior items, then rewrite interior edges through them
let interior = lower_items(nodes, params, cursor, flat_nodes, flat_edges)?;
for e in &edges {
@@ -315,22 +315,25 @@ fn inline_composite(
}
}
// resolve the output port to a flat (node, field)
let out = match &interior[output.node] {
ItemLowering::Leaf { index } => {
if output.field >= flat_nodes[*index].schema().output.len() {
return Err(CompileError::OutputPortOutOfRange);
}
(*index, output.field)
// resolve each re-exported field to a flat (node, field), in declared order
let mut out: Vec<(usize, usize)> = Vec::with_capacity(output.len());
for of in &output {
if of.node >= item_count {
return Err(CompileError::OutputPortOutOfRange);
}
ItemLowering::Composite { output: nested, .. } => {
// a nested composite exposes exactly one output field
if output.field != 0 {
return Err(CompileError::OutputPortOutOfRange);
let resolved = match &interior[of.node] {
ItemLowering::Leaf { index } => {
if of.field >= flat_nodes[*index].schema().output.len() {
return Err(CompileError::OutputPortOutOfRange);
}
(*index, of.field)
}
*nested
}
};
ItemLowering::Composite { output: nested, .. } => {
*nested.get(of.field).ok_or(CompileError::OutputPortOutOfRange)?
}
};
out.push(resolved);
}
// resolve each input role to flat targets (a target into a nested composite
// fans further) and kind-check every role
@@ -373,11 +376,7 @@ fn rewrite_edge(
(*index, e.from_field)
}
ItemLowering::Composite { output, .. } => {
// a composite exposes one output field; reading any other is malformed
if e.from_field != 0 {
return Err(CompileError::BadInteriorIndex);
}
*output
*output.get(e.from_field).ok_or(CompileError::BadInteriorIndex)?
}
};
let targets = resolve_target(&Target { node: e.to, slot: e.slot }, lowerings)?;
@@ -529,7 +528,7 @@ mod tests {
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 },
vec![OutField { node: 2, field: 0, name: "out".into() }],
)
}
@@ -546,7 +545,7 @@ mod tests {
// 3 interior nodes (Pass1, Pass1, Join2) at flat 0..2, then SinkF64 at 3
assert_eq!(nodes.len(), 4);
// interior edges rewritten at offset 0, then the output edge resolves the
// composite's OutPort (interior node 2, field 0) to the sink (flat node 3)
// composite's OutField (interior node 2, field 0) to the sink (flat node 3)
assert_eq!(
edges,
vec![
@@ -563,6 +562,54 @@ mod tests {
);
}
#[test]
fn composite_reexports_two_fields_to_distinct_consumers() {
// composite: two independent Pass1 leaves; role 0 -> leaf 0, role 1 -> leaf 1;
// output record re-exports leaf 0 as "a", leaf 1 as "b".
let c = Composite::new(
"two_out",
vec![pass1(), pass1()],
vec![],
vec![
vec![Target { node: 0, slot: 0 }],
vec![Target { node: 1, slot: 0 }],
],
vec![
OutField { node: 0, field: 0, name: "a".into() },
OutField { node: 1, field: 0, name: "b".into() },
],
);
// composite is item 0; two sinks (items 1, 2) read its two output fields by
// from_field; one source fans into both roles.
let bp = Blueprint::new(
vec![BlueprintNode::Composite(c), sink_f64(), sink_f64()],
vec![SourceSpec {
kind: ScalarKind::F64,
targets: vec![Target { node: 0, slot: 0 }, Target { node: 0, slot: 1 }],
}],
vec![
Edge { from: 0, to: 1, slot: 0, from_field: 0 }, // field "a" -> sink 1
Edge { from: 0, to: 2, slot: 0, from_field: 1 }, // field "b" -> sink 2
],
);
let (nodes, sources, edges) = bp.compile().expect("valid multi-output composite");
// flat layout: Pass1(0), Pass1(1), SinkF64(2), SinkF64(3)
assert_eq!(nodes.len(), 4);
// from_field 0 resolves to leaf 0, from_field 1 to leaf 1 — distinct producers
assert_eq!(
edges,
vec![
Edge { from: 0, to: 2, slot: 0, from_field: 0 },
Edge { from: 1, to: 3, slot: 0, from_field: 0 },
]
);
// the source fanned into both interior leaves
assert_eq!(
sources[0].targets,
vec![Target { node: 0, slot: 0 }, Target { node: 1, slot: 0 }]
);
}
#[test]
fn nested_composite_inlines() {
// outer composite wraps the inner fan_composite as its only interior item,
@@ -575,7 +622,7 @@ mod tests {
vec![BlueprintNode::Composite(inner)],
vec![],
vec![vec![Target { node: 0, slot: 0 }]],
OutPort { node: 0, field: 0 },
vec![OutField { node: 0, field: 0, name: "out".into() }],
);
let bp = Blueprint::new(
vec![BlueprintNode::Composite(outer), sink_f64()],
@@ -600,6 +647,58 @@ mod tests {
);
}
#[test]
fn outer_reexports_two_fields_of_inner_composite() {
// inner re-exports two leaves as "a","b"; outer re-exposes both inner roles
// and re-exports inner field 0 and field 1 (the latter exercises the nested arm).
let inner = Composite::new(
"inner_two",
vec![pass1(), pass1()],
vec![],
vec![
vec![Target { node: 0, slot: 0 }],
vec![Target { node: 1, slot: 0 }],
],
vec![
OutField { node: 0, field: 0, name: "a".into() },
OutField { node: 1, field: 0, name: "b".into() },
],
);
let outer = Composite::new(
"outer_two",
vec![BlueprintNode::Composite(inner)],
vec![],
vec![
vec![Target { node: 0, slot: 0 }], // outer role 0 -> inner role 0
vec![Target { node: 0, slot: 1 }], // outer role 1 -> inner role 1
],
vec![
OutField { node: 0, field: 0, name: "x".into() }, // inner field 0
OutField { node: 0, field: 1, name: "y".into() }, // inner field 1 (nested arm)
],
);
let bp = Blueprint::new(
vec![BlueprintNode::Composite(outer), sink_f64(), sink_f64()],
vec![SourceSpec {
kind: ScalarKind::F64,
targets: vec![Target { node: 0, slot: 0 }, Target { node: 0, slot: 1 }],
}],
vec![
Edge { from: 0, to: 1, slot: 0, from_field: 0 }, // outer field x -> sink 1
Edge { from: 0, to: 2, slot: 0, from_field: 1 }, // outer field y -> sink 2
],
);
let (nodes, _sources, edges) = bp.compile().expect("valid nested multi-output");
assert_eq!(nodes.len(), 4); // Pass1, Pass1, SinkF64, SinkF64
assert_eq!(
edges,
vec![
Edge { from: 0, to: 2, slot: 0, from_field: 0 },
Edge { from: 1, to: 3, slot: 0, from_field: 0 },
]
);
}
#[test]
fn bad_interior_index_rejected() {
// interior edge references interior node 9, which does not exist
@@ -608,7 +707,7 @@ mod tests {
vec![pass1()],
vec![Edge { from: 0, to: 9, slot: 0, from_field: 0 }],
vec![vec![Target { node: 0, slot: 0 }]],
OutPort { node: 0, field: 0 },
vec![OutField { node: 0, field: 0, name: "out".into() }],
);
let bp = Blueprint::new(vec![BlueprintNode::Composite(c)], vec![], vec![]);
// the Ok arm holds Box<dyn Node> (not Debug), so assert via the Err arm.
@@ -623,7 +722,7 @@ mod tests {
vec![pass1(), sink_i64()],
vec![],
vec![vec![Target { node: 0, slot: 0 }, Target { node: 1, slot: 0 }]],
OutPort { node: 0, field: 0 },
vec![OutField { node: 0, field: 0, name: "out".into() }],
);
let bp = Blueprint::new(vec![BlueprintNode::Composite(c)], vec![], vec![]);
// the Ok arm holds Box<dyn Node> (not Debug), so assert via the Err arm.
@@ -638,13 +737,32 @@ mod tests {
vec![pass1()],
vec![],
vec![vec![Target { node: 0, slot: 0 }]],
OutPort { node: 0, field: 5 },
vec![OutField { node: 0, field: 5, name: "out".into() }],
);
let bp = Blueprint::new(vec![BlueprintNode::Composite(c)], vec![], vec![]);
// the Ok arm holds Box<dyn Node> (not Debug), so assert via the Err arm.
assert_eq!(bp.compile().err(), Some(CompileError::OutputPortOutOfRange));
}
#[test]
fn consume_of_missing_output_field_is_rejected() {
// a single-field composite; a consumer reads from_field 1 (past the 1-field
// record) -> the rewrite_edge range-check rejects it.
let c = Composite::new(
"c",
vec![pass1()],
vec![],
vec![vec![Target { node: 0, slot: 0 }]],
vec![OutField { node: 0, field: 0, name: "a".into() }],
);
let bp = Blueprint::new(
vec![BlueprintNode::Composite(c), sink_f64()],
vec![SourceSpec { kind: ScalarKind::F64, targets: vec![Target { node: 0, slot: 0 }] }],
vec![Edge { from: 0, to: 1, slot: 0, from_field: 1 }], // only field 0 exists
);
assert_eq!(bp.compile().err(), Some(CompileError::BadInteriorIndex));
}
#[test]
fn bootstrap_error_is_wrapped() {
// a top-level kind mismatch: a Pass1 f64 output wired into a SinkI64 i64
@@ -733,7 +851,7 @@ mod tests {
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 },
vec![OutField { node: 2, field: 0, name: "out".into() }],
)
}
@@ -799,6 +917,64 @@ mod tests {
assert!(!comp_ex_v.is_empty(), "exposure trace must be populated");
}
/// E2E (cycle 0018): a composite's multi-field output record is selected
/// field-wise downstream all the way through `bootstrap + run` — two consumers
/// reading distinct `from_field`s off one multi-output composite record the two
/// distinct interior producers' streams, deterministically. The Task-2 unit
/// tests stop at `compile()` (edge resolution); this one runs the harness, so a
/// regression that resolved both taps to the same producer (or dropped a field)
/// would surface as identical recorded traces here, not just a bad edge table.
#[test]
fn multi_output_composite_taps_distinct_fields_through_a_run() {
let prices = synthetic_prices();
let (tx_a, rx_a) = mpsc::channel();
let (tx_b, rx_b) = mpsc::channel();
// composite: two SMAs of different lengths, each its own input role; the
// output record re-exports SMA-fast as "a" (field 0) and SMA-slow as "b"
// (field 1). One source fans into both roles; two recorders tap the two
// fields by from_field.
let c = Composite::new(
"two_sma",
vec![Sma::factory().into(), Sma::factory().into()],
vec![],
vec![
vec![Target { node: 0, slot: 0 }],
vec![Target { node: 1, slot: 0 }],
],
vec![
OutField { node: 0, field: 0, name: "a".into() },
OutField { node: 1, field: 0, name: "b".into() },
],
);
let bp = Blueprint::new(
vec![
BlueprintNode::Composite(c),
Recorder::factory(vec![ScalarKind::F64], Firing::Any, tx_a).into(),
Recorder::factory(vec![ScalarKind::F64], Firing::Any, tx_b).into(),
],
vec![SourceSpec {
kind: ScalarKind::F64,
targets: vec![Target { node: 0, slot: 0 }, Target { node: 0, slot: 1 }],
}],
vec![
Edge { from: 0, to: 1, slot: 0, from_field: 0 }, // field "a" (SMA-2) -> recorder a
Edge { from: 0, to: 2, slot: 0, from_field: 1 }, // field "b" (SMA-4) -> recorder b
],
);
let mut h = bp
.bootstrap_with_params(vec![Scalar::I64(2), Scalar::I64(4)])
.expect("multi-output composite bootstraps");
h.run(vec![prices]);
let a = rx_a.try_iter().collect::<Vec<_>>();
let b = rx_b.try_iter().collect::<Vec<_>>();
// both fields recorded something (the equality below is meaningful only if
// populated) and the two taps captured different streams — distinct fast vs
// slow SMA, so the two from_field selections resolve to distinct producers.
assert!(!a.is_empty() && !b.is_empty(), "both field taps must be populated");
assert_ne!(a, b, "the two from_field taps must record distinct interior streams");
}
#[test]
fn injecting_a_different_vector_changes_the_run() {
let prices = synthetic_prices();
@@ -913,7 +1089,7 @@ mod tests {
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 },
vec![OutField { node: 2, field: 0, name: "out".into() }],
);
// outer composite "strategy": the inner composite + a LinComb([1,-1])
let strategy = Composite::new(
@@ -921,7 +1097,7 @@ mod tests {
vec![BlueprintNode::Composite(fast_slow), LinComb::factory(2).into()],
vec![],
vec![vec![Target { node: 0, slot: 0 }]],
OutPort { node: 0, field: 0 },
vec![OutField { node: 0, field: 0, name: "out".into() }],
);
let bp = Blueprint::new(vec![BlueprintNode::Composite(strategy)], vec![], vec![]);
@@ -964,7 +1140,7 @@ mod tests {
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 },
vec![OutField { node: 2, field: 0, name: "out".into() }],
);
// outer composite "strategy": the inner composite + a LinComb([1,-1])
let strategy = Composite::new(
@@ -972,7 +1148,7 @@ mod tests {
vec![BlueprintNode::Composite(fast_slow), LinComb::factory(2).into()],
vec![],
vec![vec![Target { node: 0, slot: 0 }]],
OutPort { node: 0, field: 0 },
vec![OutField { node: 0, field: 0, name: "out".into() }],
);
let bp = Blueprint::new(vec![BlueprintNode::Composite(strategy)], vec![], vec![]);
+1 -1
View File
@@ -35,7 +35,7 @@ mod blueprint;
mod harness;
mod report;
pub use blueprint::{Blueprint, BlueprintNode, CompileError, Composite, OutPort};
pub use blueprint::{Blueprint, BlueprintNode, CompileError, Composite, OutField};
pub use harness::{BootstrapError, Edge, Harness, SourceSpec, Target};
pub use report::{f64_field, summarize, RunManifest, RunMetrics, RunReport};
// #29: re-export the core scalar vocabulary a Blueprint builder needs