docs: add components/sud.py step lifecycle spec

Captures the ramp/hold/user-confirm phase order and a couple of
non-obvious clarifications from a design discussion (ramp is gated
purely by the schedule's 'ramp' key, not actual-vs-target temp;
WAIT_USER is a distinct phase, not an extended hold), plus one open
gap: pure-hold steps never re-set the controller's target temperature.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LhiQe64F74uHV8jzuoSa5K
This commit is contained in:
2026-06-22 08:40:45 +02:00
co-authored by Claude Sonnet 4.6
parent 7d79d885b5
commit 48d5235558
+58
View File
@@ -0,0 +1,58 @@
# 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.