# components/sud.py step lifecycle spec Confirmed spec for a `Sud` schedule step's internal phase order (`components/sud.py`), from a design discussion clarifying how `ramp`/ `hold`/user-confirm interact. ## Phase order `ramp` -> `hold` -> user-confirm. Each step independently opts into `ramp` and/or `hold` by including that key (`_build_step()`, `sud.py:35-45`); the other phase is skipped entirely if its key is absent. The `'ramp'` key's presence is the *only* trigger for ramping (`_advance()`, `sud.py:192-210`) - there is no diff-based decision (`T_actual` vs. `T_target`) anywhere that decides whether to ramp, and no internal setpoint-trajectory limiter in the temperature controller (`set_theta_soll()` is an instant, unconditional assignment, `components/pid/temp_controller_base.py:81-82`). The `ramp` key both gates ramping *and* carries its settings (`rate`); it does not merely override settings on top of an always-on ramp. ## Duration `0` Supported for hold: `hold_remaining = duration * 60.0` can be `0`; `tick()` (`sud.py:186-190`) finishes the step on the next tick once `hold_remaining <= 0`. ## End-of-step branching `_finish_step()` (`sud.py:212-216`): no `user_wait_for_continue` -> advances immediately to the next step; `user_wait_for_continue: true` -> enters `WAIT_USER`, resumed only by `confirm()`. `WAIT_USER` is not "hold, prolonged": `HOLDING` and `WAIT_USER` are distinct `SudState` values. The hold countdown runs to completion *before* `WAIT_USER` begins - `tick()` only decrements `hold_remaining` while `state == HOLDING`, so the timer is frozen (not extended) during the wait. It delays the next step, but it's a separate third phase, not a longer hold. The gating field is `user_wait_for_continue`, not "presence of a user message": `user_message` (display text) and `user_wait_for_continue` (the actual block) are independent fields - a step can show a message without blocking, or block silently. ## Open items - [ ] **Pure-hold steps never (re-)set the controller's target temperature.** For a step with only `hold` (no `ramp`), `tasks/sud.py`'s `on_step_changed` never calls `tc.set_theta_soll()` - the guard at line 73 requires `ramp is not None`. The controller's target temperature for a pure-hold step is whatever was left from the previous step's ramp; there's no explicit fallback to the hold step's own `temperature` field. `README.md` (Mash schedules section) documents this as designed behavior ("`SudTask` drives ... `theta_soll`/`heatrate_soll` from ramp steps ... counts down hold steps' duration"), so it's intentional, not a bug - but it means a schedule can't express a temperature jump straight into a hold without a `ramp` block.