feat(0088): §A shared gate predicates — edge_kind_check, resolution helpers, try_bind
Iteration-1 §A of the construction service (#157): the surface-agnostic shared predicates the eager op-script path and the holistic finalize gates both call — no second validator (C24). - edge_kind_check (blueprint.rs): the per-edge producer/consumer kind check, lifted verbatim out of validate_wiring's edge loop into a pub(crate) fn that validate_wiring now calls — behaviour-preserving (same Bootstrap(KindMismatch) variant; the existing compile-time gate test stays green). - resolve_input_slot / resolve_output_field (builder.rs): the exactly-one-match name→index resolution extracted as pub(crate) fns over (&NodeSchema, &str), so the runtime-String op-script names share GraphBuilder's resolution; GraphBuilder delegates and maps to the unchanged BuildError variants. - try_bind + BindOpError (aura-core node.rs): the fallible Result twin of bind (bind keeps its panic contract and pinned messages verbatim; try_bind is a separate method reporting the same three conditions as values). - check_param_namespace_injective + validate_wiring widened to pub(crate) for the finalize-stage reuse by the upcoming GraphSession::finish (§B). Partial iteration: §B (the GraphSession/Op/replay surface + introspection) and the §A→§B integration land next (plan Tasks 4-8). compile_with_params / check_ports_connected are behaviourally unchanged. Full suite green; clippy clean. Plan correction folded in: Task 3's test originally referenced aura_std::Sma — infeasible inside aura-core (aura-std depends on aura-core; the reverse edge is a dependency cycle). Adapted to a local PrimitiveBuilder probe with identical assertions. refs #157
This commit is contained in:
@@ -43,7 +43,7 @@ pub use column::{Column, Window};
|
|||||||
pub use ctx::Ctx;
|
pub use ctx::Ctx;
|
||||||
pub use error::KindMismatch;
|
pub use error::KindMismatch;
|
||||||
pub use node::{
|
pub use node::{
|
||||||
zip_params, BoundParam, FieldSpec, Firing, Node, NodeSchema, ParamSpec, PortSpec,
|
zip_params, BindOpError, BoundParam, FieldSpec, Firing, Node, NodeSchema, ParamSpec, PortSpec,
|
||||||
PrimitiveBuilder,
|
PrimitiveBuilder,
|
||||||
};
|
};
|
||||||
pub use scalar::{Scalar, ScalarKind, Timestamp};
|
pub use scalar::{Scalar, ScalarKind, Timestamp};
|
||||||
|
|||||||
@@ -235,6 +235,65 @@ impl PrimitiveBuilder {
|
|||||||
});
|
});
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The fallible twin of [`bind`](Self::bind): same exactly-one-match + kind
|
||||||
|
/// rule, returning [`BindOpError`] instead of panicking. Used by the op-script
|
||||||
|
/// construction surface, where a bad param is rejected at the op, not a panic.
|
||||||
|
pub fn try_bind(mut self, slot: &str, value: Scalar) -> Result<Self, BindOpError> {
|
||||||
|
let matches: Vec<usize> = self
|
||||||
|
.schema
|
||||||
|
.params
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter(|(_, p)| p.name == slot)
|
||||||
|
.map(|(i, _)| i)
|
||||||
|
.collect();
|
||||||
|
let pos = match matches.as_slice() {
|
||||||
|
[pos] => *pos,
|
||||||
|
[] => return Err(BindOpError::UnknownParam(slot.to_string())),
|
||||||
|
_ => return Err(BindOpError::AmbiguousParam(slot.to_string())),
|
||||||
|
};
|
||||||
|
if value.kind() != self.schema.params[pos].kind {
|
||||||
|
return Err(BindOpError::KindMismatch {
|
||||||
|
param: slot.to_string(),
|
||||||
|
expected: self.schema.params[pos].kind,
|
||||||
|
got: value.kind(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let name = self.schema.params[pos].name.clone();
|
||||||
|
let kind = self.schema.params[pos].kind;
|
||||||
|
let mut orig = pos;
|
||||||
|
let mut prior: Vec<usize> = self.bound.iter().map(|b| b.pos).collect();
|
||||||
|
prior.sort_unstable();
|
||||||
|
for p in prior {
|
||||||
|
if p <= orig {
|
||||||
|
orig += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.bound.push(BoundParam { pos: orig, name, kind, value });
|
||||||
|
self.schema.params.remove(pos);
|
||||||
|
let inner = self.build;
|
||||||
|
self.build = Box::new(move |open: &[Cell]| {
|
||||||
|
let mut full = open.to_vec();
|
||||||
|
full.insert(pos, value.cell());
|
||||||
|
inner(&full)
|
||||||
|
});
|
||||||
|
Ok(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A fallible-bind fault — the `Result` twin of `bind`'s panics, for the
|
||||||
|
/// data-level construction surface (`GraphSession`, #157). `bind` keeps its
|
||||||
|
/// panic contract (and its pinned messages); `try_bind` reports the same three
|
||||||
|
/// conditions as values.
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub enum BindOpError {
|
||||||
|
/// No still-open param has this name.
|
||||||
|
UnknownParam(String),
|
||||||
|
/// More than one still-open param has this name.
|
||||||
|
AmbiguousParam(String),
|
||||||
|
/// The value's kind does not equal the param's declared kind.
|
||||||
|
KindMismatch { param: String, expected: ScalarKind, got: ScalarKind },
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A node's declared interface: its inputs (in order) and its output record — an
|
/// A node's declared interface: its inputs (in order) and its output record — an
|
||||||
@@ -550,6 +609,41 @@ mod tests {
|
|||||||
);
|
);
|
||||||
let _ = b.bind("length", Scalar::f64(2.0)); // F64 value for an I64 slot
|
let _ = b.bind("length", Scalar::f64(2.0)); // F64 value for an I64 slot
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn try_bind_ok_and_typed_errors() {
|
||||||
|
use super::BindOpError;
|
||||||
|
// A probe with one i64 param `length` (a real std node like `Sma` would
|
||||||
|
// pull in aura-std, but aura-core cannot dev-depend on it without a
|
||||||
|
// dev-cycle that compiles aura-core twice — incompatible `Scalar` types).
|
||||||
|
let probe = || {
|
||||||
|
PrimitiveBuilder::new(
|
||||||
|
"Probe",
|
||||||
|
NodeSchema {
|
||||||
|
inputs: vec![],
|
||||||
|
output: vec![],
|
||||||
|
params: vec![ParamSpec { name: "length".into(), kind: ScalarKind::I64 }],
|
||||||
|
},
|
||||||
|
|_| Box::new(Bare),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
// ok: exact name, matching kind
|
||||||
|
assert!(probe().try_bind("length", Scalar::i64(3)).is_ok());
|
||||||
|
// unknown param name
|
||||||
|
assert_eq!(
|
||||||
|
probe().try_bind("nope", Scalar::i64(3)).err(),
|
||||||
|
Some(BindOpError::UnknownParam("nope".into()))
|
||||||
|
);
|
||||||
|
// kind mismatch (f64 into an i64 param)
|
||||||
|
assert_eq!(
|
||||||
|
probe().try_bind("length", Scalar::f64(3.0)).err(),
|
||||||
|
Some(BindOpError::KindMismatch {
|
||||||
|
param: "length".into(),
|
||||||
|
expected: ScalarKind::I64,
|
||||||
|
got: ScalarKind::F64,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -563,7 +563,7 @@ fn resolve_ranges(space: &[ParamSpec], ranges: &[(String, ParamRange)]) -> Resul
|
|||||||
/// `param_space()` order is reported (the order is deterministic). Single source of
|
/// `param_space()` order is reported (the order is deterministic). Single source of
|
||||||
/// duplicate detection; called from `compile_with_params` and from both binders
|
/// duplicate detection; called from `compile_with_params` and from both binders
|
||||||
/// before name resolution.
|
/// before name resolution.
|
||||||
fn check_param_namespace_injective(space: &[ParamSpec]) -> Result<(), CompileError> {
|
pub(crate) fn check_param_namespace_injective(space: &[ParamSpec]) -> Result<(), CompileError> {
|
||||||
let mut seen = std::collections::HashSet::new();
|
let mut seen = std::collections::HashSet::new();
|
||||||
for p in space {
|
for p in space {
|
||||||
if !seen.insert(p.name.as_str()) {
|
if !seen.insert(p.name.as_str()) {
|
||||||
@@ -620,12 +620,34 @@ pub enum CompileError {
|
|||||||
DoubleWiredPort { node: usize, slot: usize },
|
DoubleWiredPort { node: usize, slot: usize },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The per-edge kind predicate, shared by `validate_wiring` (holistic) and the
|
||||||
|
/// eager `connect` op (`construction.rs`) — one check, two cadences (no second
|
||||||
|
/// validator). Looks the producer field + consumer slot up by index and rejects
|
||||||
|
/// a kind mismatch with the SAME variant bootstrap uses, so existing
|
||||||
|
/// compiled-graph tests stay green.
|
||||||
|
pub(crate) fn edge_kind_check(
|
||||||
|
from: &NodeSchema,
|
||||||
|
from_field: usize,
|
||||||
|
to: &NodeSchema,
|
||||||
|
slot: usize,
|
||||||
|
) -> Result<(), CompileError> {
|
||||||
|
let f = from.output.get(from_field).ok_or(CompileError::BadInteriorIndex)?;
|
||||||
|
let s = to.inputs.get(slot).ok_or(CompileError::BadInteriorIndex)?;
|
||||||
|
if f.kind != s.kind {
|
||||||
|
return Err(CompileError::Bootstrap(BootstrapError::KindMismatch {
|
||||||
|
producer: f.kind,
|
||||||
|
consumer: s.kind,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Pre-build structural validation via `signature()` (no node constructed): every
|
/// Pre-build structural validation via `signature()` (no node constructed): every
|
||||||
/// edge's producer field and consumer slot are in range and kind-matched; every
|
/// edge's producer field and consumer slot are in range and kind-matched; every
|
||||||
/// output re-export and role target is in range and kind-consistent. Recurses into
|
/// output re-export and role target is in range and kind-consistent. Recurses into
|
||||||
/// nested composites so the checks hold at every level. This is what lets `compile`
|
/// nested composites so the checks hold at every level. This is what lets `compile`
|
||||||
/// reject a wiring fault before any build closure fires.
|
/// reject a wiring fault before any build closure fires.
|
||||||
fn validate_wiring(
|
pub(crate) fn validate_wiring(
|
||||||
nodes: &[BlueprintNode],
|
nodes: &[BlueprintNode],
|
||||||
edges: &[Edge],
|
edges: &[Edge],
|
||||||
roles: &[Role],
|
roles: &[Role],
|
||||||
@@ -638,14 +660,7 @@ fn validate_wiring(
|
|||||||
for e in edges {
|
for e in edges {
|
||||||
let from = nodes.get(e.from).ok_or(CompileError::BadInteriorIndex)?.signature();
|
let from = nodes.get(e.from).ok_or(CompileError::BadInteriorIndex)?.signature();
|
||||||
let to = nodes.get(e.to).ok_or(CompileError::BadInteriorIndex)?.signature();
|
let to = nodes.get(e.to).ok_or(CompileError::BadInteriorIndex)?.signature();
|
||||||
let f = from.output.get(e.from_field).ok_or(CompileError::BadInteriorIndex)?;
|
edge_kind_check(&from, e.from_field, &to, e.slot)?;
|
||||||
let s = to.inputs.get(e.slot).ok_or(CompileError::BadInteriorIndex)?;
|
|
||||||
if f.kind != s.kind {
|
|
||||||
return Err(CompileError::Bootstrap(BootstrapError::KindMismatch {
|
|
||||||
producer: f.kind,
|
|
||||||
consumer: s.kind,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// roles: every target in range, and all targets of one role share a kind
|
// roles: every target in range, and all targets of one role share a kind
|
||||||
// (RoleKindMismatch — the existing variant, today read off built schema()).
|
// (RoleKindMismatch — the existing variant, today read off built schema()).
|
||||||
@@ -926,6 +941,34 @@ mod tests {
|
|||||||
use aura_std::{Bias, Ema, Recorder, SimBroker, Sma, Sub};
|
use aura_std::{Bias, Ema, Recorder, SimBroker, Sma, Sub};
|
||||||
use std::sync::mpsc;
|
use std::sync::mpsc;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn edge_kind_check_accepts_match_and_rejects_mismatch() {
|
||||||
|
use super::edge_kind_check;
|
||||||
|
use aura_core::{FieldSpec, NodeSchema, PortSpec, ScalarKind};
|
||||||
|
// producer: one f64 output field; consumer: slot 0 is f64, slot 1 is bool.
|
||||||
|
let from = NodeSchema {
|
||||||
|
inputs: vec![],
|
||||||
|
output: vec![FieldSpec { name: "v".into(), kind: ScalarKind::F64 }],
|
||||||
|
params: vec![],
|
||||||
|
};
|
||||||
|
let to = NodeSchema {
|
||||||
|
inputs: vec![
|
||||||
|
PortSpec { kind: ScalarKind::F64, firing: aura_core::Firing::Any, name: "a".into() },
|
||||||
|
PortSpec { kind: ScalarKind::Bool, firing: aura_core::Firing::Any, name: "b".into() },
|
||||||
|
],
|
||||||
|
output: vec![],
|
||||||
|
params: vec![],
|
||||||
|
};
|
||||||
|
assert!(edge_kind_check(&from, 0, &to, 0).is_ok());
|
||||||
|
assert_eq!(
|
||||||
|
edge_kind_check(&from, 0, &to, 1),
|
||||||
|
Err(CompileError::Bootstrap(BootstrapError::KindMismatch {
|
||||||
|
producer: ScalarKind::F64,
|
||||||
|
consumer: ScalarKind::Bool,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// Build + bootstrap + run + drain + summarize one swept point into a
|
/// Build + bootstrap + run + drain + summarize one swept point into a
|
||||||
/// `RunReport`, using a fresh harness per point (disjoint runs, C1). A free
|
/// `RunReport`, using a fresh harness per point (disjoint runs, C1). A free
|
||||||
/// `fn` (Copy + Sync) so it serves both as the `sweep`/binder closure and as a
|
/// `fn` (Copy + Sync) so it serves both as the `sweep`/binder closure and as a
|
||||||
|
|||||||
@@ -162,17 +162,14 @@ impl GraphBuilder {
|
|||||||
.schemas
|
.schemas
|
||||||
.get(p.node)
|
.get(p.node)
|
||||||
.ok_or(BuildError::BadHandle { node: p.node })?;
|
.ok_or(BuildError::BadHandle { node: p.node })?;
|
||||||
let m: Vec<usize> = schema
|
match resolve_input_slot(schema, p.name) {
|
||||||
.inputs
|
Ok(i) => Ok((p.node, i)),
|
||||||
.iter()
|
Err(PortResolveError::Unknown) => {
|
||||||
.enumerate()
|
Err(BuildError::UnknownInPort { node: p.node, name: p.name.to_string() })
|
||||||
.filter(|(_, port)| port.name == p.name)
|
}
|
||||||
.map(|(i, _)| i)
|
Err(PortResolveError::Ambiguous) => {
|
||||||
.collect();
|
Err(BuildError::AmbiguousInPort { node: p.node, name: p.name.to_string() })
|
||||||
match m.as_slice() {
|
}
|
||||||
[i] => Ok((p.node, *i)),
|
|
||||||
[] => Err(BuildError::UnknownInPort { node: p.node, name: p.name.to_string() }),
|
|
||||||
_ => Err(BuildError::AmbiguousInPort { node: p.node, name: p.name.to_string() }),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,19 +180,57 @@ impl GraphBuilder {
|
|||||||
.schemas
|
.schemas
|
||||||
.get(p.node)
|
.get(p.node)
|
||||||
.ok_or(BuildError::BadHandle { node: p.node })?;
|
.ok_or(BuildError::BadHandle { node: p.node })?;
|
||||||
|
match resolve_output_field(schema, p.name) {
|
||||||
|
Ok(i) => Ok(i),
|
||||||
|
Err(PortResolveError::Unknown) => {
|
||||||
|
Err(BuildError::UnknownOutPort { node: p.node, name: p.name.to_string() })
|
||||||
|
}
|
||||||
|
Err(PortResolveError::Ambiguous) => {
|
||||||
|
Err(BuildError::AmbiguousOutPort { node: p.node, name: p.name.to_string() })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A name→index resolution outcome, shared by `GraphBuilder` (mapped to
|
||||||
|
/// `BuildError`) and `GraphSession` (mapped to `OpError`). Over `&str`, so it
|
||||||
|
/// serves both `&'static str` literals and runtime document names.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub(crate) enum PortResolveError {
|
||||||
|
Unknown,
|
||||||
|
Ambiguous,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve an input-port name to its slot index by exactly-one-match.
|
||||||
|
pub(crate) fn resolve_input_slot(schema: &NodeSchema, name: &str) -> Result<usize, PortResolveError> {
|
||||||
let m: Vec<usize> = schema
|
let m: Vec<usize> = schema
|
||||||
.output
|
.inputs
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.filter(|(_, f)| f.name == p.name)
|
.filter(|(_, port)| port.name == name)
|
||||||
.map(|(i, _)| i)
|
.map(|(i, _)| i)
|
||||||
.collect();
|
.collect();
|
||||||
match m.as_slice() {
|
match m.as_slice() {
|
||||||
[i] => Ok(*i),
|
[i] => Ok(*i),
|
||||||
[] => Err(BuildError::UnknownOutPort { node: p.node, name: p.name.to_string() }),
|
[] => Err(PortResolveError::Unknown),
|
||||||
_ => Err(BuildError::AmbiguousOutPort { node: p.node, name: p.name.to_string() }),
|
_ => Err(PortResolveError::Ambiguous),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resolve an output-field name to its field index by exactly-one-match.
|
||||||
|
pub(crate) fn resolve_output_field(schema: &NodeSchema, name: &str) -> Result<usize, PortResolveError> {
|
||||||
|
let m: Vec<usize> = schema
|
||||||
|
.output
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter(|(_, f)| f.name == name)
|
||||||
|
.map(|(i, _)| i)
|
||||||
|
.collect();
|
||||||
|
match m.as_slice() {
|
||||||
|
[i] => Ok(*i),
|
||||||
|
[] => Err(PortResolveError::Unknown),
|
||||||
|
_ => Err(PortResolveError::Ambiguous),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -297,6 +332,19 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn shared_port_resolution_matches_exactly_one() {
|
||||||
|
use super::{resolve_input_slot, resolve_output_field, PortResolveError};
|
||||||
|
use crate::BlueprintNode;
|
||||||
|
use aura_std::Sub;
|
||||||
|
let schema = BlueprintNode::from(Sub::builder()).signature(); // lhs, rhs -> value
|
||||||
|
assert_eq!(resolve_input_slot(&schema, "lhs"), Ok(0));
|
||||||
|
assert_eq!(resolve_input_slot(&schema, "rhs"), Ok(1));
|
||||||
|
assert_eq!(resolve_input_slot(&schema, "nope"), Err(PortResolveError::Unknown));
|
||||||
|
assert_eq!(resolve_output_field(&schema, "value"), Ok(0));
|
||||||
|
assert_eq!(resolve_output_field(&schema, "nope"), Err(PortResolveError::Unknown));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn unknown_in_port_is_reported_at_build() {
|
fn unknown_in_port_is_reported_at_build() {
|
||||||
let mut g = GraphBuilder::new("x");
|
let mut g = GraphBuilder::new("x");
|
||||||
|
|||||||
@@ -310,17 +310,30 @@ crates/aura-core/src/node.rs`):
|
|||||||
#[test]
|
#[test]
|
||||||
fn try_bind_ok_and_typed_errors() {
|
fn try_bind_ok_and_typed_errors() {
|
||||||
use super::BindOpError;
|
use super::BindOpError;
|
||||||
use aura_std::Sma; // SMA has one i64 param `length`
|
// A probe with one i64 param `length` (a real std node like `Sma` would
|
||||||
|
// pull in aura-std, but aura-core cannot dev-depend on it without a
|
||||||
|
// dev-cycle that compiles aura-core twice — incompatible `Scalar` types).
|
||||||
|
let probe = || {
|
||||||
|
PrimitiveBuilder::new(
|
||||||
|
"Probe",
|
||||||
|
NodeSchema {
|
||||||
|
inputs: vec![],
|
||||||
|
output: vec![],
|
||||||
|
params: vec![ParamSpec { name: "length".into(), kind: ScalarKind::I64 }],
|
||||||
|
},
|
||||||
|
|_| Box::new(Bare),
|
||||||
|
)
|
||||||
|
};
|
||||||
// ok: exact name, matching kind
|
// ok: exact name, matching kind
|
||||||
assert!(Sma::builder().try_bind("length", Scalar::i64(3)).is_ok());
|
assert!(probe().try_bind("length", Scalar::i64(3)).is_ok());
|
||||||
// unknown param name
|
// unknown param name
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Sma::builder().try_bind("nope", Scalar::i64(3)).err(),
|
probe().try_bind("nope", Scalar::i64(3)).err(),
|
||||||
Some(BindOpError::UnknownParam("nope".into()))
|
Some(BindOpError::UnknownParam("nope".into()))
|
||||||
);
|
);
|
||||||
// kind mismatch (f64 into an i64 param)
|
// kind mismatch (f64 into an i64 param)
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Sma::builder().try_bind("length", Scalar::f64(3.0)).err(),
|
probe().try_bind("length", Scalar::f64(3.0)).err(),
|
||||||
Some(BindOpError::KindMismatch {
|
Some(BindOpError::KindMismatch {
|
||||||
param: "length".into(),
|
param: "length".into(),
|
||||||
expected: ScalarKind::I64,
|
expected: ScalarKind::I64,
|
||||||
@@ -332,6 +345,12 @@ fn try_bind_ok_and_typed_errors() {
|
|||||||
|
|
||||||
> `.err()` not `.unwrap_err()`: the `Ok` type `PrimitiveBuilder` is not `Debug`
|
> `.err()` not `.unwrap_err()`: the `Ok` type `PrimitiveBuilder` is not `Debug`
|
||||||
> (it holds a build closure), mirroring `builder.rs`'s tests.
|
> (it holds a build closure), mirroring `builder.rs`'s tests.
|
||||||
|
>
|
||||||
|
> **Plan correction (orchestrator, post-block):** the original Step-1 test
|
||||||
|
> referenced `aura_std::Sma`, infeasible inside `aura-core` (aura-std depends on
|
||||||
|
> aura-core; the reverse edge is a dependency cycle). Replaced with a local
|
||||||
|
> `PrimitiveBuilder::new` probe carrying identical assertions — `Bare` is the
|
||||||
|
> test module's existing zero-output node.
|
||||||
|
|
||||||
- [ ] **Step 2: Run test to verify it fails**
|
- [ ] **Step 2: Run test to verify it fails**
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user