Composite output is a single port — multi-line indicators (MACD, Bollinger…) can't re-export a record #40

Closed
opened 2026-06-07 23:32:41 +02:00 by Brummel · 1 comment
Owner

A composite exposes exactly one output field (Composite::output: OutPort). Multi-line indicators — MACD (macd / signal / histogram), Bollinger (upper / mid / lower), Stochastic (%K / %D), Ichimoku (5 lines) — cannot be authored as a composition and re-exported as a unit; only one line escapes the boundary. The MACD PoC (commit d8b2a28) could expose only the histogram for this reason.

Diagnosis (grounded in the code)

The substrate is already multi-field everywhere except the composite boundary:

  • Node::schema().output is Vec<FieldSpec> — a K-column record (OHLCV = 5 columns through one port), supported today.
  • Edge.from_field selects which output field a consumer reads.
  • The inliner already routes from_field for leaf producers — a multi-output leaf works today (blueprint.rs rewrite_edge, the Leaf arm).

The only cap is the composite boundary, in three spots in crates/aura-engine/src/blueprint.rs:

  • struct Composite { output: OutPort } (~line 48) — one (node, field).
  • rewrite_edge (~line 377): a composite consumer asserts from_field != 0 → error.
  • inline_composite (~line 326): same field != 0 guard for nested composites.

Invariant-consistent (does NOT touch C8 / C7 / C4)

  • "≤1 output" (C8) means one port; "≤1 record per eval" means one row — neither is the column count. OHLCV (5 columns, one port) is the living proof.
  • A 3-line MACD = one record of 3 fields, one port, one row per eval.
  • A strategy still outputs one exposure — a composite whose output record happens to be 1 field (C7). Indicator composites have K fields, strategy composites have 1. Same mechanism, different arity.

This is a boundary completion, not an invariant change.

Fix shape

  • Composite::output: OutPort → Vec<OutPort> (the output record = ordered re-exported interior fields).
  • rewrite_edge / inline_composite: index that Vec by from_field + range-check — exactly the shape the Leaf arm already has — instead of asserting == 0.
  • Render: [out:k] markers (analogous to [in:k]) and K fan-out edges at the opaque composite node.
  • Validation: output-record arity + per-field kind-checks.
  • Authoring: Composite::new(… output: vec![…]).

Relationship to the "composite-edge signature" theme

This is the output half of giving a composite a typed, named boundary signature. The closed #30 gave leaf params their typed names; FieldSpec.name already carries output-field names (macd / signal / hist) — they just aren't surfaced at the boundary. The still-open sibling gap is composite-level param aliasing: two SMAs in one composite both surface as length (["sma_cross.length", "sma_cross.length"]) with no way to alias them to shortLen / longLen. Inputs (roles), outputs (this issue), and params (aliasing) together form the "composite boundary gets a named typed signature" movement — the form behind macd(fast, slow, signal) -> (macd, signal, hist).

A composite exposes exactly **one** output field (`Composite::output: OutPort`). Multi-line indicators — MACD (macd / signal / histogram), Bollinger (upper / mid / lower), Stochastic (%K / %D), Ichimoku (5 lines) — cannot be authored as a composition and re-exported as a unit; only one line escapes the boundary. The MACD PoC (commit d8b2a28) could expose only the histogram for this reason. ## Diagnosis (grounded in the code) The substrate is **already multi-field everywhere except the composite boundary**: - `Node::schema().output` is `Vec<FieldSpec>` — a K-column record (OHLCV = 5 columns through one port), supported today. - `Edge.from_field` selects *which* output field a consumer reads. - The inliner already routes `from_field` for **leaf** producers — a multi-output leaf works today (`blueprint.rs` `rewrite_edge`, the `Leaf` arm). The only cap is the composite boundary, in three spots in `crates/aura-engine/src/blueprint.rs`: - `struct Composite { output: OutPort }` (~line 48) — one `(node, field)`. - `rewrite_edge` (~line 377): a composite consumer asserts `from_field != 0 → error`. - `inline_composite` (~line 326): same `field != 0` guard for nested composites. ## Invariant-consistent (does NOT touch C8 / C7 / C4) - "≤1 output" (C8) means one **port**; "≤1 record per eval" means one **row** — neither is the column count. OHLCV (5 columns, one port) is the living proof. - A 3-line MACD = **one** record of 3 fields, **one** port, **one** row per eval. - A strategy still outputs **one** exposure — a composite whose output record happens to be 1 field (C7). Indicator composites have K fields, strategy composites have 1. Same mechanism, different arity. This is a boundary completion, not an invariant change. ## Fix shape - `Composite::output: OutPort → Vec<OutPort>` (the output record = ordered re-exported interior fields). - `rewrite_edge` / `inline_composite`: index that `Vec` by `from_field` + range-check — exactly the shape the `Leaf` arm already has — instead of asserting `== 0`. - Render: `[out:k]` markers (analogous to `[in:k]`) and K fan-out edges at the opaque composite node. - Validation: output-record arity + per-field kind-checks. - Authoring: `Composite::new(… output: vec![…])`. ## Relationship to the "composite-edge signature" theme This is the **output** half of giving a composite a typed, named boundary signature. The closed #30 gave *leaf* params their typed names; `FieldSpec.name` already carries output-field names (`macd` / `signal` / `hist`) — they just aren't surfaced at the boundary. The still-open sibling gap is **composite-level param aliasing**: two SMAs in one composite both surface as `length` (`["sma_cross.length", "sma_cross.length"]`) with no way to alias them to `shortLen` / `longLen`. Inputs (roles), outputs (this issue), and params (aliasing) together form the "composite boundary gets a named typed signature" movement — the form behind `macd(fast, slow, signal) -> (macd, signal, hist)`.
Brummel added the feature label 2026-06-07 23:32:41 +02:00
Author
Owner

Decision (boundary-signature correlation analysis): params, inputs, and outputs are the same named-positional-interior-projection pattern and converge at the renderer (the legible macd(fast, slow, signal) -> (macd, signal, hist) signature needs all three names at once). Two consequences for THIS issue:

  1. The output type is born name-readyVec<OutField { node, field, name }>, not bare Vec<OutPort> — so it is not reopened later just to add names. Output-field naming ships with the multi-output capability here.
  2. The pure-legibility half (param aliasing + input-role naming on the already-working param/input edges) is split to #41, which depends on this issue and reuses the same named-projection vocabulary.

Co-designed in one brainstorm ("composite boundary signature"), implemented in sequence: #40 capability + output names first, #41 legibility second.

Decision (boundary-signature correlation analysis): params, inputs, and outputs are the *same* named-positional-interior-projection pattern and converge at the renderer (the legible `macd(fast, slow, signal) -> (macd, signal, hist)` signature needs all three names at once). Two consequences for THIS issue: 1. The output type is born **name-ready** — `Vec<OutField { node, field, name }>`, not bare `Vec<OutPort>` — so it is not reopened later just to add names. Output-field naming ships *with* the multi-output capability here. 2. The pure-legibility half (param aliasing + input-role naming on the already-working param/input edges) is split to #41, which depends on this issue and reuses the same named-projection vocabulary. Co-designed in one brainstorm ("composite boundary signature"), implemented in sequence: #40 capability + output names first, #41 legibility second.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Brummel/Aura#40