Node output is a record (composite stream), not a single scalar #1

Closed
opened 2026-06-03 15:05:02 +02:00 by Brummel · 0 comments
Owner

Motivation

Today every node emits exactly one scalar per cycle:

  • aura-core/src/node.rs:37NodeSchema.output: ScalarKind (singular)
  • aura-core/src/node.rs:46Node::eval(...) -> Option<Scalar>
  • aura-engine/src/harness.rsEdge { from, to } forwards that one scalar; the harness stores one output column per node.

This is too narrow. A node's output is generally a record of scalars — a bundle of named, typed base columns that together form one composite stream over the cycles. A single scalar is just the degenerate K=1 case. OHLCV is the obvious example (open/high/low/close/volume emitted together); the position table (C10) is another (5-column position events). C8's current wording — "at most one output (one series per node)" — conflates "one output port" with "one scalar series" and is wrong: the port stays single, but its payload is a record.

This is a BLOCKER for the trading half of the walking skeleton: the position table (C10) and the broker nodes that consume it cannot exist until a node can emit more than one column.

Design decisions (converged)

  1. Output generalizes, input does not.

    • NodeSchema.output: ScalarKindRecordSchema (an ordered list of { name, kind } fields over the four base types).
    • Node::eval(...) -> Option<Scalar>-> Option<Record> (one row of K scalars; None = no emission this cycle).
    • Scalar output is the degenerate 1-field record — there is no separate scalar-output path.
    • Stored as K parallel SoA base columns. The record is structural bundling over the four base types (C7 intact), not a fifth scalar type. No dyn Any, no heterogeneous buffer.
  2. Binding is always field-wise.

    • An input slot still binds exactly one base column. Edge gains from_field: usize — which field of the producer's record it reads. Kind-check per field: producer.output[from_field].kind == consumer.inputs[to_slot].kind.
    • Consuming a whole record (e.g. OHLC into a node) = N edges, one per field. No special "bind whole record" mechanism.
    • Input side, lookback windows, and firing (C6) are otherwise unchanged.
  3. Record fields are co-fresh by construction.

    • The K fields of a record are emitted together in one eval, so they always share a cycle / timestamp. Splitting a record across N input bindings therefore needs no barrier among those fields; C6 remains for cross-source synchronization only.
  4. This is a contract change, not a refactor. C8 must be revised (and C7 sharpened) in the design ledger: a node has one output port whose payload is a record of 1..K base columns.

Blast radius

  • aura-core: new Record / RecordSchema types; NodeSchema.output becomes RecordSchema; Node::eval returns Option<Record>.
  • aura-std: Sma, Sub migrate to 1-field records (degenerate case; behaviour unchanged).
  • aura-engine: Edge gains from_field; the harness stores K output columns per node and kind-checks per field; the per-cycle push / forward path threads the field index.

Scope when picked up

Prove the generalization with a neutral multi-field test node (OHLCV) — a pure substrate change, isolated from any trading domain model. The position table (C10) and brokers are deliberately out of scope; they are later cycles that consume this capability.

Ledger

Revise C8 (output is a record, not a single series) and sharpen C7 (composite stream = bundle of base columns is the node-output model, not just an ingestion-side note) when this lands.

## Motivation Today every node emits exactly one scalar per cycle: - `aura-core/src/node.rs:37` — `NodeSchema.output: ScalarKind` (singular) - `aura-core/src/node.rs:46` — `Node::eval(...) -> Option<Scalar>` - `aura-engine/src/harness.rs` — `Edge { from, to }` forwards that one scalar; the harness stores one output column per node. This is too narrow. A node's output is generally a **record of scalars** — a bundle of named, typed base columns that together form one composite stream over the cycles. A single scalar is just the degenerate K=1 case. OHLCV is the obvious example (open/high/low/close/volume emitted together); the position table (C10) is another (5-column position events). C8's current wording — "at most one output (one series per node)" — conflates "one output port" with "one scalar series" and is wrong: the port stays single, but its payload is a record. This is a **BLOCKER** for the trading half of the walking skeleton: the position table (C10) and the broker nodes that consume it cannot exist until a node can emit more than one column. ## Design decisions (converged) 1. **Output generalizes, input does not.** - `NodeSchema.output: ScalarKind` → `RecordSchema` (an ordered list of `{ name, kind }` fields over the four base types). - `Node::eval(...) -> Option<Scalar>` → `-> Option<Record>` (one row of K scalars; `None` = no emission this cycle). - Scalar output is the degenerate **1-field record** — there is no separate scalar-output path. - Stored as **K parallel SoA base columns**. The record is structural bundling over the four base types (C7 intact), **not** a fifth scalar type. No `dyn Any`, no heterogeneous buffer. 2. **Binding is always field-wise.** - An input slot still binds exactly one base column. `Edge` gains `from_field: usize` — which field of the producer's record it reads. Kind-check per field: `producer.output[from_field].kind == consumer.inputs[to_slot].kind`. - Consuming a whole record (e.g. OHLC into a node) = N edges, one per field. **No special "bind whole record" mechanism.** - Input side, lookback windows, and firing (C6) are otherwise unchanged. 3. **Record fields are co-fresh by construction.** - The K fields of a record are emitted together in one `eval`, so they always share a cycle / timestamp. Splitting a record across N input bindings therefore needs no barrier among those fields; C6 remains for *cross-source* synchronization only. 4. **This is a contract change, not a refactor.** C8 must be revised (and C7 sharpened) in the design ledger: a node has one output port whose payload is a record of 1..K base columns. ## Blast radius - `aura-core`: new `Record` / `RecordSchema` types; `NodeSchema.output` becomes `RecordSchema`; `Node::eval` returns `Option<Record>`. - `aura-std`: `Sma`, `Sub` migrate to 1-field records (degenerate case; behaviour unchanged). - `aura-engine`: `Edge` gains `from_field`; the harness stores K output columns per node and kind-checks per field; the per-cycle push / forward path threads the field index. ## Scope when picked up Prove the generalization with a **neutral multi-field test node (OHLCV)** — a pure substrate change, isolated from any trading domain model. The position table (C10) and brokers are deliberately **out of scope**; they are later cycles that consume this capability. ## Ledger Revise **C8** (output is a record, not a single series) and sharpen **C7** (composite stream = bundle of base columns is the node-output model, not just an ingestion-side note) when this lands.
Brummel added this to the (deleted) milestone 2026-06-03 15:05:02 +02:00
Brummel added the BLOCKER label 2026-06-03 15:05:02 +02:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Brummel/Aura#1