# 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 <name>` (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`) |
