std vocabulary gap: Sign / Select / CumSum primitives #281
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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 helpersign0()incrates/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
Counterprimitive needed — it composes:count_true = CumSum(Select(cond, 1.0, 0.0)). Orthogonal set over convenience set.Notes
crates/aura-std/src/vocabulary.rs:134) — this is a vocabulary decision, not a refactor; hence an issue rather than a drive-by PR.OpenMomentumnode 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.VolTfStop(zero-arg, param-generic, currently roster-absent pervocabulary.rs:11-18scoping comment) should join the roster in the same pass.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 existingLatchis an SR Bool register, not a sample-and-hold.GatedSMA{ value: F64, gate: Bool } -> F64, paramlength— 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 (viaSelect)."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
Resampleexposes 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.Simplification (from review discussion — the "gate in front of a generic closure" observation): withdrawing
GatedSMAandSampleHoldfrom the candidate set. Both were node-level solutions to a fabric-level concern.The engine already has the general mechanism:
eval -> Noneis 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), andFiring::Anyis 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— forwardsvalueiffgate, else quiet (None).makes every existing stateful reducer gated by composition, unmodified:
GatedSMA(x, g, N)≡SMA(When(x, g), N); a gatedCumSum/Delaycompose identically. Per-node gated variants would have been the combinatorial explosion (every reducer × gated/ungated);Whenis 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:
Whenfeeding aBarrier(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.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.
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.
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.