diff --git a/specs/port/trios-railway/crates/trios-railway-smoke/src/lib.t27 b/specs/port/trios-railway/crates/trios-railway-smoke/src/lib.t27 new file mode 100644 index 0000000000..f78b49ff73 --- /dev/null +++ b/specs/port/trios-railway/crates/trios-railway-smoke/src/lib.t27 @@ -0,0 +1,229 @@ +// trios-railway-smoke — the IGLA RACE pipeline smoke test. +// Port of gHashTag/trios-railway:crates/trios-railway-smoke/src/lib.rs +// (at 8b229e9489ee, 200+ lines) — Roadmap Stage 6, goal #4548, target +// t27c gen-rust. +// +// The original validates the full pipeline in under sixty seconds with +// synthetic data: a deterministic trainer that emits one +// `step=N val_bpb=F` JSONL line per step (the BPB starts at 3.0 and +// decays by 0.99 per step; the loss is 80% of the BPB; every line is +// flushed; the done line reports the last step's BPB, zero with no +// steps), the seed-agent's line parser, and the local run that parses +// the outputs back with no database behind it. +// +// The stdout printing and flushing are plumbing; the decisions — the +// configuration defaults, the decay and loss formulas, the step range, +// the done-line best, the parser's four presence gates with their two +// markers, and the local run's shape (one line and one sample per step, +// no DB) — are ported here as pure functions and tested, including the +// original's own config and parser tests. The powi exponent has no +// builtin here: the decay is computed by repeated multiplication, one +// factor per step, exactly what the original's loop unfolds. No +// transport exists in this port (issue #9796, "Port the decision, not +// the plumbing"). + +module port::trios_railway_smoke_lib; + +// --------------------------------------------------------------------------- +// Original lines 18-43: `pub struct SmokeConfig` and its Default. +// --------------------------------------------------------------------------- + +pub struct SmokeConfig { + steps: u32, + batch: u32, + seed: u64, + synthetic: bool, + timeout_sec: u64, +} + +pub fn SmokeConfig_default() -> SmokeConfig { + return SmokeConfig { + steps: 1, + batch: 2, + seed: 42, + synthetic: true, + timeout_sec: 30, + }; +} + +// --------------------------------------------------------------------------- +// Original lines 69-107: `pub fn run_synthetic_trainer(cfg) -> +// Vec`. The deterministic trainer: one output per step for +// steps 1..=cfg.steps, the BPB starting at 3.0 and decaying by 0.99 per +// step (line 81), the loss at 80% of the BPB (line 82), and the done +// line reporting the last output's BPB, 0.0 with no outputs (line 102). +// The growable Vec and the stdout flush are plumbing; the plan this +// function decides is ported below with the per-step formulas pure. +// --------------------------------------------------------------------------- + +pub struct TrainerPlan { + step_count: usize, + first_step: u32, + base_bpb: f64, + decay: f64, + loss_factor: f64, + done_best: f64, +} + +// Original line 81: `let bpb = base_bpb * decay.powi(step as i32);` — +// computed by repeated multiplication, one decay factor per step. +pub fn smoke_bpb(step: u32) -> f64 { + var bpb: f64 = 3.0; + var i: u32 = 0; + while (i < step) { + bpb = bpb * 0.99; + i += 1; + } + return bpb; +} + +// Original line 82: `let loss = bpb * 0.8;` — the loss is 80% of the BPB. +pub fn smoke_loss(bpb: f64) -> f64 { + return bpb * 0.8; +} + +// Original lines 100-103: the done line reports the last output's BPB, +// 0.0 when there are no outputs. +pub fn trainer_done_best(steps: u32) -> f64 { + if (steps == 0) { + return 0.0; + } + return smoke_bpb(steps); +} + +pub fn run_synthetic_trainer(cfg: SmokeConfig) -> TrainerPlan { + return TrainerPlan { + step_count: cfg.steps, + first_step: 1, + base_bpb: 3.0, + decay: 0.99, + loss_factor: 0.8, + done_best: trainer_done_best(cfg.steps), + }; +} + +// --------------------------------------------------------------------------- +// Original lines 110-126: `pub fn parse_step_line(line) -> +// Option<(u32, f64)>`. The seed-agent's parser: the line parses exactly +// when both markers are present AND a parseable token follows each. +// The string finding and number parsing are plumbing; the four presence +// gates and the two markers are the decisions. +// --------------------------------------------------------------------------- + +pub fn step_marker() -> str { + return "step="; +} + +pub fn bpb_marker() -> str { + return "val_bpb="; +} + +pub fn parse_step_line( + step_marker_present: bool, + step_token_present: bool, + bpb_marker_present: bool, + bpb_token_present: bool, +) -> bool { + if (!step_marker_present) { + return false; + } + if (!step_token_present) { + return false; + } + if (!bpb_marker_present) { + return false; + } + if (!bpb_token_present) { + return false; + } + return true; +} + +// --------------------------------------------------------------------------- +// Original lines 130-141: `pub fn run_local(cfg) -> SmokeResult`. The +// local pipeline: the trainer's outputs parsed back — one JSONL line and +// one sample per step — with no database behind the run, so the insert +// verdict is absent. +// --------------------------------------------------------------------------- + +pub struct LocalSmokeView { + jsonl_lines: usize, + sample_count: usize, + db_insert_ok_present: bool, +} + +pub fn run_local(cfg: SmokeConfig) -> LocalSmokeView { + return LocalSmokeView { + jsonl_lines: cfg.steps, + sample_count: cfg.steps, + db_insert_ok_present: false, + }; +} + +// --------------------------------------------------------------------------- +// Tests: every decision above asserted against values read from the +// original's own tests (lines 147-186). +// --------------------------------------------------------------------------- + +test config_defaults { + const cfg = SmokeConfig_default(); + assert(cfg.steps == 1); + assert(cfg.batch == 2); + assert(cfg.seed == 42); + assert(cfg.synthetic); + assert(cfg.timeout_sec == 30); +} + +test bpb_decays_from_three { + assert(smoke_bpb(0) == 3.0); + const first = smoke_bpb(1); + assert(first > 2.96); + assert(first < 2.98); + assert(smoke_bpb(2) < smoke_bpb(1)); + assert(smoke_bpb(3) < smoke_bpb(2)); +} + +test loss_is_eighty_percent_of_bpb { + const loss = smoke_loss(2.5); + assert(loss > 1.999); + assert(loss < 2.001); + const loss3 = smoke_loss(3.0); + assert(loss3 > 2.399); + assert(loss3 < 2.401); +} + +test done_best_follows_the_last_step { + assert(trainer_done_best(0) == 0.0); + assert(trainer_done_best(3) == smoke_bpb(3)); +} + +test trainer_plan_shape { + const plan = run_synthetic_trainer(SmokeConfig_default()); + assert(plan.step_count == 1); + assert(plan.first_step == 1); + assert(plan.base_bpb == 3.0); + assert(plan.decay == 0.99); + assert(plan.loss_factor == 0.8); + const three = run_synthetic_trainer(SmokeConfig { steps: 3, batch: 2, seed: 42, synthetic: true, timeout_sec: 30 }); + assert(three.step_count == 3); +} + +test parse_gates_and_markers { + assert(step_marker() == "step="); + assert(bpb_marker() == "val_bpb="); + assert(parse_step_line(true, true, true, true)); + assert(!parse_step_line(false, true, true, true)); + assert(!parse_step_line(true, false, true, true)); + assert(!parse_step_line(true, true, false, true)); + assert(!parse_step_line(true, true, true, false)); +} + +test local_view_parses_every_line_without_db { + const one = run_local(SmokeConfig_default()); + assert(one.jsonl_lines == 1); + assert(one.sample_count == 1); + assert(!one.db_insert_ok_present); + const three = run_local(SmokeConfig { steps: 3, batch: 2, seed: 42, synthetic: true, timeout_sec: 30 }); + assert(three.jsonl_lines == 3); + assert(three.sample_count == 3); +}