SAFETY-41: correct the comment, and fix the check it cites

Review found two false statements in the comment and one weak check. All
three are fixed here; the constant itself is unchanged at 150 s and was
never in question.

1. "That caps this constant at 900/5 = 180 s" was FALSE ABOUT THE CHECK IT
   CITED. test_clamp.py part 2 sweeps i = 0..899 and the rising edge at
   i=0 is free, so it measured `1 + floor(899/R)` - one alarm more than a
   real quarter earns. Measured: 224 s was GREEN across the whole suite.
   Anyone raising this to 200 s would have had a green board and a
   requirement quietly unmet - the same defect recorded at
   test_clamp.py:216, one notch weaker.

   The check now grades floor(QUARTER_S / REASSERT_S) directly, so it
   enforces the ceiling the comment claims for it. Verified: 150 PASS,
   180 PASS, 181 FAIL, 224 FAIL. 180 is also the true requirement edge -
   on the worst sliding 900 s window 180 earns 5 alarms and 181 earns 4.

2. The "60 cycles of the §10.3 runaway" note was attributed to this module
   and truncated. It is main.py:2479 and reads "60 cycles of the §10.3
   runaway produce 0 write-path engagements and 2 loop-side ones". Quoted
   in full, attributed correctly.

3. "48 engagements in 25 minutes" was left implying a pinned rail. It is
   roughly one cycle in six - real, and far above the 2-in-60 the guard
   was sized for, but not continuous. Restated, and labelled as a live-log
   reading rather than a measurement in this repo.

Also recorded: the 1500 W tuning derivation is in the OTHER repo
(admin/goodwe NOTES.md, "Simulated 2 kW load step (gain 0.6, clamp
1500)"), so this repo cannot corroborate it and a reader should not go
looking here; and MIN_PER_QUARTER = 5 is a test-local integer, not a spec
figure - the 900 s quarter is system-wide but the 5 is not, so the 180 s
ceiling rests on one number a test author chose.

Full suite: 20 passed, 0 failed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU
This commit is contained in:
glenn schrooyen
2026-09-05 16:53:46 +02:00
co-authored by Claude Opus 5
parent 3516947b5e
commit 1d393ced22
2 changed files with 59 additions and 19 deletions
+44 -19
View File
@@ -149,27 +149,52 @@ REASON_CODE = "OUTPUT_CLAMP"
#
# ⚠️ RAISED 30 s -> 150 s on 2026-09-05, and the reason is a REGIME CHANGE, not a
# change of mind about the alarm. 30 s was the planning cadence (2.3), chosen
# when a loop-side engagement was a rare event: this module's own note records
# "60 cycles of the §10.3 runaway produce 2 loop-side engagements". On the live
# install it produced 48 IN 25 MINUTES of ordinary operation, because `max_w` is
# 5000 W where the loop's tuning was derived at a 1500 W clamp (SAFETY-39) - so
# the integrator reaches its bound constantly and the notification re-asserted
# every 30 s, all day. An alarm that speaks that often about the normal case is
# how an operator learns to ignore the one that matters, which is the exact
# failure this module's docstring is written against.
# when a loop-side engagement was a rare event: `main.py`'s note at the action
# log records "60 cycles of the §10.3 runaway produce 0 write-path engagements
# and 2 loop-side ones". On the live install the counter reached 48 loop-side
# ENGAGEMENTS in ~25 minutes of ordinary operation - roughly one cycle in six,
# not a pinned rail, but far more often than a guard sized for 2-in-60 assumed -
# and at a 30 s re-assert the owner received a persistent notification about it
# all day.
#
# ⚠️ 150 s IS NOT A COMFORT CHOICE, IT IS THE REQUIREMENT'S OWN EDGE, WITH ONE
# STEP OF MARGIN. test_clamp.py part 2 pins the cadence in WALL-CLOCK terms
# rather than against this constant - a loop pinned on the rail must speak at
# least MIN_PER_QUARTER = 5 times inside the 900 s quarter that bills the
# capacity tariff. That caps this constant at 900/5 = 180 s. At 180 s the check
# passes with exactly 5 of a required 5, so any later change to the firing logic
# tips it red; 150 s gives 6. The floor is the thing that must not move.
# ⚠️ THE FIGURE ABOVE IS AN ENGAGEMENT COUNT READ OFF THE LIVE LOG, NOT A
# MEASUREMENT IN THIS REPO, and it is not the alarm count. Do not compare it
# with the 2-in-60 above without noting that one is engagements and the other is
# engagements too, but over a runaway rather than ordinary running.
#
# ⚠️ THIS IS A STOP-GAP AND SHOULD BE REVISITED. It reduces how often the alarm
# repeats; it does not make the alarm correct. SAFETY-40 is the wording and the
# i-clamped/clamped split, and SAFETY-39 is the tuning that would stop the loop
# railing in the first place. Neither is done by this line.
# WHY the loop reaches its integrator bound so much more often than the guard's
# author saw: `max_w` on the live install is 5000 W, while the loop's slew was
# derived against a 1500 W clamp - admin/goodwe `NOTES.md` §"Simulated 2 kW load
# step (gain 0.6, clamp 1500)". ⚠️ THAT DERIVATION IS IN THE OTHER REPO; nothing
# in this one corroborates it, so do not go looking here. SAFETY-39 is the ticket
# that re-derives it, and fixing that is what would stop the railing; this
# constant only changes how often the alarm repeats.
#
# ⚠️ 150 s IS THE REQUIREMENT'S EDGE WITH MARGIN, AND THE CHECK NOW ENFORCES THAT
# EDGE. `test_clamp.py` part 2 grades the cadence in wall-clock terms - a pinned
# loop must speak at least MIN_PER_QUARTER = 5 times inside the 900 s quarter
# that bills the capacity tariff - which caps this constant at 900/5 = 180 s.
# Measured on the worst sliding 900 s window: 180 s earns 5 alarms, 181 s earns
# 4. 150 s earns 6.
#
# ⚠️ THAT CHECK USED TO PERMIT 224 s, AND THE FIX SHIPPED WITH THIS COMMIT. Its
# sweep starts at i=0, where the rising edge is free, so it measured one alarm
# more than a real quarter earns. A later editor raising this to 200 s would have
# had a green suite and a requirement quietly unmet. `test_clamp.py` now grades
# floor(QUARTER_S / REASSERT_S) directly.
#
# ⚠️ MIN_PER_QUARTER = 5 IS A TEST-LOCAL INTEGER, not a spec figure - it exists
# only in `test_clamp.py` and the CHANGELOG entry that introduced it. The 900 s
# quarter IS system-wide (`tariff.py`, `freeze.py`, `DOCS.md`). So the 180 s
# ceiling rests on one number a test author chose; if that number is ever
# revisited, this constant moves with it.
#
# ⚠️ THIS IS A STOP-GAP AND DOES NOT MAKE THE ALARM CORRECT. The message still
# calls the integrator bound a BUG SIGNAL and still pairs a request from one
# moment with a `self.target` read at another - SAFETY-40. Nothing here is
# suppressed: every engagement is still counted, the action-log row is written on
# every engaged cycle regardless, and the rising edge of every episode alarms
# unconditionally, so no alarm is DROPPED - only delayed.
REASSERT_S = 150.0
# Consecutive clean cycles before the alarm de-asserts (10.2 hysteresis).
+15
View File
@@ -239,6 +239,21 @@ print(f" pinned on the rail for {QUARTER_S:.0f} s at 1 s cycles: "
check(f"a loop pinned for a whole billing quarter speaks at least "
f"{MIN_PER_QUARTER} times (it spoke {alarms})",
alarms >= MIN_PER_QUARTER)
# ⚠️ REVIEW-FOUND, SAFETY-41, and it is the SAME DEFECT as the one recorded
# twenty lines above, one notch weaker. The sweep above runs i = 0..899 and the
# rising edge at i=0 is FREE, so it measures `1 + floor(899 / REASSERT_S)` - one
# alarm more than a real quarter earns. Measured on this very check:
# REASSERT_S = 180 -> 5 alarms, green (and 180 is the true edge: on the worst
# REASSERT_S = 224 -> 5 alarms, GREEN sliding 900 s window 180 gives 5 and
# REASSERT_S = 225 -> 4 alarms, red 181 gives 4)
# So the check as written permits 224 s while the REQUIREMENT permits 180 s, and
# anyone raising the constant to 200 s would get a green suite and conclude the
# floor was met when a real quarter would earn 4 alarms. Grade the cadence
# DIRECTLY, so the check enforces the ceiling its own comment claims for it.
check(f"...and the CADENCE itself honours the floor, independent of where the "
f"sweep happens to start: floor({QUARTER_S:.0f}/{REASSERT_S:.0f}) = "
f"{int(QUARTER_S // REASSERT_S)} >= {MIN_PER_QUARTER}",
QUARTER_S // REASSERT_S >= MIN_PER_QUARTER)
check(f"...and on no more than {MAX_FRACTION:.0%} of cycles (it was "
f"{alarms / QUARTER_S:.1%})", alarms <= MAX_FRACTION * QUARTER_S)
check("...while every single cycle is still counted",