std vocabulary gap: Sign / Select / CumSum primitives #281

Closed
opened 2026-07-17 17:18:10 +02:00 by claude · 5 comments
Collaborator

Surfaced by the UK100 open-momentum scratch study: the roster has Bool producers (Gt, EqConst, And) but no general Bool→F64 bridge back, no sign extractor, and no generic run-length accumulator — so elementary conditional logic is not blueprint-composable.

Proposed cells (all zero-arg builders → roster-eligible; one scalar per eval → C8-clean)

  • Sign { value: F64 } -> F64 (−1.0 / 0.0 / +1.0). The need already exists inside std as the private helper sign0() in crates/aura-std/src/position_management.rs:101-109 — inlined instead of vocabulary.
  • Select { cond: Bool, then: F64, otherwise: F64 } -> F64. The 2:1 mux — the missing RTL standard cell; today conditional values require arithmetic contortions because nothing consumes Bool back into the F64 plane generically.
  • CumSum { series: F64 } -> F64. Run-length accumulation with O(1) scalar state. Exists three times domain-embedded instead of once generically: SimBroker.cum (sim_broker.rs:41), PositionManagement.cum_realized_r (position_management.rs:65), CostRunner.cum (cost.rs:143-151).

No Counter primitive needed — it composes: count_true = CumSum(Select(cond, 1.0, 0.0)). Orthogonal set over convenience set.

Notes

  • The roster is deliberately test-pinned at 29 entries (crates/aura-std/src/vocabulary.rs:134) — this is a vocabulary decision, not a refactor; hence an issue rather than a drive-by PR.
  • Honest scope limit: these cells would NOT have made the scratch study's OpenMomentum node blueprint-composable (cross-day "rolling rate over the last N qualifying days, excluding today" has no window primitive that counts days rather than cycles — role-2 escalation was correct there). They shrink such nodes and make simpler conditional strategies composable; they don't replace them.
  • Candidate follow-up question for the same decision: whether VolTfStop (zero-arg, param-generic, currently roster-absent per vocabulary.rs:11-18 scoping comment) should join the roster in the same pass.
Surfaced by the UK100 open-momentum scratch study: the roster has Bool *producers* (`Gt`, `EqConst`, `And`) but no general Bool→F64 bridge back, no sign extractor, and no generic run-length accumulator — so elementary conditional logic is not blueprint-composable. ## Proposed cells (all zero-arg builders → roster-eligible; one scalar per eval → C8-clean) - **`Sign`** `{ value: F64 } -> F64` (−1.0 / 0.0 / +1.0). The need already exists *inside* std as the private helper `sign0()` in `crates/aura-std/src/position_management.rs:101-109` — inlined instead of vocabulary. - **`Select`** `{ cond: Bool, then: F64, otherwise: F64 } -> F64`. The 2:1 mux — the missing RTL standard cell; today conditional values require arithmetic contortions because nothing consumes Bool back into the F64 plane generically. - **`CumSum`** `{ series: F64 } -> F64`. Run-length accumulation with O(1) scalar state. Exists three times domain-embedded instead of once generically: `SimBroker.cum` (`sim_broker.rs:41`), `PositionManagement.cum_realized_r` (`position_management.rs:65`), `CostRunner.cum` (`cost.rs:143-151`). No `Counter` primitive needed — it composes: `count_true = CumSum(Select(cond, 1.0, 0.0))`. Orthogonal set over convenience set. ## Notes - The roster is deliberately test-pinned at 29 entries (`crates/aura-std/src/vocabulary.rs:134`) — this is a vocabulary decision, not a refactor; hence an issue rather than a drive-by PR. - Honest scope limit: these cells would NOT have made the scratch study's `OpenMomentum` node blueprint-composable (cross-day "rolling rate over the last N *qualifying days*, excluding today" has no window primitive that counts days rather than cycles — role-2 escalation was correct there). They shrink such nodes and make simpler conditional strategies composable; they don't replace them. - Candidate follow-up question for the same decision: whether `VolTfStop` (zero-arg, param-generic, currently roster-absent per `vocabulary.rs:11-18` scoping comment) should join the roster in the same pass.
claude added the feature label 2026-07-17 17:18:10 +02:00
Author
Collaborator

Correction + extension, prompted by review discussion: the issue text claims the scratch study's "rolling rate over the last N qualifying days, excluding today" is not composable in principle ("no window primitive that counts days rather than cycles"). That claim is wrong as stated. The day-ness dissolves into an event stream: what a sample is is defined by a gate Bool, not by the statistic node. Two further generic cells make the whole construct blueprint-composable:

  • SampleHold { value: F64, gate: Bool } -> F64 — F64 latch with enable (holds the value from the last gate=true cycle). The existing Latch is an SR Bool register, not a sample-and-hold.
  • GatedSMA { value: F64, gate: Bool } -> F64, param length — SMA with a clock-enable: ring buffer over the last N gated samples, pushed only on gate=true, mean emitted every cycle. A "rate" is the mean of a 0/1 stream (via Select).

"Excluding today" is then pure gate causality: the qualify gate fires on the day-closing rollover (k→4), so during today's read window today's sample is not yet in the buffer — no special exclusion semantics needed.

Sketch over SessionFrankfurt(5) + Resample(5): d0 = SampleHold(Sign(Sub(bar.close, bar.open)), EqConst(k,1)); cont = Select(Gt(Mul(agg_sign, d0), 0), 1, 0); p_long/p_short = GatedSMA(cont, And(qualify, Gt(d0,0)/Gt(0,d0))).

Pattern note: all five candidates (Sign, Select, CumSum, SampleHold, GatedSMA) are one category — the missing clock-enable standard cells of the RTL metaphor.

Honest residue that keeps role-2 nodes legitimate for this study: (a) the offline study's data-quality gating (≥3 M1 bars per bucket, C0-anchor presence) is unreachable unless Resample exposes its per-bar sample count as an output field; (b) the whole composition sits on #280 — without jitter normalization in Resample/Session it computes on shifted candles.

Correction + extension, prompted by review discussion: the issue text claims the scratch study's "rolling rate over the last N qualifying days, excluding today" is not composable in principle ("no window primitive that counts days rather than cycles"). That claim is wrong as stated. The day-ness dissolves into an event stream: what a sample *is* is defined by a gate Bool, not by the statistic node. Two further generic cells make the whole construct blueprint-composable: - **`SampleHold`** `{ value: F64, gate: Bool } -> F64` — F64 latch with enable (holds the value from the last gate=true cycle). The existing `Latch` is an SR Bool register, not a sample-and-hold. - **`GatedSMA`** `{ value: F64, gate: Bool } -> F64`, param `length` — SMA with a clock-enable: ring buffer over the last N *gated samples*, pushed only on gate=true, mean emitted every cycle. A "rate" is the mean of a 0/1 stream (via `Select`). "Excluding today" is then pure gate causality: the qualify gate fires on the day-closing rollover (k→4), so during today's read window today's sample is not yet in the buffer — no special exclusion semantics needed. Sketch over `SessionFrankfurt(5)` + `Resample(5)`: `d0 = SampleHold(Sign(Sub(bar.close, bar.open)), EqConst(k,1))`; `cont = Select(Gt(Mul(agg_sign, d0), 0), 1, 0)`; `p_long/p_short = GatedSMA(cont, And(qualify, Gt(d0,0)/Gt(0,d0)))`. Pattern note: all five candidates (Sign, Select, CumSum, SampleHold, GatedSMA) are one category — the missing **clock-enable standard cells** of the RTL metaphor. Honest residue that keeps role-2 nodes legitimate for this study: (a) the offline study's data-quality gating (≥3 M1 bars per bucket, C0-anchor presence) is unreachable unless `Resample` exposes its per-bar sample count as an output field; (b) the whole composition sits on #280 — without jitter normalization in Resample/Session it computes on shifted candles.
Author
Collaborator

Simplification (from review discussion — the "gate in front of a generic closure" observation): withdrawing GatedSMA and SampleHold from the candidate set. Both were node-level solutions to a fabric-level concern.

The engine already has the general mechanism: eval -> None is a quiet cycle (precedent: Resample's emit-on-rollover), the run loop is freshness-gated (aura-engine/src/lib.rs:9 — a node re-evaluates only on fresh input, so downstream state advances per firing, not per cycle), and Firing::Any is fire-on-any-fresh + hold (aura-core/src/node.rs:19-27) — i.e. sparse→dense sample-and-hold is already port semantics.

So gating belongs in the stream, not in the node: one new cell

  • When { value: F64, gate: Bool } -> F64 — forwards value iff gate, else quiet (None).

makes every existing stateful reducer gated by composition, unmodified: GatedSMA(x, g, N)SMA(When(x, g), N); a gated CumSum/Delay compose identically. Per-node gated variants would have been the combinatorial explosion (every reducer × gated/ungated); When is the single clock-enable driver the RTL metaphor asks for.

Revised candidate set (orthogonal, all zero-arg → roster-eligible): Sign, Select, CumSum, When.

One design point to settle when this lands: When feeding a Barrier(N) group — a quiet member means the group does not fire that cycle (correct as-of the barrier contract, but worth an explicit doc sentence + test so the stall semantics are pinned deliberately).

The earlier sketch simplifies accordingly: p_long = SMA(When(cont, And(qualify, Gt(d0, 0))), N) — no bespoke windowed-rate cell at all.

Simplification (from review discussion — the "gate in front of a generic closure" observation): **withdrawing `GatedSMA` and `SampleHold`** from the candidate set. Both were node-level solutions to a fabric-level concern. The engine already has the general mechanism: `eval -> None` is a quiet cycle (precedent: `Resample`'s emit-on-rollover), the run loop is freshness-gated (`aura-engine/src/lib.rs:9` — a node re-evaluates only on fresh input, so downstream state advances per *firing*, not per cycle), and `Firing::Any` is fire-on-any-fresh **+ hold** (`aura-core/src/node.rs:19-27`) — i.e. sparse→dense sample-and-hold is already port semantics. So gating belongs in the *stream*, not in the node: one new cell - **`When`** `{ value: F64, gate: Bool } -> F64` — forwards `value` iff `gate`, else quiet (`None`). makes every existing stateful reducer gated by composition, unmodified: `GatedSMA(x, g, N)` ≡ `SMA(When(x, g), N)`; a gated `CumSum`/`Delay` compose identically. Per-node gated variants would have been the combinatorial explosion (every reducer × gated/ungated); `When` is the single clock-enable driver the RTL metaphor asks for. Revised candidate set (orthogonal, all zero-arg → roster-eligible): **`Sign`, `Select`, `CumSum`, `When`**. One design point to settle when this lands: `When` feeding a `Barrier(N)` group — a quiet member means the group does not fire that cycle (correct as-of the barrier contract, but worth an explicit doc sentence + test so the stall semantics are pinned deliberately). The earlier sketch simplifies accordingly: `p_long = SMA(When(cont, And(qualify, Gt(d0, 0))), N)` — no bespoke windowed-rate cell at all.
claude added this to the Measurement as a first-class citizen milestone 2026-07-17 18:08:01 +02:00
claude self-assigned this 2026-07-17 18:49:51 +02:00
Author
Collaborator

Fork decisions (derived autonomously, 2026-07-17) ahead of spec production for the four-cell set (Sign / Select / CumSum / When — the revised candidate set from this thread).

Fork: does VolTfStop join the roster in the same pass? (the follow-up question raised in the issue body) → No — out of scope for this pass.
Basis: derived. The milestone frames layer 2 as exactly the four orthogonal cells, and its acceptance sketch (the UK100 open-momentum study as a hand-authored blueprint) composes from SessionFrankfurt + Resample + the four cells — VolTfStop's roster membership serves no goal of this milestone. Roster membership is a deliberate vocabulary act (the count-pin exists to force that consciousness); folding an unrelated membership decision into this pass would be scope creep, not consistency. The question itself remains open and undecided — it is recorded here so closing this issue does not silently bury it.

Fork: When's input firing — co-fresh barrier vs any-fresh + hold?Firing::Any on both inputs (value, gate), warm-up None until both legs have seen a value.
Basis: derived. (1) A clock-enable in the RTL metaphor is level-sensitive: the enable line level is sampled when the register clocks, regardless of when it was last driven — any-fresh + hold is exactly that semantics, a barrier is not. (2) House style: every multi-input std cell (Gt, And, Latch, Select's siblings) declares Firing::Any; Barrier appears only where co-freshness is the point (Resample's OHLC legs). (3) Compositional reach: with a dense gate (the normal case — Bool producers Gt/EqConst/And emit every warm cycle) the two options are behaviourally identical, but with a sparse gate (a Resample-tier producer emitting on rollover only) Any+hold gives the useful reading — the enable stays asserted between gate emissions — while a barrier would stall the cell except at coincidence instants.

Settled details the spec will pin (from the sources, no fork): Sign adopts the sign0() semantics already private in position_management.rs (>0→+1, <0→−1, else 0.0 — NaN maps to 0.0), and sign0 itself stays untouched (minimal scope, no drive-by refactor). Select is the 2:1 mux with warm-up None until all three legs are warm. CumSum is Kahan/Neumaier-compensated like Sma, for the same accumulative-drift reason documented there. When forwards value iff gate is true, else eval→None (quiet cycle, Resample precedent) — and the spec pins, with an explicit doc sentence + test, that a quiet When feeding a Barrier(N) group stalls that group for the cycle (correct per the barrier contract: a quiet member is not at the current timestamp). Roster grows 29→33 with type ids "Sign", "Select", "CumSum", "When"; both count pins (the in-crate shape test and aura-cli's --vocabulary e2e) are bumped as the conscious act the pin exists for.

Fork decisions (derived autonomously, 2026-07-17) ahead of spec production for the four-cell set (Sign / Select / CumSum / When — the revised candidate set from this thread). **Fork: does VolTfStop join the roster in the same pass?** (the follow-up question raised in the issue body) → **No — out of scope for this pass.** Basis: derived. The milestone frames layer 2 as exactly the four orthogonal cells, and its acceptance sketch (the UK100 open-momentum study as a hand-authored blueprint) composes from SessionFrankfurt + Resample + the four cells — VolTfStop's roster membership serves no goal of this milestone. Roster membership is a deliberate vocabulary act (the count-pin exists to force that consciousness); folding an unrelated membership decision into this pass would be scope creep, not consistency. The question itself remains open and undecided — it is recorded here so closing this issue does not silently bury it. **Fork: When's input firing — co-fresh barrier vs any-fresh + hold?** → **Firing::Any on both inputs (value, gate), warm-up None until both legs have seen a value.** Basis: derived. (1) A clock-enable in the RTL metaphor is level-sensitive: the enable *line level* is sampled when the register clocks, regardless of when it was last driven — any-fresh + hold is exactly that semantics, a barrier is not. (2) House style: every multi-input std cell (Gt, And, Latch, Select's siblings) declares Firing::Any; Barrier appears only where co-freshness is the point (Resample's OHLC legs). (3) Compositional reach: with a dense gate (the normal case — Bool producers Gt/EqConst/And emit every warm cycle) the two options are behaviourally identical, but with a sparse gate (a Resample-tier producer emitting on rollover only) Any+hold gives the useful reading — the enable stays asserted between gate emissions — while a barrier would stall the cell except at coincidence instants. Settled details the spec will pin (from the sources, no fork): Sign adopts the sign0() semantics already private in position_management.rs (>0→+1, <0→−1, else 0.0 — NaN maps to 0.0), and sign0 itself stays untouched (minimal scope, no drive-by refactor). Select is the 2:1 mux with warm-up None until all three legs are warm. CumSum is Kahan/Neumaier-compensated like Sma, for the same accumulative-drift reason documented there. When forwards value iff gate is true, else eval→None (quiet cycle, Resample precedent) — and the spec pins, with an explicit doc sentence + test, that a quiet When feeding a Barrier(N) group stalls that group for the cycle (correct per the barrier contract: a quiet member is not at the current timestamp). Roster grows 29→33 with type ids "Sign", "Select", "CumSum", "When"; both count pins (the in-crate shape test and aura-cli's --vocabulary e2e) are bumped as the conscious act the pin exists for.
Author
Collaborator

Spec auto-signed (2026-07-17): the spec for the four clock-enable cells (Sign / Select / CumSum / When — roster growth 29→33, When's downstream hold/stall semantics pinned by doc + engine tests) was signed autonomously. The signature is the grounding-check PASS: an independent fresh-context review ratified every load-bearing assumption against currently-green tests (eleven assumptions, including the crux that a fired node returning None does not advance downstream freshness, and that Barrier groups stall on a quiet member). No human signed; planning proceeds. The spec is a git-ignored working file; its durable design record is the ledger lift at cycle close.

Spec auto-signed (2026-07-17): the spec for the four clock-enable cells (Sign / Select / CumSum / When — roster growth 29→33, When's downstream hold/stall semantics pinned by doc + engine tests) was signed autonomously. The signature is the grounding-check PASS: an independent fresh-context review ratified every load-bearing assumption against currently-green tests (eleven assumptions, including the crux that a fired node returning None does not advance downstream freshness, and that Barrier groups stall on a quiet member). No human signed; planning proceeds. The spec is a git-ignored working file; its durable design record is the ledger lift at cycle close.
Author
Collaborator

Shipped as part of the "Measurement as a first-class citizen" milestone (merged to main via cb6211c). The four clock-enable cells Sign/Select/CumSum/When are in the std roster (now 33). This issue stayed open only because the slice commits referenced it with rather than a closing marker; closing now that the work is merged.

Shipped as part of the "Measurement as a first-class citizen" milestone (merged to main via cb6211c). The four clock-enable cells Sign/Select/CumSum/When are in the std roster (now 33). This issue stayed open only because the slice commits referenced it with rather than a closing marker; closing now that the work is merged.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Brummel/Aura#281