feat: node output is a record (composite stream), not a single scalar
Cycle 0005 (BLOCKER #1): a node's output generalizes from one scalar to a record of K >= 1 named base-scalar columns, with a scalar being the degenerate K = 1 case. A node keeps exactly one output port; its payload is now an ordered bundle of base columns (a composite stream, C7). This unblocks every multi-column producer the engine needs next -- OHLCV bars and, later, the C10 position-event table -- none of which could exist while a node emitted at most one column. Revises C8, sharpens C7 in the design ledger. Contract (aura-core): - `NodeSchema.output: ScalarKind` -> `Vec<FieldSpec>` (FieldSpec = { name: &'static str, kind }; the field position is what an Edge binds, the name is metadata for later sinks/playground, C18). - `Node::eval -> Option<Scalar>` -> `-> Option<&[Scalar]>`: a node fills a buffer it owns (sized once at construction) and returns a borrowed K-field row; `None` still means filter/not-warmed. This is the C7-faithful representation chosen with the user: zero per-cycle heap allocation on the forward path (rejected: Option<Vec<Scalar>> allocates per fire; an inline fixed [Scalar; N] bakes a width cap; engine-owned output columns invert the eval model). Scalar output is the degenerate 1-field record -- no separate scalar path. Field-wise binding (aura-engine): - `Edge` gains `from_field: usize` -- which producer column this edge forwards. Consuming a whole record is N edges; there is no "bind whole record" mechanism. - bootstrap kind-checks per field (`from.output.get(from_field)` -> BadIndex if out of range; `field.kind != slot.kind` -> KindMismatch). - the run loop copies a producer's returned row into one reused scratch buffer (resolving the borrow; no per-cycle alloc once warm) and scatters scratch[from_field] into each out-edge slot. `NodeBox.out_len` (set at bootstrap) backs a debug_assert on the returned row width. `run` returns `Vec<Option<Vec<Scalar>>>` (the observed node's full row per cycle -- a materialization-surface alloc, not the inter-node hot path). - the K fields of one record are co-fresh by construction (one eval, one timestamp), so C6 is untouched. aura-std: Sma/Sub migrate to the 1-field degenerate case (hold a [Scalar; 1] buffer; behaviour unchanged). Sub drops #[derive(Default)] (Scalar has no Default) for a manual Default. Proof (aura-engine tests, +5): an Ohlcv 5-field bundler fed by five timestamp-aligned barrier sources emits one complete bar per timestamp (ohlcv_bundles_five_field_record); a downstream Sub binds high(1)-low(2) field-wise, observed end-to-end + a determinism re-run (edge_binds_single_field_high_minus_low); a second consumer binds close(3)-open(0) on the same record (distinct_edges_read_distinct_fields); must-fail: bootstrap_rejects_from_field_out_of_range (BadIndex), bootstrap_rejects_per_field_kind_mismatch (a TwoField [f64,i64] producer, the i64 field into an f64 slot). All prior 0003/0004 tests adapted to from_field + the Vec return, expected vectors unchanged (scalar = 1-field record). Gates re-run by the orchestrator (not just the agent): cargo build/test --workspace --all-targets (36: aura-core 19 + aura-std 3 + aura-engine 14, 0 failed) / clippy --workspace --all-targets -D warnings clean; surface-purity grep (no dyn-Any / Rc / RefCell) clean. Ledger C8/C7 edits verified (Option<&[Scalar]> present, "one series per node" gone, cycle-0005 realization note added). The glossary composite/node record-reality pass is the audit-time follow-up. closes #1 refs walking-skeleton Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+19
-6
@@ -168,7 +168,10 @@ in a cycle carries that cycle's timestamp).
|
||||
### C7 — Four scalar base types, streamed as SoA
|
||||
**Guarantee.** Only `i64`, `f64`, `bool`, `timestamp` (newtype over i64,
|
||||
epoch-ns UTC) are streamed, as columnar Structure-of-Arrays. Composite streams
|
||||
(OHLCV) are bundles of base columns. Edges are type-erased to these four kinds;
|
||||
(OHLCV) are bundles of base columns — this is the **node-output model** too: a
|
||||
node emits a record of 1..K base columns (C8), each forwarded field-wise to a
|
||||
consumer slot; the bundle is structural, never a fifth scalar type. Edges are
|
||||
type-erased to these four kinds;
|
||||
the type check is paid once at wiring/sim-start, then the topology is frozen per
|
||||
sim → direct dispatch, no per-event allocation.
|
||||
**Forbids.** Streaming non-scalars (String, Records, tables, calendars) — those
|
||||
@@ -182,19 +185,29 @@ Type-erasure at the edge is also forced by the cdylib boundary (C13).
|
||||
**Guarantee.** A node implements `schema()` (declares each input's scalar type,
|
||||
required lookback depth, and firing group, **and the node's own tunable
|
||||
parameters — typed, with ranges**, which aggregate into the blueprint's
|
||||
param-space the optimizer sweeps, C12/C19/C20) + `eval(ctx) -> Option<Scalar>`. The
|
||||
param-space the optimizer sweeps, C12/C19/C20) + `eval(ctx) -> Option<&[Scalar]>`. The
|
||||
engine provides read-only, zero-copy windows into each input's SoA ring buffer
|
||||
(`ctx.f64_in(x)[k]`, sized at wiring); a node may *additionally* keep its own
|
||||
mutable series for derived/intermediate state. `None`/Void return = filter /
|
||||
not-yet-warmed-up. A node is a **producer, a consumer, or both**: a
|
||||
producer/transformer exposes **at most one** output (one series per node); a
|
||||
**pure consumer (sink)** — chart, equity, logger — has **no** output. Sources
|
||||
are pure producers; sinks are pure consumers.
|
||||
producer/transformer exposes **one output port**, whose payload is a **record of
|
||||
1..K base-scalar columns** (a scalar is the degenerate K=1 record; an `eval`
|
||||
returns a borrowed row, one value per column); a **pure consumer (sink)** — chart,
|
||||
equity, logger — has **no** output. Sources are pure producers; sinks are pure
|
||||
consumers.
|
||||
**Forbids.** A node sizing/growing its input lookback at runtime; more than one
|
||||
output per node (model as multiple nodes); copy-on-read of input history.
|
||||
output **port** per node; a fifth scalar type or a heterogeneous output payload
|
||||
(a record is a bundle of base columns, C7); copy-on-read of input history.
|
||||
**Why.** Engine-provided windows mean LLM-authored code cannot mis-manage
|
||||
lookback bookkeeping, and history passes through zero-copy. Fixed, pre-sized
|
||||
buffers suit deterministic, pre-dimensioned sims (no realloc in the hot loop).
|
||||
**Realization (cycle 0005).** `NodeSchema.output` is a `Vec<FieldSpec>` (named base
|
||||
columns; length 1 = scalar). Binding is **field-wise only**: `Edge::from_field`
|
||||
selects one producer column per edge; consuming a whole record is N edges (no
|
||||
"bind whole record" mechanism). The K fields of one record are **co-fresh by
|
||||
construction** (one `eval`, one timestamp), so C6 is untouched. `eval` returns
|
||||
`Option<&[Scalar]>` — a borrowed row into a node-owned buffer — so the forward
|
||||
path allocates nothing per cycle (C7).
|
||||
|
||||
### C9 — Fractal, acyclic composition
|
||||
**Guarantee.** A composite is itself a `Node` that wires a sub-graph and exposes
|
||||
|
||||
Reference in New Issue
Block a user