From df1ed2d6e93f0259855b6b3606cd112e60654fb1 Mon Sep 17 00:00:00 2001 From: dmitrii-f-t27 Date: Sat, 17 Oct 2026 00:43:51 -0300 Subject: [PATCH] spec(port): port specs/port/trios-railway/crates/trios-igla-race/src/ema.t27 (Closes #9683) Port gHashTag/trios-railway:crates/trios-igla-race/src/ema.rs (at 385c2143ef46, 18 lines) as specs/port/trios-railway/crates/ trios-igla-race/src/ema.t27 - Roadmap Stage 6, goal #4548, target t27c gen-rust. The EMA tracker's decisions are ported as pure functions and tested (6 tests, 6 pass, 0 vacuous, 11 runtime asserts, 100% function coverage): the tracker starts unseeded, the first update seeds the average with the sample, every following update blends it as v * (1 - alpha) + x * alpha - including the alpha extremes, where alpha 0 freezes the average and alpha 1 tracks the sample. The Rust Option becomes the presence flag on the state, and the &mut self threading becomes the state chain, keeping both listed functions pure. No transport exists in this port. Verified: parse OK, spec-status IMPLEMENTED, typecheck 0 errors, coverage 3/3, test-report 6/6 pass, 0 BLOCKED, gen 65 lines with 0 'not yet implemented', boundary exactly this one file, dupe_scan: all 3 names unique across 2040 specs, bare body scan adds no duplicate group (752/221 unchanged). Dmitrii Fedorov ([@dmitrii-f-t27](https://github.com/dmitrii-f-t27)) --- .../crates/trios-igla-race/src/ema.t27 | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 specs/port/trios-railway/crates/trios-igla-race/src/ema.t27 diff --git a/specs/port/trios-railway/crates/trios-igla-race/src/ema.t27 b/specs/port/trios-railway/crates/trios-igla-race/src/ema.t27 new file mode 100644 index 0000000000..a026b650bb --- /dev/null +++ b/specs/port/trios-railway/crates/trios-igla-race/src/ema.t27 @@ -0,0 +1,90 @@ +// trios-igla-race EMA tracker — the exponential moving average. +// Port of gHashTag/trios-railway:crates/trios-igla-race/src/ema.rs +// (at 385c2143ef46, 18 lines) — Roadmap Stage 6, goal #4548, target t27c +// gen-rust. +// +// The original tracks an exponential moving average: `new` builds the +// tracker with its alpha and no value; `update` seeds the average with +// the first sample it sees, and blends every following sample as +// v * (1 - alpha) + x * alpha. The Rust `Option` value becomes the +// presence flag on the port's state, and the &mut self threading becomes +// the state chain: update takes the state and answers the next state, +// keeping both listed functions pure and tested. No transport exists in +// this port (issue #9683, "Port the decision, not the plumbing"). + +module port::trios_igla_race_ema; + +// Original lines 1-4: `pub struct EmaTracker { alpha, value: Option }`. +pub struct EmaState { + alpha: f64, + has_value: bool, + value: f64, +} + +// Original lines 6-9: `pub fn new(alpha) -> Self` — the tracker starts +// unseeded: no sample seen yet, the first update will seed it. +pub fn EmaTracker_new(alpha: f64) -> EmaState { + return EmaState { alpha: alpha, has_value: false, value: 0.0 }; +} + +// Original line 13: `v * (1.0 - alpha) + x * alpha` — the EMA blend. +pub fn ema_blend(v: f64, x: f64, alpha: f64) -> f64 { + return v * (1.0 - alpha) + x * alpha; +} + +// Original lines 11-17: `pub fn update(&mut self, x) -> f64` — the two +// decisions: an unseeded tracker takes the sample as its value (line 14, +// the seed), and a seeded one blends the sample into the average (line +// 13). The state chain carries both. +pub fn EmaTracker_update(state: EmaState, x: f64) -> EmaState { + if (!state.has_value) { + return EmaState { alpha: state.alpha, has_value: true, value: x }; + } + return EmaState { + alpha: state.alpha, + has_value: true, + value: ema_blend(state.value, x, state.alpha), + }; +} + +// --------------------------------------------------------------------------- +// Tests: the seed decision, the blend formula, and the tracker's +// behaviour at the alpha extremes. +// --------------------------------------------------------------------------- + +test new_starts_unseeded { + const s = EmaTracker_new(0.1); + assert(!s.has_value); + assert(s.alpha == 0.1); +} + +test first_update_seeds_with_the_sample { + const s = EmaTracker_update(EmaTracker_new(0.1), 5.0); + assert(s.has_value); + assert(s.value == 5.0); +} + +test blend_formula { + assert(ema_blend(10.0, 20.0, 0.25) == 12.5); + assert(ema_blend(10.0, 20.0, 0.0) == 10.0); + assert(ema_blend(10.0, 20.0, 1.0) == 20.0); +} + +test update_blends_after_the_seed { + const seeded = EmaTracker_update(EmaTracker_new(0.5), 2.0); + const blended = EmaTracker_update(seeded, 4.0); + assert(blended.has_value); + assert(blended.value == 3.0); +} + +test zero_alpha_freezes_the_average { + const seeded = EmaTracker_update(EmaTracker_new(0.0), 7.0); + const frozen = EmaTracker_update(seeded, 100.0); + assert(frozen.value == 7.0); +} + +test one_alpha_tracks_the_sample { + const seeded = EmaTracker_update(EmaTracker_new(1.0), 7.0); + const tracked = EmaTracker_update(seeded, 100.0); + assert(tracked.value == 100.0); +}