check_output_clamp() sits one line below `await self.write_setpoint(self.target)` in cycle(), and moving it above the write left test_clamp.py, test_freeze.py and test_loop.py ALL GREEN. Measured on the move itself before this commit; measured again after, where test_clamp.py part 6 goes red and the other two are still green - the gap was real and it was only ever in this file. WHAT IS ACTUALLY LOST. The guard has two evidence sources. `self.loop_clamped` is set from the Decision well above the write and survives the move, and on the 10.3 runaway replay it is the source of every engagement the alarm reports (0 write-path engagements against 2 loop-side ones) - which is exactly what makes the move nearly invisible. `self.clamp_last` is assigned by write_setpoint() and by nothing else, is None until it has run, and is what `engaged_write` reads at main.py:856. Moved above the write, that consumer goes quietly false and a DEVICE-bound clamp - max_w above the inverter's own rating, SAFETY-20's install - is reported by nothing. A guard whose second evidence source can be silently disconnected is one refactor from being half a guard. WHAT THE CHECK PINS, AND WHAT IT DELIBERATELY DOES NOT. Not the position: the comment at that line warns against tidying SAFETY-03's ordering and SAFETY-18's into one rule, and a check a rename breaks is worse than none. It pins the CONSEQUENCE - what `self.clamp_last` held each time the guard consulted the watchdog - using a spy subclassed from the shipped ClampWatchdog over a slew-limited ramp where every cycle's command is a different number, so "this cycle's evidence" and "last cycle's" are distinguishable at all. The spy hooks `clamp_wd.note()`, the consumer, rather than the guard's name: that is what keeps a rename legitimate. The same assertions are re-run through a controller whose guard is renamed and whose write lives in a helper, and were verified against main.py itself - renaming the method throughout AND moving both calls into a new `_emit_and_guard()` helper keeps part 6 green; swapping the two calls turns it red on a line that names clamp_last. WHY AC 3 NEEDED A NEW STIMULUS, AND WHY IT IS NOT DRIVEN THROUGH cycle(). Through the real cycle() the write-path half cannot engage: the arbiter files the device range and max_w as P_SAFETY LIMIT claims, so resolution.target_w is already inside both bounds before write_setpoint() re-applies them. That duplication is deliberate defence in depth; part 6 now MEASURES the zero engagements rather than leaving it as a claim in a comment, and then drives the write path directly at max_w 5000 against a +-2500 W device - the wire carries 2500 W, every loop-side flag stays clean, and the alarm fires on clamp_last alone. With the two calls swapped the same stimulus clamps the same value and the alarm is silent, which is the ticket in one pair of runs. main.py gains one comment sentence naming the check, mirroring the one SAFETY-18's block already carries for test_freeze.py part 5. No behaviour changes. Full suite: 17/17 green in goodwe-addon-ci. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,41 @@
|
||||
|
||||
## Unreleased
|
||||
|
||||
**`check_output_clamp()` could be moved above `write_setpoint()` in `cycle()`
|
||||
with test_clamp.py, test_freeze.py and test_loop.py all staying GREEN — the
|
||||
guard's write-path evidence, `self.clamp_last`, had nothing watching it.**
|
||||
Measured before the fix, on the move itself: all three suites green. The guard
|
||||
has two evidence sources and only one of them was covered. `self.loop_clamped`
|
||||
is set from the `Decision` well above the write, so it survives the move — and
|
||||
on the §10.3 runaway replay it is the source of every engagement the alarm
|
||||
reports (0 write-path engagements against 2 loop-side ones), which is precisely
|
||||
what made the move nearly invisible. `self.clamp_last` is assigned by
|
||||
`write_setpoint()` and by nothing else (`main.py`), is `None` until it has run,
|
||||
and is what `engaged_write` reads; moved above the write, that consumer goes
|
||||
quietly false and a DEVICE-bound clamp — `max_w` above the inverter's own
|
||||
rating, SAFETY-20's install — stops being reported by anything.
|
||||
test_clamp.py gains part 6, and it grades the CONSEQUENCE rather than the
|
||||
position: a spy subclassed from the shipped `ClampWatchdog` records what
|
||||
`self.clamp_last` held each time the guard consulted it, over a slew-limited
|
||||
ramp where every cycle's command is a different number, and asserts the
|
||||
evidence is always the write of THAT cycle. Hooking `clamp_wd.note()` rather
|
||||
than the guard's name is what keeps a rename legitimate; the same assertions
|
||||
are re-run through a controller whose guard is renamed and whose write lives in
|
||||
a helper, and were verified against `main.py` itself — renaming the method
|
||||
throughout and moving both calls into a new `_emit_and_guard()` helper keeps
|
||||
part 6 green, while swapping the two calls turns it red on a line that names
|
||||
`clamp_last`. AC 3 needed a stimulus of its own: through the real `cycle()` the
|
||||
write-path half can never engage, because the arbiter files the device range
|
||||
and `max_w` as P_SAFETY LIMIT claims and `resolution.target_w` is already
|
||||
inside both bounds before `write_setpoint()` re-applies them — that duplication
|
||||
is deliberate defence in depth, and part 6 now measures the 0 engagements
|
||||
rather than leaving it as a claim in a comment. So the write path is driven
|
||||
directly at `max_w` 5000 against a ±2500 W device: the wire carries 2500 W,
|
||||
every loop-side flag stays clean, and the alarm fires on `clamp_last` alone —
|
||||
with the two calls swapped, the same stimulus clamps the same value and the
|
||||
alarm is silent. `main.py` gains one comment sentence pointing at the check,
|
||||
mirroring the one SAFETY-18's block already carries for test_freeze.py part 5.
|
||||
|
||||
**`max_w` and the `LIVE_MAX_W` literal beside it were unpinned in test_freeze.py
|
||||
and test_tariff.py, the same gap TEST-06 closed one key over for
|
||||
`meter_freeze_s` (TEST-10).** Measured before the fix: `max_w: 2000 -> 4500`
|
||||
|
||||
Reference in New Issue
Block a user