First fieldtest of the sum combinators. A standalone downstream-consumer crate
(fieldtests/cycle-0008-sum-combinators/) path-depends on the engine crates and
exercises the post-0008 surface from the public interface only (rustdoc + ledger
+ glossary + project-layout, never crates/*/src).
Primary axis empirically met: the north-star two-signal combine move now uses the
shipped Add (Add::new() dropped in exactly where the 0007 fixture hand-authored
an Add2) — the 0007 boilerplate is retired, end to end through SimBroker to a
deterministic recorded pip curve.
Findings: 0 bugs, 1 friction, 2 spec_gaps, 3 working.
- working ×3: Add2 retired; LinComb weighted/variadic sums exact (<1e-12);
warm-up barrier + empty-weights panic as documented.
- spec_gap: Add/LinComb per-input firing policy (Firing::Any / mode-A as-of
join) absent from the public surface — same class as the 0007 SimBroker gap.
- spec_gap: a fired combinator emits one row per *cycle*, so a heterogeneous
same-timestamp multi-source trace can carry >1 row per timestamp (correct per
C4/C5, undocumented at this surface).
- friction: nested consumer crate still needs the empty [workspace] table
(carried from 0006/0007; #9 closed the docs half; folds into aura new).
Spec feeds the next plan as reference.
10 KiB
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-inputf64sum (input 0 + input 1), the companion toSub. EmitsNoneuntil both inputs have a value.LinComb { weights }— N-input weighted sumΣ weights[i]·input[i]. Theweightsare construction parameters that configure the node and fix its arity (weights.len()input slots, in slot order). EmitsNoneuntil all inputs have a value.LinComb::new(vec![])panics (build-time param error).LinComb([1,1])isAdd;LinComb([1,-1])isSub.
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) andprice → {Sma(3),Sma(6)} → Sub(slow spread), sums the two spreads with the shippedAdd, thenExposure → SimBroker → Recorder— the full signal-quality loop, the exact shape the 0007 fieldtest built with a hand-authoredAdd2. - Fits the cycle's primary axis: it is THE demonstration that the 0007
boilerplate is retired —
Adddrops straight in whereAdd2was. - Outcome: built, ran, matched expected. 16 pip-equity rows (one per price
cycle, leading
0.0s during warm-up exactly asSimBroker'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]), tappingSma(2),Sma(4), and theLinComboutput with three recorders, assertingcombined == 0.7·sma2 + 0.3·sma4on 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 as0.0). - (b)
LinComb::new(vec![])wrapped incatch_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 independentf64sources 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::Addinto one exposure, end to end throughSimBrokerto a recorded pip curve; bit-identical across two runs.Add::new()dropped in exactly where the 0007 fixture had a hand-authoredAdd2— 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])andLinComb([0.5,0.3,0.2])produced outputs matching the closed-form weighted sum to < 1e-12 on every fired cycle; arity followedweights.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# Panicsrustdoc 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 "EmitsNoneuntil 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 intoSimBroker's rustdoc —Add/LinCombship without the equivalent note. - Recommended action: tighten the design ledger / rustdoc — document on the
AddandLinCombstruct docs that both inputs areFiring::Any(mode A, as-of join: a fresh leg combines with the held value of the others), mirroring the note already added toSimBroker.
[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 untilaura newexists. - 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 futureaura newscaffolder (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) |