Add a typed-handle GraphBuilder for name-based blueprint wiring #64

Closed
opened 2026-06-14 01:58:07 +02:00 by Brummel · 0 comments
Owner

Problem

Blueprint topology is wired today purely by raw positional indices. An edge is
four bare usize fields — Edge { from, to, slot, from_field }
(crates/aura-engine/src/harness.rs:30) — where from/to are positions in the
Composite's nodes Vec, slot indexes the consumer's NodeSchema.inputs, and
from_field indexes the producer's output record. The authored form is dense and
brittle:

// crates/aura-engine/src/test_fixtures.rs:38
vec![
    Edge { from: 0, to: 2, slot: 0, from_field: 0 },
    Edge { from: 1, to: 2, slot: 1, from_field: 0 },
],

Inserting a node renumbers every edge that referenced a later position; the
integers carry no meaning at the call site, so hand-comments (// fast -> Sub.lhs)
stand in for what the code cannot say.

By contrast, params are already addressed by name
(bp.with("sma_cross.fast.length", 2), crates/aura-engine/src/blueprint.rs:259),
and the graph-model viewer already renders endpoints with name-ish handles
(@price, #0, crates/aura-engine/src/graph_model.rs:140). Only topology
authoring is still bare indices.

Proposed shape

A fluent, additive GraphBuilder (new module) where node references are typed
values, not strings:

let mut g = GraphBuilder::new("sma_cross");
let fast = g.add(Sma::builder().named("fast"));   // NodeHandle
let slow = g.add(Sma::builder().named("slow"));
let sub  = g.add(Sub::builder());
let price = g.input_role("price");
g.feed(price, [fast.in_("series"), slow.in_("series")]);
g.connect(fast.out("value"), sub.in_("lhs"));
g.connect(slow.out("value"), sub.in_("rhs"));
g.expose(sub.out("value"), "out");
let bp: Composite = g.build();
  • add() returns a NodeHandle carrying the assigned nodes-Vec index — a
    compiler-checked value, impossible to mistype. Only port names are strings.
  • Port names resolve against the existing PortSpec.name / FieldSpec.name
    (crates/aura-core/src/node.rs:39,50, shipped by spec
    docs/specs/0027-name-input-ports.md).
  • build() emits the existing Composite
    (crates/aura-engine/src/blueprint.rs:139) unchanged; everything downstream
    (compile_with_params, inline_composite, FlatGraph) is untouched. Names
    resolve at the authoring boundary and never reach the compilat — the same
    posture param-name resolution already has
    (crates/aura-engine/src/blueprint.rs:413), so the index-wired-compilat
    invariant (C23) holds by construction.

This is the endorsed fluent-Rust-builder shape (C10), not a stringly-typed
mini-language: endpoints are typed values, the only strings are port names looked
up against real declared schema data.

Acceptance

  • A GraphBuilder produces a Composite byte-identical (same FlatGraph
    edges/sources) to the hand-wired index form — pinned by a parity test on the
    shared sma_cross harness fixtures (crates/aura-engine/src/test_fixtures.rs).
  • Port/field-name resolution is exactly-one-match, with a recoverable error
    surface (unknown node / unknown port / ambiguous port) mirroring
    PrimitiveBuilder::bind's collect-then-reject
    (crates/aura-core/src/node.rs:170).
  • Nested composites wire through the same API (a sub-builder's Composite is
    added into the parent and addressed by its derived role/output names).
  • The raw-index Composite::new stays public and unchanged; existing call
    sites keep compiling. The two authoring forms coexist.

Verified prerequisites and limits

  • There is no impl From<Composite> for BlueprintNode today — only
    From<PrimitiveBuilder> (crates/aura-engine/src/blueprint.rs:44); nested
    composites are wrapped by hand as BlueprintNode::Composite(...) (e.g.
    crates/aura-engine/src/test_fixtures.rs:64). The add path for a nested
    composite needs this From impl (or an explicit lift method) added.
  • Resolving by port name relies on input-port names being unique within a node.
    Every shipped node satisfies this today, but spec 0027 deliberately left
    PortSpec.name non-load-bearing and unguarded — so this becomes a soft
    expectation for builder-wired nodes (a within-node uniqueness guard is the
    natural enforcement point).
  • This makes the SimBroker exposure/price ordering footgun (#21, closed)
    legible — a named slot replaces the bare index — but not structurally
    impossible: correct port names with transposed sources still resolve. The
    structural fix (promoting PortSpec.name / FieldSpec.name to load-bearing
    addressing keys plus an all-required-ports-connected check) is a distinct,
    ledger-level follow-up that spec 0027 already deferred ("its own cycle, now
    better-equipped because the names exist to validate against"); tracked
    separately.
## Problem Blueprint topology is wired today purely by raw positional indices. An edge is four bare `usize` fields — `Edge { from, to, slot, from_field }` (`crates/aura-engine/src/harness.rs:30`) — where `from`/`to` are positions in the `Composite`'s `nodes` Vec, `slot` indexes the consumer's `NodeSchema.inputs`, and `from_field` indexes the producer's output record. The authored form is dense and brittle: ```rust // crates/aura-engine/src/test_fixtures.rs:38 vec![ Edge { from: 0, to: 2, slot: 0, from_field: 0 }, Edge { from: 1, to: 2, slot: 1, from_field: 0 }, ], ``` Inserting a node renumbers every edge that referenced a later position; the integers carry no meaning at the call site, so hand-comments (`// fast -> Sub.lhs`) stand in for what the code cannot say. By contrast, params are already addressed by name (`bp.with("sma_cross.fast.length", 2)`, `crates/aura-engine/src/blueprint.rs:259`), and the graph-model viewer already renders endpoints with name-ish handles (`@price`, `#0`, `crates/aura-engine/src/graph_model.rs:140`). Only topology authoring is still bare indices. ## Proposed shape A fluent, additive `GraphBuilder` (new module) where node references are typed values, not strings: ```rust let mut g = GraphBuilder::new("sma_cross"); let fast = g.add(Sma::builder().named("fast")); // NodeHandle let slow = g.add(Sma::builder().named("slow")); let sub = g.add(Sub::builder()); let price = g.input_role("price"); g.feed(price, [fast.in_("series"), slow.in_("series")]); g.connect(fast.out("value"), sub.in_("lhs")); g.connect(slow.out("value"), sub.in_("rhs")); g.expose(sub.out("value"), "out"); let bp: Composite = g.build(); ``` - `add()` returns a `NodeHandle` carrying the assigned `nodes`-Vec index — a compiler-checked value, impossible to mistype. Only port names are strings. - Port names resolve against the existing `PortSpec.name` / `FieldSpec.name` (`crates/aura-core/src/node.rs:39,50`, shipped by spec `docs/specs/0027-name-input-ports.md`). - `build()` emits the existing `Composite` (`crates/aura-engine/src/blueprint.rs:139`) unchanged; everything downstream (`compile_with_params`, `inline_composite`, `FlatGraph`) is untouched. Names resolve at the authoring boundary and never reach the compilat — the same posture param-name resolution already has (`crates/aura-engine/src/blueprint.rs:413`), so the index-wired-compilat invariant (C23) holds by construction. This is the endorsed fluent-Rust-builder shape (C10), not a stringly-typed mini-language: endpoints are typed values, the only strings are port names looked up against real declared schema data. ## Acceptance - [ ] A `GraphBuilder` produces a `Composite` byte-identical (same `FlatGraph` edges/sources) to the hand-wired index form — pinned by a parity test on the shared sma_cross harness fixtures (`crates/aura-engine/src/test_fixtures.rs`). - [ ] Port/field-name resolution is exactly-one-match, with a recoverable error surface (unknown node / unknown port / ambiguous port) mirroring `PrimitiveBuilder::bind`'s collect-then-reject (`crates/aura-core/src/node.rs:170`). - [ ] Nested composites wire through the same API (a sub-builder's `Composite` is `add`ed into the parent and addressed by its derived role/output names). - [ ] The raw-index `Composite::new` stays public and unchanged; existing call sites keep compiling. The two authoring forms coexist. ## Verified prerequisites and limits - There is no `impl From<Composite> for BlueprintNode` today — only `From<PrimitiveBuilder>` (`crates/aura-engine/src/blueprint.rs:44`); nested composites are wrapped by hand as `BlueprintNode::Composite(...)` (e.g. `crates/aura-engine/src/test_fixtures.rs:64`). The `add` path for a nested composite needs this `From` impl (or an explicit lift method) added. - Resolving by port name relies on input-port names being unique within a node. Every shipped node satisfies this today, but spec 0027 deliberately left `PortSpec.name` non-load-bearing and unguarded — so this becomes a soft expectation for builder-wired nodes (a within-node uniqueness guard is the natural enforcement point). - This makes the SimBroker exposure/price ordering footgun (`#21`, closed) *legible* — a named slot replaces the bare index — but not structurally impossible: correct port names with transposed sources still resolve. The structural fix (promoting `PortSpec.name` / `FieldSpec.name` to load-bearing addressing keys plus an all-required-ports-connected check) is a distinct, ledger-level follow-up that spec 0027 already deferred ("its own cycle, now better-equipped because the names exist to validate against"); tracked separately.
Brummel added the feature label 2026-06-14 01:58:07 +02:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Brummel/Aura#64