plan: 0040 wiring-totality-check
Seven-task plan for the wiring-totality check (parent spec docs/specs/0040-wiring-totality-check.md,07b1ae1+ blast-radius correction69b16f7): add the two CompileError variants + check_ports_connected + its validate_wiring call site (T1); re-wire the three under-wired negative tests with a covering root role (T2); fully wire the param-order nesting test (T3); add five raw-surface tests — unwired, double-wire via edge+role and edge+edge, nested, open-role boundary (T4); add the builder-surface forgotten-leg test (T5); land the C8 ledger realization note (T6); full-suite + clippy gate (T7). The blast radius (T1's expected-4-failures gate, T2/T3's fixes) was enumerated empirically by a throwaway probe, not a hand-trace, after the hand-trace under-counted it by one (param_space_mirrors_..._under_nesting). refs #65
This commit is contained in:
@@ -0,0 +1,586 @@
|
||||
# Wiring-totality check — Implementation Plan
|
||||
|
||||
> **Parent spec:** `docs/specs/0040-wiring-totality-check.md`
|
||||
>
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: use the `implement` skill to run
|
||||
> this plan. Steps use `- [ ]` checkboxes for tracking.
|
||||
|
||||
**Goal:** Reject a graph that leaves an interior input slot unwired or wires one
|
||||
slot from more than one producer, at compile time, via a single name-free
|
||||
structural check in `validate_wiring`.
|
||||
|
||||
**Architecture:** A new `check_ports_connected(nodes, edges, roles)` is added to
|
||||
`validate_wiring` (crates/aura-engine/src/blueprint.rs), 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 any interior input slot covered 0 times (`UnconnectedPort`) or >1 times
|
||||
(`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. The blast radius is
|
||||
exactly four currently-green tests (empirically probed), fixed here.
|
||||
|
||||
**Tech Stack:** `crates/aura-engine/src/blueprint.rs` (the check, the two
|
||||
`CompileError` variants, raw-surface tests, four flipped-test fixes),
|
||||
`crates/aura-engine/src/builder.rs` (one builder-surface test), `docs/design/INDEX.md`
|
||||
(C8 realization note).
|
||||
|
||||
---
|
||||
|
||||
**Files this plan creates or modifies:**
|
||||
|
||||
- Modify: `crates/aura-engine/src/blueprint.rs` — add two `CompileError` variants
|
||||
(~:489), add `check_ports_connected` + its call site in `validate_wiring`
|
||||
(~:538/:546), re-wire 3 negative tests (~:1382/:1452/:1473), fully wire 1
|
||||
param-order test (~:1877), add 5 raw-surface tests (test module).
|
||||
- Modify: `crates/aura-engine/src/builder.rs` — add 1 builder-surface negative test
|
||||
(after `builder_harness_compiles_identically_to_hand_wired`, ~:272).
|
||||
- Modify: `docs/design/INDEX.md` — add the C8 cycle-0040 realization note (after the
|
||||
cycle-0027 realization block, ~:316).
|
||||
|
||||
---
|
||||
|
||||
### Task 1: The wiring-totality check (two variants + the function + call site)
|
||||
|
||||
**Files:**
|
||||
- Modify: `crates/aura-engine/src/blueprint.rs:489` (CompileError), `:538`/`:546`
|
||||
(validate_wiring + new fn)
|
||||
|
||||
- [ ] **Step 1: Add the two `CompileError` variants**
|
||||
|
||||
In `crates/aura-engine/src/blueprint.rs`, replace:
|
||||
|
||||
```rust
|
||||
/// A root input role `role` has no bound source (`source: None`) — an open port
|
||||
/// at the root, which has no enclosing graph to wire it. Only a fully source-
|
||||
/// bound composite is runnable.
|
||||
UnboundRootRole { role: usize },
|
||||
}
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```rust
|
||||
/// A root input role `role` has no bound source (`source: None`) — an open port
|
||||
/// at the root, which has no enclosing graph to wire it. Only a fully source-
|
||||
/// bound composite is runnable.
|
||||
UnboundRootRole { role: usize },
|
||||
/// An interior node's input `slot` is covered by no edge and no role target — a
|
||||
/// required port left unconnected (it would bootstrap a silent empty column).
|
||||
UnconnectedPort { node: usize, slot: usize },
|
||||
/// An interior node's input `slot` is covered by more than one edge/role target
|
||||
/// combined — a slot holds exactly one column, so >1 producer is ill-formed.
|
||||
DoubleWiredPort { node: usize, slot: usize },
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add `check_ports_connected` and call it from `validate_wiring`**
|
||||
|
||||
In `crates/aura-engine/src/blueprint.rs`, replace the tail of `validate_wiring`:
|
||||
|
||||
```rust
|
||||
// recurse into nested composites
|
||||
for item in nodes {
|
||||
if let BlueprintNode::Composite(c) = item {
|
||||
validate_wiring(c.nodes(), c.edges(), c.input_roles(), c.output())?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
with (insert the call before the recursion, and define the new fn right after):
|
||||
|
||||
```rust
|
||||
// wiring totality: every interior input slot covered by exactly one wiring act
|
||||
check_ports_connected(nodes, edges, roles)?;
|
||||
// recurse into nested composites
|
||||
for item in nodes {
|
||||
if let BlueprintNode::Composite(c) = item {
|
||||
validate_wiring(c.nodes(), c.edges(), c.input_roles(), c.output())?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Every interior node's every declared input slot must be covered by exactly one
|
||||
/// wiring act — one interior edge OR one role target, counted uniformly. Zero = a
|
||||
/// forgotten connection (would bootstrap a silent empty column); >1 = an ill-formed
|
||||
/// slot (a slot holds one column). Index-based, name-free; presupposes in-range
|
||||
/// edge/role indices (runs after the existing index-range checks). Mirrors the
|
||||
/// single-site shape of `check_param_namespace_injective` (the wiring-side sibling).
|
||||
fn check_ports_connected(
|
||||
nodes: &[BlueprintNode],
|
||||
edges: &[Edge],
|
||||
roles: &[Role],
|
||||
) -> Result<(), CompileError> {
|
||||
let mut coverage: std::collections::HashMap<(usize, usize), usize> =
|
||||
std::collections::HashMap::new();
|
||||
for e in edges {
|
||||
*coverage.entry((e.to, e.slot)).or_insert(0) += 1;
|
||||
}
|
||||
for role in roles {
|
||||
for t in &role.targets {
|
||||
*coverage.entry((t.node, t.slot)).or_insert(0) += 1;
|
||||
}
|
||||
}
|
||||
for (n, item) in nodes.iter().enumerate() {
|
||||
for slot in 0..item.signature().inputs.len() {
|
||||
match coverage.get(&(n, slot)).copied().unwrap_or(0) {
|
||||
1 => {}
|
||||
0 => return Err(CompileError::UnconnectedPort { node: n, slot }),
|
||||
_ => return Err(CompileError::DoubleWiredPort { node: n, slot }),
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Build and confirm exactly the four expected flips**
|
||||
|
||||
Run: `cargo build -p aura-engine`
|
||||
Expected: builds with 0 errors (the two `usize`-only variants break no derive or
|
||||
exhaustive match; `HashMap` is used by its full path, no new import).
|
||||
|
||||
Run: `cargo test -p aura-engine 2>&1 | tail -3`
|
||||
Expected: `test result: FAILED. ... 4 failed ...` — and the four are exactly
|
||||
`bad_interior_index_rejected`, `role_kind_mismatch_rejected`,
|
||||
`output_port_out_of_range_rejected`, and
|
||||
`param_space_mirrors_compiled_flat_node_param_order_under_nesting`. (Any other
|
||||
failure is an un-enumerated flip — STOP and re-survey; the probe established these
|
||||
four are the complete set.)
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Re-wire the three under-wired negative tests (covering root role)
|
||||
|
||||
Each wraps a faulty composite `c` in a root with empty edges + empty roles, so the
|
||||
new check pre-empts the deep fault with `UnconnectedPort` at the root level. Add a
|
||||
covering root role targeting `c`'s one input slot (mirroring the existing pattern at
|
||||
blueprint.rs:1414). The deep fault then surfaces via the recursion (the index/kind/
|
||||
output checks inside `validate_wiring(c)` run before `c`'s own connectivity check),
|
||||
so each STILL asserts its original variant.
|
||||
|
||||
**Files:**
|
||||
- Modify: `crates/aura-engine/src/blueprint.rs:1382`, `:1452`, `:1473`
|
||||
|
||||
- [ ] **Step 1: Re-wire `bad_interior_index_rejected`**
|
||||
|
||||
Replace:
|
||||
|
||||
```rust
|
||||
let bp = Composite::new(
|
||||
"root",
|
||||
vec![BlueprintNode::Composite(c)],
|
||||
vec![],
|
||||
vec![],
|
||||
vec![], // output
|
||||
);
|
||||
// the Ok arm holds Box<dyn Node> (not Debug), so assert via the Err arm.
|
||||
assert_eq!(bp.compile().err(), Some(CompileError::BadInteriorIndex));
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```rust
|
||||
let bp = Composite::new(
|
||||
"root",
|
||||
vec![BlueprintNode::Composite(c)],
|
||||
vec![],
|
||||
vec![Role { name: "src".into(), targets: vec![Target { node: 0, slot: 0 }], source: Some(ScalarKind::F64) }],
|
||||
vec![], // output
|
||||
);
|
||||
// the Ok arm holds Box<dyn Node> (not Debug), so assert via the Err arm.
|
||||
assert_eq!(bp.compile().err(), Some(CompileError::BadInteriorIndex));
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Re-wire `role_kind_mismatch_rejected`**
|
||||
|
||||
Replace:
|
||||
|
||||
```rust
|
||||
let bp = Composite::new(
|
||||
"root",
|
||||
vec![BlueprintNode::Composite(c)],
|
||||
vec![],
|
||||
vec![],
|
||||
vec![], // output
|
||||
);
|
||||
// the Ok arm holds Box<dyn Node> (not Debug), so assert via the Err arm.
|
||||
assert_eq!(bp.compile().err(), Some(CompileError::RoleKindMismatch { role: 0 }));
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```rust
|
||||
let bp = Composite::new(
|
||||
"root",
|
||||
vec![BlueprintNode::Composite(c)],
|
||||
vec![],
|
||||
vec![Role { name: "src".into(), targets: vec![Target { node: 0, slot: 0 }], source: Some(ScalarKind::F64) }],
|
||||
vec![], // output
|
||||
);
|
||||
// the Ok arm holds Box<dyn Node> (not Debug), so assert via the Err arm.
|
||||
assert_eq!(bp.compile().err(), Some(CompileError::RoleKindMismatch { role: 0 }));
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Re-wire `output_port_out_of_range_rejected`**
|
||||
|
||||
Replace:
|
||||
|
||||
```rust
|
||||
let bp = Composite::new(
|
||||
"root",
|
||||
vec![BlueprintNode::Composite(c)],
|
||||
vec![],
|
||||
vec![],
|
||||
vec![], // output
|
||||
);
|
||||
// the Ok arm holds Box<dyn Node> (not Debug), so assert via the Err arm.
|
||||
assert_eq!(bp.compile().err(), Some(CompileError::OutputPortOutOfRange));
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```rust
|
||||
let bp = Composite::new(
|
||||
"root",
|
||||
vec![BlueprintNode::Composite(c)],
|
||||
vec![],
|
||||
vec![Role { name: "src".into(), targets: vec![Target { node: 0, slot: 0 }], source: Some(ScalarKind::F64) }],
|
||||
vec![], // output
|
||||
);
|
||||
// the Ok arm holds Box<dyn Node> (not Debug), so assert via the Err arm.
|
||||
assert_eq!(bp.compile().err(), Some(CompileError::OutputPortOutOfRange));
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Verify the three pass**
|
||||
|
||||
Run: `cargo test -p aura-engine bad_interior_index_rejected`
|
||||
Expected: PASS (`1 passed`).
|
||||
Run: `cargo test -p aura-engine role_kind_mismatch_rejected`
|
||||
Expected: PASS (`1 passed`).
|
||||
Run: `cargo test -p aura-engine output_port_out_of_range_rejected`
|
||||
Expected: PASS (`1 passed`).
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Fully wire the param-order nesting test
|
||||
|
||||
`param_space_mirrors_compiled_flat_node_param_order_under_nesting` compiles a
|
||||
deliberately under-wired `strategy` (LinComb's two `term` inputs never fed) only to
|
||||
read its flat param order. Fully wire it: fan `fast_slow`'s output into both LinComb
|
||||
terms, and add a covering root role for `strategy`'s `price` input. Wiring changes no
|
||||
node and no param, so the asserted `space == from_compilat` equality is unchanged.
|
||||
|
||||
**Files:**
|
||||
- Modify: `crates/aura-engine/src/blueprint.rs:1877`
|
||||
|
||||
- [ ] **Step 1: Wire the `strategy` edges and the root role**
|
||||
|
||||
In the function `param_space_mirrors_compiled_flat_node_param_order_under_nesting`,
|
||||
replace (this span is unique to that function — it ends with the
|
||||
`"nested composite compiles"` compile line, which the sibling param-order test lacks):
|
||||
|
||||
```rust
|
||||
let strategy = Composite::new(
|
||||
"strategy",
|
||||
vec![BlueprintNode::Composite(fast_slow), LinComb::builder(2).into()],
|
||||
vec![],
|
||||
vec![Role { name: "price".into(), targets: vec![Target { node: 0, slot: 0 }], source: None }],
|
||||
vec![OutField { node: 0, field: 0, name: "out".into() }],
|
||||
);
|
||||
let bp = Composite::new(
|
||||
"root",
|
||||
vec![BlueprintNode::Composite(strategy)],
|
||||
vec![],
|
||||
vec![],
|
||||
vec![], // output
|
||||
);
|
||||
|
||||
// the aggregated, path-qualified projection (borrows; take it first since
|
||||
// compile() consumes self — same ordering as the single-level mirror test)
|
||||
let space = bp.param_space();
|
||||
|
||||
// the same blueprint, compiled to its flat node array; each flat node's
|
||||
// own declared params, concatenated in flat-node order
|
||||
let flat = bp.compile_with_params(&[Scalar::I64(2), Scalar::I64(4), Scalar::F64(1.0), Scalar::F64(-1.0)]).expect("nested composite compiles");
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```rust
|
||||
let strategy = Composite::new(
|
||||
"strategy",
|
||||
vec![BlueprintNode::Composite(fast_slow), LinComb::builder(2).into()],
|
||||
// fan fast_slow's output into both LinComb terms so every interior slot
|
||||
// is wired (the totality check, cycle 0040); param order is unaffected.
|
||||
vec![
|
||||
Edge { from: 0, to: 1, slot: 0, from_field: 0 },
|
||||
Edge { from: 0, to: 1, slot: 1, from_field: 0 },
|
||||
],
|
||||
vec![Role { name: "price".into(), targets: vec![Target { node: 0, slot: 0 }], source: None }],
|
||||
vec![OutField { node: 0, field: 0, name: "out".into() }],
|
||||
);
|
||||
let bp = Composite::new(
|
||||
"root",
|
||||
vec![BlueprintNode::Composite(strategy)],
|
||||
vec![],
|
||||
vec![Role { name: "price".into(), targets: vec![Target { node: 0, slot: 0 }], source: Some(ScalarKind::F64) }],
|
||||
vec![], // output
|
||||
);
|
||||
|
||||
// the aggregated, path-qualified projection (borrows; take it first since
|
||||
// compile() consumes self — same ordering as the single-level mirror test)
|
||||
let space = bp.param_space();
|
||||
|
||||
// the same blueprint, compiled to its flat node array; each flat node's
|
||||
// own declared params, concatenated in flat-node order
|
||||
let flat = bp.compile_with_params(&[Scalar::I64(2), Scalar::I64(4), Scalar::F64(1.0), Scalar::F64(-1.0)]).expect("nested composite compiles");
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify it passes**
|
||||
|
||||
Run: `cargo test -p aura-engine param_space_mirrors_compiled_flat_node_param_order_under_nesting`
|
||||
Expected: PASS (`1 passed`).
|
||||
|
||||
---
|
||||
|
||||
### Task 4: New raw-surface tests (unwired, double-wired, nested, open-role boundary)
|
||||
|
||||
Add these five tests to the `#[cfg(test)] mod tests` block in
|
||||
`crates/aura-engine/src/blueprint.rs` (alongside the existing negative tests). They
|
||||
use the in-module helpers `pass1()` (one f64 input "in", one output) and `sink_f64()`
|
||||
(one f64 input "in", no output), and the in-scope `Composite`, `Edge`, `Role`,
|
||||
`Target`, `OutField`, `BlueprintNode`, `CompileError`, `ScalarKind`.
|
||||
|
||||
**Files:**
|
||||
- Modify: `crates/aura-engine/src/blueprint.rs` (test module)
|
||||
|
||||
- [ ] **Step 1: Add the five tests**
|
||||
|
||||
```rust
|
||||
#[test]
|
||||
fn unconnected_interior_slot_rejected() {
|
||||
// pass1 is fed by a role; sink_f64's one input is left unwired -> rejected.
|
||||
let bp = Composite::new(
|
||||
"root",
|
||||
vec![pass1(), sink_f64()],
|
||||
vec![],
|
||||
vec![Role { name: "src".into(), targets: vec![Target { node: 0, slot: 0 }], source: Some(ScalarKind::F64) }],
|
||||
vec![],
|
||||
);
|
||||
assert_eq!(
|
||||
bp.compile().err(),
|
||||
Some(CompileError::UnconnectedPort { node: 1, slot: 0 })
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn double_wired_slot_rejected_edge_and_role() {
|
||||
// sink_f64's one input is targeted by BOTH an edge (from pass1) and a role.
|
||||
let bp = Composite::new(
|
||||
"root",
|
||||
vec![pass1(), sink_f64()],
|
||||
vec![Edge { from: 0, to: 1, slot: 0, from_field: 0 }],
|
||||
vec![
|
||||
Role { name: "src".into(), targets: vec![Target { node: 0, slot: 0 }], source: Some(ScalarKind::F64) },
|
||||
Role { name: "extra".into(), targets: vec![Target { node: 1, slot: 0 }], source: Some(ScalarKind::F64) },
|
||||
],
|
||||
vec![],
|
||||
);
|
||||
assert_eq!(
|
||||
bp.compile().err(),
|
||||
Some(CompileError::DoubleWiredPort { node: 1, slot: 0 })
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn double_wired_slot_rejected_two_edges() {
|
||||
// two producers' edges land on sink_f64's single input slot.
|
||||
let bp = Composite::new(
|
||||
"root",
|
||||
vec![pass1(), pass1(), sink_f64()],
|
||||
vec![
|
||||
Edge { from: 0, to: 2, slot: 0, from_field: 0 },
|
||||
Edge { from: 1, to: 2, slot: 0, from_field: 0 },
|
||||
],
|
||||
vec![
|
||||
Role { name: "a".into(), targets: vec![Target { node: 0, slot: 0 }], source: Some(ScalarKind::F64) },
|
||||
Role { name: "b".into(), targets: vec![Target { node: 1, slot: 0 }], source: Some(ScalarKind::F64) },
|
||||
],
|
||||
vec![],
|
||||
);
|
||||
assert_eq!(
|
||||
bp.compile().err(),
|
||||
Some(CompileError::DoubleWiredPort { node: 2, slot: 0 })
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unconnected_slot_in_nested_composite_rejected() {
|
||||
// inner composite c: pass1 (fed by c's role) + sink_f64 (interior slot
|
||||
// unwired). The root covers c's one input, so the fault surfaces only via
|
||||
// the recursion into c -> UnconnectedPort at c's interior index (1, 0).
|
||||
let c = Composite::new(
|
||||
"c",
|
||||
vec![pass1(), sink_f64()],
|
||||
vec![],
|
||||
vec![Role { name: "in".into(), targets: vec![Target { node: 0, slot: 0 }], source: None }],
|
||||
vec![OutField { node: 0, field: 0, name: "out".into() }],
|
||||
);
|
||||
let bp = Composite::new(
|
||||
"root",
|
||||
vec![BlueprintNode::Composite(c)],
|
||||
vec![],
|
||||
vec![Role { name: "src".into(), targets: vec![Target { node: 0, slot: 0 }], source: Some(ScalarKind::F64) }],
|
||||
vec![],
|
||||
);
|
||||
assert_eq!(
|
||||
bp.compile().err(),
|
||||
Some(CompileError::UnconnectedPort { node: 1, slot: 0 })
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn open_role_provider_is_not_flagged_unconnected() {
|
||||
// a composite whose OPEN input role (source: None) feeds its interior slot,
|
||||
// used as a nested node with that role covered by the enclosing root, must
|
||||
// compile — the open role is a provider, not an unwired consumer.
|
||||
let c = Composite::new(
|
||||
"c",
|
||||
vec![pass1()],
|
||||
vec![],
|
||||
vec![Role { name: "in".into(), targets: vec![Target { node: 0, slot: 0 }], source: None }],
|
||||
vec![OutField { node: 0, field: 0, name: "out".into() }],
|
||||
);
|
||||
let bp = Composite::new(
|
||||
"root",
|
||||
vec![BlueprintNode::Composite(c)],
|
||||
vec![],
|
||||
vec![Role { name: "src".into(), targets: vec![Target { node: 0, slot: 0 }], source: Some(ScalarKind::F64) }],
|
||||
vec![],
|
||||
);
|
||||
assert!(bp.compile().is_ok(), "open role as provider must not be mis-flagged");
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify the five pass**
|
||||
|
||||
Run: `cargo test -p aura-engine unconnected_interior_slot_rejected`
|
||||
Expected: PASS (`1 passed`).
|
||||
Run: `cargo test -p aura-engine double_wired_slot_rejected_edge_and_role`
|
||||
Expected: PASS (`1 passed`).
|
||||
Run: `cargo test -p aura-engine double_wired_slot_rejected_two_edges`
|
||||
Expected: PASS (`1 passed`).
|
||||
Run: `cargo test -p aura-engine unconnected_slot_in_nested_composite_rejected`
|
||||
Expected: PASS (`1 passed`).
|
||||
Run: `cargo test -p aura-engine open_role_provider_is_not_flagged_unconnected`
|
||||
Expected: PASS (`1 passed`).
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Builder-surface forgotten-leg test
|
||||
|
||||
Mirror `builder_harness_compiles_identically_to_hand_wired` but omit the
|
||||
exposure→broker connection: every name resolves so `build()` succeeds, but
|
||||
`compile_with_params` rejects the uncovered broker exposure slot. Proves the
|
||||
`GraphBuilder` surface inherits the check.
|
||||
|
||||
**Files:**
|
||||
- Modify: `crates/aura-engine/src/builder.rs` (test module, after
|
||||
`builder_harness_compiles_identically_to_hand_wired`)
|
||||
|
||||
- [ ] **Step 1: Add the test**
|
||||
|
||||
```rust
|
||||
#[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 })
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify it passes**
|
||||
|
||||
Run: `cargo test -p aura-engine unconnected_port_rejected_on_builder_surface`
|
||||
Expected: PASS (`1 passed`).
|
||||
|
||||
---
|
||||
|
||||
### Task 6: C8 ledger realization note
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/design/INDEX.md:316` (after the cycle-0027 realization block, before
|
||||
the blank line preceding `### C9`)
|
||||
|
||||
- [ ] **Step 1: Insert the cycle-0040 realization note**
|
||||
|
||||
Find the end of the C8 "Realization (cycle 0027 — name input ports…)" block (the
|
||||
paragraph ending "…a name-consuming validation is its own future cycle.", ~line 316),
|
||||
and insert a blank line then this paragraph immediately after it:
|
||||
|
||||
```text
|
||||
**Realization (cycle 0040 — wiring totality: every input slot connected exactly
|
||||
once, #65).** The node contract's "the engine provides a window into each input"
|
||||
presupposes each input is actually fed; until now an unwired interior input slot was
|
||||
accepted and bootstrapped to a silent empty column (`harness.rs`), and two producers
|
||||
into one slot — ill-formed, since a slot holds one column — was likewise
|
||||
uncompiled-against. Cycle 0040 makes a valid graph **total and single-valued** over
|
||||
its interior input slots: `check_ports_connected` (in `validate_wiring`, run at every
|
||||
nesting level) requires every interior node's every declared input slot to be covered
|
||||
by **exactly one** wiring act — one interior `Edge { to, slot }` or one role
|
||||
`Target { node, slot }`, edges and role targets counted uniformly. Zero coverage is
|
||||
`CompileError::UnconnectedPort`, more than one is `CompileError::DoubleWiredPort`. A
|
||||
composite's **own** input roles (`source: None`) are coverage *providers* — the
|
||||
wired-by-enclosing boundary, the root case already guarded by `UnboundRootRole` —
|
||||
never consumers, so only interior input slots are subject to the rule. There is **no
|
||||
optional-input concept**: every declared input port is required (no shipped node runs
|
||||
meaningfully without one; an unwarmed mode-A input is *wired-but-not-yet-valued*, not
|
||||
unwired). The check is **index-based and name-free** — it touches no name machinery
|
||||
and emits nothing into the compilat, so C23 is untouched (it proves the existing
|
||||
raw-index wiring is total and single-valued). Inherited identically by the raw
|
||||
`Composite::new` path and the `GraphBuilder::build()` path (both compile via
|
||||
`compile_with_params`).
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Confirm the note landed**
|
||||
|
||||
Run: `grep -c "cycle 0040 — wiring totality" docs/design/INDEX.md`
|
||||
Expected: `1`
|
||||
|
||||
---
|
||||
|
||||
### Task 7: Full-suite + lint gate
|
||||
|
||||
**Files:** none (verification only)
|
||||
|
||||
- [ ] **Step 1: Full workspace test**
|
||||
|
||||
Run: `cargo test --workspace 2>&1 | grep -E "test result:" | grep -v "0 failed" | head`
|
||||
Expected: no output (every `test result:` line reports `0 failed`).
|
||||
|
||||
- [ ] **Step 2: Clippy**
|
||||
|
||||
Run: `cargo clippy --workspace --all-targets -- -D warnings`
|
||||
Expected: finishes with 0 warnings/errors.
|
||||
Reference in New Issue
Block a user