Unify the blueprint main-graph render through the composite definition machinery #48

Closed
opened 2026-06-08 22:45:00 +02:00 by Brummel · 0 comments
Owner

Symptom

aura graph --macd renders the main graph far poorer than a composite's
interior. From the rendered main graph it is not visible what the strategy
actually does:

[macd] → [Exposure] → [SimBroker] → [Recorder]

Two facts the strategy turns on are absent: (a) that the histogram field —
not the macd line, not the signal — drives Exposure (the edge carries
from_field: 2, see crates/aura-cli/src/main.rs:254), and (b) anything
Exposure computes. The whole strategy is one line —

exposure = clamp(MACD_histogram(price; fast, slow, signal) / scale, -1, +1)

— and none of it reaches the graph.

The composite where: view, by contrast, already solves the "show what a node
is" problem (goal A) completely: a typed signature title, leaf labels
carrying aliased param names + ordered input-slot stubs ([EMA(fast)],
[Sub(#Ef,#Es)]), and output bindings ([macd := Sub(...)]).

Root cause — accidental code divergence, not a design principle

Two divergent render paths in crates/aura-cli/src/graph.rs. The leaf-label
difference is one line:

  • graph.rs:44render_blueprint (main graph): Leaf(factory) => factory.label() (bare)
  • graph.rs:299render_definition (composite): Leaf(factory) => leaf_label(c, i, factory) (enriched)

The main graph takes the bare variant; the composite interior takes the
enriched one. [Exposure] could run through leaf_label exactly like
[EMA(fast)] and show its params + slot stubs. It does not, only because
render_blueprint is a separate, poorer path that never inherited the
composite machinery (leaf_label, the edge handling, output_binding).

Structural basis — the blueprint is a composite for render purposes

Struct shapes in crates/aura-engine/src/blueprint.rs:

struct Composite { name, nodes: Vec<BlueprintNode>, edges: Vec<Edge>,
                   input_roles: Vec<Role>, params: Vec<ParamAlias>, output: Vec<OutField> }
struct Blueprint {       nodes: Vec<BlueprintNode>, edges: Vec<Edge>,
                   sources: Vec<SourceSpec> }

nodes and edges are the same types — identity, not analogy. The A-solving
machinery (leaf_label, edge handling, output_binding) operates solely on
nodes + edges, so it applies to a Blueprint with no semantic change. The
blueprint differs only at the borders, and each is a small render adapter,
not a blocker:

  1. Entries. sources (typed, bound) vs input_roles (named, open). Both
    already render as an entry marker — [source:F64] is the counterpart of
    [price].
  2. Params. Blueprint has no alias field, but Blueprint::param_space()
    (blueprint.rs:183) yields the same per-node information; leaf_label
    would source params from there instead of c.params().
  3. Outputs. See the sink sharpening below.

Sink sharpening (load-bearing)

A sink is not a type. Verified: a grep for Sink across aura-engine /
aura-core finds only test fixtures (SinkF64 / SinkI64) and test names —
no trait Sink, no enum variant, no model category. The only structural mark
is an empty output schema, output: vec![] (Recorder,
crates/aura-std/src/recorder.rs). The test node_is_producer_and_sink_at_once
(crates/aura-engine/src/harness.rs:1108) shows sinkhood is not even exclusive
— a node can produce and be tapped at once.

Therefore sinkhood is a topology role / convention: the zero-output bottom end
of C8's "at most one output", not a node kind. Consequences:

  • A sink is orthogonal to OutField. OutField is a forwarded port (for a
    consumer); a sink is a terminal node (side effect, no consumer). They are not
    two forms of "output"; a composite may have both.
  • Any graph — composites included — may contain sinks. The model has no
    mechanism forbidding an interior sink in a composite.
  • Render-side, the third border difference is therefore not a special case: a
    sink is a leaf like any other, run through leaf_label, that happens to have
    no outgoing edge. No special handling.

Direction (names the unification; does not prescribe the implementation)

One shared render path over the common shape: nodes + edges + entries +
per-node params + outputs. The blueprint is the root composite with
differently-shaped borders. render_definition already is that path — it is
merely typed to &Composite instead of the shared shape. Routing the blueprint
through it makes the main graph inherit, for free: leaf params, slot stubs, and
from_field edge semantics.

Relationship to other findings

  • Subsumes the separately-surfaced from_field finding: edges drop
    from_field / slot at graph.rs:60, and ascii-dag's add_edge label slot
    at graph.rs:87 is passed None. A unified path that renders edge
    field/slot labels in both views resolves it as a by-product, so it is folded
    in here rather than filed separately.
  • Distinct from #28 (render a consumer's own graph — reach), #37
    (interactive enter-to-expand — playground), and #40 (multi-output composite
    boundary — closed; the substrate this builds on). These are neighbours, not
    duplicates.

Invariant note

C12 states the harness is ontologically not a node (a closed root, not a
composable fragment). That is the wrong axis for the render: ontology
(composable vs terminal) and render form (which fields are drawn) are
independent. The harness is ontologically not a node and
render-structurally a composite — both hold at once. Earlier reasoning that
used C12 to deny the main graph a signature was a category error; it is
recorded here so the next reader does not repeat it.

## Symptom `aura graph --macd` renders the main graph far poorer than a composite's interior. From the rendered main graph it is not visible what the strategy actually does: ``` [macd] → [Exposure] → [SimBroker] → [Recorder] ``` Two facts the strategy turns on are absent: (a) that the `histogram` field — not the macd line, not the signal — drives `Exposure` (the edge carries `from_field: 2`, see `crates/aura-cli/src/main.rs:254`), and (b) anything `Exposure` computes. The whole strategy is one line — ``` exposure = clamp(MACD_histogram(price; fast, slow, signal) / scale, -1, +1) ``` — and none of it reaches the graph. The composite `where:` view, by contrast, already solves the "show what a node is" problem (goal **A**) completely: a typed signature title, leaf labels carrying aliased param names + ordered input-slot stubs (`[EMA(fast)]`, `[Sub(#Ef,#Es)]`), and output bindings (`[macd := Sub(...)]`). ## Root cause — accidental code divergence, not a design principle Two divergent render paths in `crates/aura-cli/src/graph.rs`. The leaf-label difference is one line: - `graph.rs:44` — `render_blueprint` (main graph): `Leaf(factory) => factory.label()` (bare) - `graph.rs:299` — `render_definition` (composite): `Leaf(factory) => leaf_label(c, i, factory)` (enriched) The main graph takes the bare variant; the composite interior takes the enriched one. `[Exposure]` could run through `leaf_label` exactly like `[EMA(fast)]` and show its params + slot stubs. It does not, only because `render_blueprint` is a separate, poorer path that never inherited the composite machinery (`leaf_label`, the edge handling, `output_binding`). ## Structural basis — the blueprint is a composite for render purposes Struct shapes in `crates/aura-engine/src/blueprint.rs`: ```rust struct Composite { name, nodes: Vec<BlueprintNode>, edges: Vec<Edge>, input_roles: Vec<Role>, params: Vec<ParamAlias>, output: Vec<OutField> } struct Blueprint { nodes: Vec<BlueprintNode>, edges: Vec<Edge>, sources: Vec<SourceSpec> } ``` `nodes` and `edges` are the same types — identity, not analogy. The A-solving machinery (`leaf_label`, edge handling, `output_binding`) operates solely on `nodes` + `edges`, so it applies to a `Blueprint` with no semantic change. The blueprint differs only at the **borders**, and each is a small render adapter, not a blocker: 1. **Entries.** `sources` (typed, bound) vs `input_roles` (named, open). Both already render as an entry marker — `[source:F64]` is the counterpart of `[price]`. 2. **Params.** `Blueprint` has no alias field, but `Blueprint::param_space()` (`blueprint.rs:183`) yields the same per-node information; `leaf_label` would source params from there instead of `c.params()`. 3. **Outputs.** See the sink sharpening below. ## Sink sharpening (load-bearing) A sink is not a type. Verified: a grep for `Sink` across `aura-engine` / `aura-core` finds only test fixtures (`SinkF64` / `SinkI64`) and test names — no `trait Sink`, no enum variant, no model category. The only structural mark is an empty output schema, `output: vec![]` (`Recorder`, `crates/aura-std/src/recorder.rs`). The test `node_is_producer_and_sink_at_once` (`crates/aura-engine/src/harness.rs:1108`) shows sinkhood is not even exclusive — a node can produce and be tapped at once. Therefore sinkhood is a topology role / convention: the zero-output bottom end of C8's "at most one output", not a node kind. Consequences: - A sink is orthogonal to `OutField`. `OutField` is a forwarded port (for a consumer); a sink is a terminal node (side effect, no consumer). They are not two forms of "output"; a composite may have both. - Any graph — composites included — may contain sinks. The model has no mechanism forbidding an interior sink in a composite. - Render-side, the third border difference is therefore not a special case: a sink is a leaf like any other, run through `leaf_label`, that happens to have no outgoing edge. No special handling. ## Direction (names the unification; does not prescribe the implementation) One shared render path over the common shape: `nodes` + `edges` + entries + per-node params + outputs. The blueprint is the root composite with differently-shaped borders. `render_definition` already is that path — it is merely typed to `&Composite` instead of the shared shape. Routing the blueprint through it makes the main graph inherit, for free: leaf params, slot stubs, and `from_field` edge semantics. ## Relationship to other findings - Subsumes the separately-surfaced `from_field` finding: edges drop `from_field` / `slot` at `graph.rs:60`, and ascii-dag's `add_edge` label slot at `graph.rs:87` is passed `None`. A unified path that renders edge field/slot labels in both views resolves it as a by-product, so it is folded in here rather than filed separately. - Distinct from `#28` (render a consumer's own graph — reach), `#37` (interactive enter-to-expand — playground), and `#40` (multi-output composite boundary — closed; the substrate this builds on). These are neighbours, not duplicates. ## Invariant note C12 states the harness is ontologically not a node (a closed root, not a composable fragment). That is the wrong axis for the render: ontology (composable vs terminal) and render form (which fields are drawn) are independent. The harness is ontologically not a node **and** render-structurally a composite — both hold at once. Earlier reasoning that used C12 to deny the main graph a signature was a category error; it is recorded here so the next reader does not repeat it.
Brummel added the feature label 2026-06-08 22:45:00 +02:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Brummel/Aura#48