# Fieldtest — cycle-0008 (sum combinators) — 2026-06-04 **Status:** Draft — awaiting orchestrator triage **Author:** fieldtester (dispatched by fieldtest skill) ## Scope Cycle 0008 ships the missing sum combinator(s) in `aura-std` so the north-star "combine one signal with another" research move (C10) is expressible from shipped blocks alone — retiring the hand-authored `Add2` the cycle-0007 fieldtest had to write: - `Add` — parameterless two-input `f64` sum (input 0 + input 1), the companion to `Sub`. Emits `None` until both inputs have a value. - `LinComb { weights }` — N-input weighted sum `Σ weights[i]·input[i]`. The `weights` are construction parameters that configure the node and fix its arity (`weights.len()` input slots, in slot order). Emits `None` until *all* inputs have a value. `LinComb::new(vec![])` panics (build-time param error). `LinComb([1,1])` is `Add`; `LinComb([1,-1])` is `Sub`. No engine or manifest change this cycle (purely additive `aura-std`, C16). **Build exercised:** all five binaries built and run from the current working tree via `cargo run --manifest-path fieldtests/cycle-0008-sum-combinators/Cargo.toml --bin ` (debug profile, HEAD source recompiled each invocation — confirmed by the `Compiling aura-core/std/engine` lines). The fixture is a standalone downstream-consumer crate that `path`-depends on the engine crates and uses only the public surface (rustdoc via `cargo doc`, the design ledger, the glossary, `docs/project-layout.md` — never `crates/*/src`). ## Examples ### fieldtests/cycle-0008-sum-combinators/c0008_1_combine_with_add.rs — north-star combine with shipped Add - Wires `price → {Sma(2),Sma(4)} → Sub` (fast spread) and `price → {Sma(3),Sma(6)} → Sub` (slow spread), sums the two spreads with the **shipped `Add`**, then `Exposure → SimBroker → Recorder` — the full signal-quality loop, the exact shape the 0007 fieldtest built with a hand-authored `Add2`. - Fits the cycle's primary axis: it is THE demonstration that the 0007 boilerplate is retired — `Add` drops straight in where `Add2` was. - Outcome: built, ran, matched expected. 16 pip-equity rows (one per price cycle, leading `0.0`s during warm-up exactly as `SimBroker`'s rustdoc describes); final equity `+10.6667`; two runs bit-identical (C1). ### fieldtests/cycle-0008-sum-combinators/c0008_2_lincomb_weighted.rs — weighted combine via LinComb - `price → {Sma(2),Sma(4)} → LinComb([0.7,0.3])`, tapping `Sma(2)`, `Sma(4)`, and the `LinComb` output with three recorders, asserting `combined == 0.7·sma2 + 0.3·sma4` on every fired cycle. - Fits the weighted-combine axis: confirms the *weighted* sum (not a plain sum) is what reaches exposure. - Outcome: built, ran, matched expected. 5 LinComb rows (warm-up gated by the slower `Sma(4)`); every row matched the weighted sum to < 1e-12. ### fieldtests/cycle-0008-sum-combinators/c0008_3_lincomb_three_legs.rs — variadic N>2 arity - `price → {Sma(2),Sma(3),Sma(4)} → LinComb([0.5,0.3,0.2])`, three legs, output asserted against the 3-leg weighted sum each cycle; first fire asserted at t=3 (the slowest leg's warm-up). - Fits the variadic-arity axis: arity is fixed by `weights.len()` = 3 input slots. - Outcome: built, ran, matched expected. 4 rows, first at t=3, all weighted sums matched. ### fieldtests/cycle-0008-sum-combinators/c0008_4_lincomb_edges.rs — warm-up barrier + empty-weights panic - (a) Two legs warming up at different times (`Sma(2)` at t=1, `Sma(5)` at t=4): asserts LinComb's first output is at t=4 and no row exists before both legs are warm (no cold leg silently folded in as `0.0`). - (b) `LinComb::new(vec![])` wrapped in `catch_unwind`: asserts it panics, as the rustdoc documents. - Fits the edge-behaviour axis (warm-up + empty-weights panic). - Outcome: built, ran, matched expected. First fire at t=4, no leading rows; the empty-weights call panicked as documented. ### fieldtests/cycle-0008-sum-combinators/c0008_5_lincomb_two_sources.rs — firing-policy probe (two heterogeneous sources) - `LinComb([1,1])` fed by two *independent* `f64` sources updating on different cycles (source A at t=0..5, source B only at t=1 and t=4). Records what LinComb emits to reveal its per-input firing policy (C6), which the public surface does not state. - Fits the variadic/combine axis at its genuinely-heterogeneous corner — the north-star "combine A with a different-rate B" the docs motivate. - Outcome: built, ran. Emitted 6 rows: once both legs are seen (t≥1), LinComb re-fires on *every* fresh cycle combining the fresh leg with the held value of the other (mode A / as-of join), and at t=4 — where both sources fire as two distinct same-timestamp cycles (C4) — it emits **two** rows (114.0 then 124.0). This is internally consistent (C4/C5) but is not predictable from the rustdoc; see the spec_gap below. ## Findings ### [working] North-star combine expressed with shipped Add — Add2 retired - Example 1. - Two MA-cross spreads summed by `aura_std::Add` into one exposure, end to end through `SimBroker` to a recorded pip curve; bit-identical across two runs. `Add::new()` dropped in exactly where the 0007 fixture had a hand-authored `Add2` — same wiring shape, one fewer node definition to author. - Why working: the cycle's primary promise (retire the 0007 boilerplate) is met empirically — the combinator exists, composes, and the curve is deterministic. - Recommended action: carry-on. ### [working] LinComb weighted & variadic sums are arithmetically exact - Examples 2 and 3. - `LinComb([0.7,0.3])` and `LinComb([0.5,0.3,0.2])` produced outputs matching the closed-form weighted sum to < 1e-12 on every fired cycle; arity followed `weights.len()`; warm-up gated correctly by the slowest leg. - Why working: the weighted/variadic behaviour the rustdoc promises holds, on first try, with no surprises in the same-source case. - Recommended action: carry-on. ### [working] Warm-up barrier and empty-weights panic behave as documented - Example 4. - LinComb withheld output until every leg had a value (no cold-leg-as-`0.0`); `LinComb::new(vec![])` panicked as the `# Panics` rustdoc states. - Why working: the two documented edge behaviours are observable and correct. - Recommended action: carry-on. ### [spec_gap] Add / LinComb per-input firing policy is absent from the public surface - Example 5 (surfaced); latent in 1–4 (all same-source, so co-fresh by construction, hiding the question). - What happened: combining two *different-rate* sources, LinComb re-fires on every cycle in which any leg is fresh (once both have been seen), pairing the fresh leg with the held value of the other — i.e. **mode A / as-of join** (`Firing::Any`). The rustdoc says only "Emits `None` until all inputs have a value"; it does not say whether a combine is an as-of join (mode A) or an all-fresh barrier (mode B). A downstream consumer combining, say, an M5 signal with a daily-bias leg cannot predict from the public surface whether the bias is held-and-combined every M5 cycle (mode A) or only combined on the rare cycles where both are fresh (mode B). The two readings give materially different equity curves. - Why spec_gap: the surface is silent; I picked the natural reading (mode A, the as-of join — the C6 default for fire-on-fresh combination) and it matched what shipped, but mode B is an equally plausible reading of "combine two signals," and the choice is invisible. This is the **same class of gap** the cycle-0007 fieldtest raised for `SimBroker` (firing policy + warm-up shape not on the surface), which was then patched into `SimBroker`'s rustdoc — `Add`/`LinComb` ship without the equivalent note. - Recommended action: tighten the design ledger / rustdoc — document on the `Add` and `LinComb` struct docs that both inputs are `Firing::Any` (mode A, as-of join: a fresh leg combines with the held value of the others), mirroring the note already added to `SimBroker`. ### [spec_gap] Multiple combined rows at one timestamp (same-ts multi-source) is undocumented - Example 5. - What happened: at t=4 LinComb emitted **two** rows (114.0 then 124.0) because source A and source B both carry t=4 and are therefore two distinct cycles (C4 tie-break by source order), and a mode-A node fires once per fresh cycle. A consumer reading "one combined value per cycle" naturally expects one row per *timestamp* and is surprised to find two. - Why spec_gap: this is correct and forced by C4/C5 (same timestamp from two sources = two cycles), but nothing on the public combinator surface tells a consumer that a recorded combined stream can contain multiple rows sharing one `event_ts`. The reading "one row per timestamp" is plausible and wrong; the shipped reading "one row per cycle" is right but undocumented at this surface. - Recommended action: ratify (the behaviour is correct per C4) and tighten the rustdoc — a one-line note on the combinator (or in project-layout's recorded- trace description) that with heterogeneous same-timestamp sources a fired node emits one row per *cycle*, so a trace may carry >1 row per timestamp. ### [friction] Standalone consumer crate still needs the empty [workspace] table - All examples (the fixture `Cargo.toml`). - What happened: a nested consumer crate under the engine repo must carry an empty `[workspace]` table or cargo refuses to build it (walks up to the engine workspace). I added it from the documented hint (`docs/project-layout.md`), so it cost only the one line — but it is still a hand-step every nested consumer pays until `aura new` exists. - Why friction: the task completed, but the surface still forces a boilerplate line the docs now *predict* (cycle-0006/0007 raised this; #9 closed the docs half). Recording it keeps the residual cost visible until the scaffolder lands. - Recommended action: carry-on / plan — fold the emitted `[workspace]` line into the future `aura new` scaffolder (already tracked); no new action this cycle. ## Recommendation summary | Finding | Class | Action | |---|---|---| | North-star combine with shipped Add — Add2 retired | working | carry-on | | LinComb weighted & variadic sums exact | working | carry-on | | Warm-up barrier + empty-weights panic as documented | working | carry-on | | Add/LinComb firing policy absent from public surface | spec_gap | tighten ledger/rustdoc (mirror SimBroker note) | | Multiple combined rows at one timestamp undocumented | spec_gap | ratify + tighten rustdoc | | Consumer crate still needs empty `[workspace]` | friction | carry-on (folds into `aura new`) |