90e298d3b4
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.
180 lines
10 KiB
Markdown
180 lines
10 KiB
Markdown
# 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`) |
|