feat(aura-engine): reject unwired or double-wired input ports
A graph that leaves an interior input slot unconnected, or wires one slot from more than one producer, is now a compile-time error instead of a silently-wrong run. Until now an unwired interior slot was accepted and bootstrapped to an empty column (harness.rs:137-148), so a forgotten connection ran a deterministic but wrong backtest; two producers into one slot — ill-formed, since a slot holds one column — was likewise uncompiled-against. A single name-free check, `check_ports_connected`, is added to `validate_wiring` after the existing index/kind checks and before the nested-composite recursion. It counts coverage of each (interior-node, slot) uniformly across interior edges and role targets, and rejects zero coverage (new `CompileError::UnconnectedPort`) or >1 (new `CompileError::DoubleWiredPort`). Because `validate_wiring` is called once from `compile_with_params` and recurses, both the raw `Composite::new` path and the `GraphBuilder::build()` path inherit it at every nesting level with no builder-side code (mirrors `check_param_namespace_injective`, the param-side sibling). A composite's own open input roles (source: None) are coverage providers — the wired-by-enclosing boundary, the root already guarded by UnboundRootRole — not consumers. Index-based and name-free: nothing reaches the compilat, so C23 holds. Supporting fix: the check computes `signature()` on every interior node before the recursion validates that node's interior, so `derive_signature` and `interior_slot_kind` are made bounds-total (a placeholder kind for an out-of-range OutField/target, never a panic; the real fault is still reported by validate_wiring's guarded OutputPortOutOfRange/BadInteriorIndex). This latent panic was exposed by the new check, not introduced by it. Test maintenance (blast radius enumerated empirically, not hand-traced): four existing tests relied incidentally on under-wiring being accepted — three negative tests get a covering root role so their intended deep fault still surfaces via the recursion, and one param-order nesting test is fully wired (param order unchanged). Six new tests: five raw-surface (unwired, double-wire via edge+role and edge+edge, nested, open-role boundary) and one builder-surface forgotten-leg (proving the GraphBuilder inherits the check). Narrowed scope of #65: the original "promote names to load-bearing wiring keys / close the exposure-price swap structurally" goal was dropped — #64 already moved authoring to handles + visible port names, so the residual same-kind swap is a valid-but-wrong name choice no structural check catches, and a structural fix would push domain semantics into the domain-free engine (C10/C7). This ships the name-free half that stands on its own: wiring completeness, which catches forgotten and doubled connections. Verified: cargo test --workspace 231 passed / 0 failed; cargo clippy --workspace --all-targets -- -D warnings clean. closes #65
This commit is contained in:
@@ -271,6 +271,32 @@ mod tests {
|
||||
assert_eq!(built_flat.sources, hand_flat.sources);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unconnected_port_rejected_on_builder_surface() {
|
||||
use crate::CompileError;
|
||||
use aura_core::Scalar;
|
||||
// The forgotten-exposure-leg harness: every port/field name resolves, so
|
||||
// build() succeeds; but broker.input("exposure") is never connected, so the
|
||||
// wiring-totality check rejects it at compile (broker is node 1, slot 0).
|
||||
let (tx_eq, _r1) = mpsc::channel();
|
||||
let mut g = GraphBuilder::new("root");
|
||||
let expo = g.add(Exposure::builder());
|
||||
let broker = g.add(SimBroker::builder(0.0001));
|
||||
let eq = g.add(Recorder::builder(vec![ScalarKind::F64], Firing::Any, tx_eq));
|
||||
let price = g.source_role("price", ScalarKind::F64);
|
||||
g.feed(price, [expo.input("signal"), broker.input("price")]);
|
||||
// BUG: g.connect(expo.output("exposure"), broker.input("exposure")) missing.
|
||||
g.connect(broker.output("equity"), eq.input("col[0]"));
|
||||
let compiled = g
|
||||
.build()
|
||||
.expect("all port/field names resolve")
|
||||
.compile_with_params(&[Scalar::F64(0.5)]);
|
||||
assert_eq!(
|
||||
compiled.err(),
|
||||
Some(CompileError::UnconnectedPort { node: 1, slot: 0 })
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_in_port_is_reported_at_build() {
|
||||
let mut g = GraphBuilder::new("x");
|
||||
|
||||
Reference in New Issue
Block a user