From cab267c4f3485a3ba184693b4e3411963bc9c22f Mon Sep 17 00:00:00 2001 From: glenn schrooyen Date: Sun, 6 Sep 2026 23:26:22 +0200 Subject: [PATCH] OPT-09: the 6500 W window is a bound the ratings never let the site reach MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Round-2 non-blocking findings, both fixed. ⚠️ THE WINDOW FIGURE WAS ARITHMETIC NOBODY HAD RUN. `min_w * (w_A + w_B) / w_B` = 6500 W is correct about `_fill()`'s PROPORTIONAL regime and is not a reachable steady state: the healthy pack's own 5000 W rating binds first. SWEPT on part 8b's own fixture, reproduced rather than transcribed: load equal OPT-09 site target A B 1000 199/200 0/200 1080 997 83 2000 199/200 0/200 2170 2003 167 3000 199/200 0/200 3250 3000 250 4000 199/200 4/200 4340 4006 334 4500 199/200 8/200 4890 4514 376 4900 199/200 41/200 5320 4911 409 <- largest suppressed 5000 199/200 195/200 8000 5000 3000 <- A at its rating 6000 199/200 195/200 8000 5000 3000 So the alarm returns BELOW the formula bound, not above it, and the largest site target reached with the wedged unit still under the 500 W threshold is 5320 W - not 6500. The equal-weights "suppressed below 1000 W" bound is unreachable for the same reason: the equal arm names the wedged unit 199/200 at EVERY load swept, 1000 W included. The formula and its check are kept, because the mechanism they state is right and it is the mechanism that generalises to other ratings. What was wrong was the sentence around them, in four places, implying the site passes through the bound. Each now carries the sweep and says the rating binds first. ⚠️ THIS NUMBER CAME FROM THE REVIEW, NOT FROM THE BRANCH, and was adopted in good faith by the implementer. The reviewer found it in round 2 and said so. That is worth recording: a figure arriving from a trusted reviewer got less scrutiny than one written by the author, and PROC-01's eight fabricated numbers in two days is now nine. The rule that catches it is the same either way - run it before you ship it. Also: the DECISIONS header's base sha, stale for the third time (464d398 -> 3368231 -> 3d29993, moved by two rebases and caught by review each time). It now records that history, and the header is why `test_translations.py`'s counts are computed rather than typed. SAFETY-43 is now named by id where the follow-up is referenced, so a reader can follow the thread from the disclosure to its closure. Suite: 25 files, all pass. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU --- goodwe_controller/CHANGELOG.md | 6 +++++- .../DECISIONS/OPT-09-soc-aware-weights.md | 10 +++++++--- goodwe_controller/app/packshare.py | 2 ++ goodwe_controller/test_packshare.py | 10 ++++++++-- 4 files changed, 22 insertions(+), 6 deletions(-) diff --git a/goodwe_controller/CHANGELOG.md b/goodwe_controller/CHANGELOG.md index 4470cbe..e22c7d9 100644 --- a/goodwe_controller/CHANGELOG.md +++ b/goodwe_controller/CHANGELOG.md @@ -289,7 +289,11 @@ own rating, the allocator's last-resort pass hands the wedged unit a big share again and the alarm returns. So in the quiet regime - where `unmet_w` is 0, the meter is satisfied and no action-log line fires - **a wedged inverter with less energy headroom than its sibling can now go unreported at ordinary house -loads.** `test_packshare.py` part 8b grades it and pins the CURRENT behaviour, so +loads.** + +⚠️ BOTH BOUNDS ARE ARITHMETIC ABOUT `_fill()`'s PROPORTIONAL REGIME AND NEITHER IS A REACHABLE STEADY STATE. Swept on this fixture, the healthy pack's own 5000 W rating binds FIRST: suppressed 0/200 up to a 3000 W load, 4/200 at 4000 W, 41/200 at 4900 W (site target 5320 W, the largest reached with the wedged unit still under 500 W), then 195/200 at 5000 W once `_fill()` pins A at its cap and hands B 3000 W. So the alarm returns BELOW the 6500 W formula bound, not above it. The equal-weights 1000 W bound is unreachable for the same reason - the equal arm names the wedged unit 199/200 at every load swept, 1000 W included. + +`test_packshare.py` part 8b grades it and pins the CURRENT behaviour, so the day it changes a named check moves. Closing it means making `delinquent()` relative to the ask rather than absolute, which is SAFETY-42's code and a design change; that is filed as its own card and is deliberately not done here. diff --git a/goodwe_controller/DECISIONS/OPT-09-soc-aware-weights.md b/goodwe_controller/DECISIONS/OPT-09-soc-aware-weights.md index 5732102..16f6233 100644 --- a/goodwe_controller/DECISIONS/OPT-09-soc-aware-weights.md +++ b/goodwe_controller/DECISIONS/OPT-09-soc-aware-weights.md @@ -1,6 +1,6 @@ # OPT-09 - the weights parameter, and what it is honestly for -**Status:** implemented. **Branch:** `OPT-09`. **Base:** `release/1.0` @ `3368231` +**Status:** implemented. **Branch:** `OPT-09`. **Base:** `release/1.0` @ `3d29993` (rebased onto the UX-02 merge; it read `3368231`, and before that `464d398` - each rebase moved it and review caught it each time, which is why the counts in `test_translations.py` are now computed rather than typed) (`git merge-base HEAD 3368231`; the branch was rebased and this line used to name the pre-rebase base, `464d398`). @@ -198,8 +198,12 @@ named**. The suppression window widens from `site target < 2 * min_w` = 1000 W t `site target < min_w * (w_A + w_B) / w_B`, which on that fixture is **6500 W** of a 9600 W site; above it - once the healthy pack is at its own rating and `_fill()`'s last-resort pass hands the wedged unit a big share again - the alarm -returns. `test_packshare.py` part 8b grades all of it and **pins the current, -unflattering behaviour**, so that the day it changes a named check moves. +returns. ⚠️ BOTH BOUNDS ARE ARITHMETIC ABOUT `_fill()`'s PROPORTIONAL REGIME AND NEITHER IS A REACHABLE STEADY STATE. Swept on this fixture, the healthy pack's own 5000 W rating binds FIRST: suppressed 0/200 up to a 3000 W load, 4/200 at 4000 W, 41/200 at 4900 W (site target 5320 W, the largest reached with the wedged unit still under 500 W), then 195/200 at 5000 W once `_fill()` pins A at its cap and hands B 3000 W. So the alarm returns BELOW the 6500 W formula bound, not above it. The equal-weights 1000 W bound is unreachable for the same reason - the equal arm names the wedged unit 199/200 at every load swept, 1000 W included. + +`test_packshare.py` part 8b grades all of it and **pins the current, +unflattering behaviour**, so that the day it changes a named check moves. The +closure - `delinquent()` measured against the ask rather than a flat 500 W - is +card **SAFETY-43**. So the seam, stated accurately: **OPT-09 stops the ask AND narrows SAFETY-42's detection window for a low-headroom unit.** The two cards do not merely meet from diff --git a/goodwe_controller/app/packshare.py b/goodwe_controller/app/packshare.py index d222c45..a353d96 100644 --- a/goodwe_controller/app/packshare.py +++ b/goodwe_controller/app/packshare.py @@ -80,6 +80,8 @@ suppression window widens from `site target < 2 * min_w` = 1000 W to an upper edge: past the healthy pack's own rating, `_fill()`'s last-resort pass hands the wedged unit a big share again and the alarm returns. + ⚠️ BOTH BOUNDS ARE ARITHMETIC ABOUT `_fill()`'s PROPORTIONAL REGIME AND NEITHER IS A REACHABLE STEADY STATE. Swept on this fixture, the healthy pack's own 5000 W rating binds FIRST: suppressed 0/200 up to a 3000 W load, 4/200 at 4000 W, 41/200 at 4900 W (site target 5320 W, the largest reached with the wedged unit still under 500 W), then 195/200 at 5000 W once `_fill()` pins A at its cap and hands B 3000 W. So the alarm returns BELOW the 6500 W formula bound, not above it. The equal-weights 1000 W bound is unreachable for the same reason - the equal arm names the wedged unit 199/200 at every load swept, 1000 W included. + ⚠️ SO THE SEAM IS NOT "OPT-09 CANNOT SEE IT". IT IS: OPT-09 STOPS THE ASK **AND** NARROWS SAFETY-42's DETECTION WINDOW FOR A LOW-HEADROOM UNIT. That is a disclosed residual, not a fixed thing. Closing it means making diff --git a/goodwe_controller/test_packshare.py b/goodwe_controller/test_packshare.py index 97445e3..f1b555e 100644 --- a/goodwe_controller/test_packshare.py +++ b/goodwe_controller/test_packshare.py @@ -629,9 +629,11 @@ print("8b. ⚠️ AND WHAT THE WEIGHTS DO TO SAFETY-42's DETECTION WINDOW, WHICH # window is `site target < 2 * min_w` = 1000 W; at OPT-09's weights it is # `site target < min_w * (w_A + w_B) / w_B`, which on this fixture is 6500 W. # +# ⚠️ BOTH BOUNDS ARE ARITHMETIC ABOUT `_fill()`'s PROPORTIONAL REGIME AND NEITHER IS A REACHABLE STEADY STATE. Swept on this fixture, the healthy pack's own 5000 W rating binds FIRST: suppressed 0/200 up to a 3000 W load, 4/200 at 4000 W, 41/200 at 4900 W (site target 5320 W, the largest reached with the wedged unit still under 500 W), then 195/200 at 5000 W once `_fill()` pins A at its cap and hands B 3000 W. So the alarm returns BELOW the 6500 W formula bound, not above it. The equal-weights 1000 W bound is unreachable for the same reason - the equal arm names the wedged unit 199/200 at every load swept, 1000 W included. +# # ⚠️ THIS IS A DETECTION REGRESSION AT N=2 AND IT IS NOT FIXED HERE. Making # `delinquent()` relative to the ask rather than absolute is SAFETY-42's code and -# a design change; it is filed as its own card. What this part does is GRADE THE +# a design change; it is filed as card SAFETY-43. What this part does is GRADE THE # CURRENT BEHAVIOUR so that the day it changes, a named check moves. # # ⚠️ AND THE FIXTURE IS A SAFETY-42 TRUE POSITIVE BY THAT CARD'S OWN DEFINITION, @@ -747,7 +749,11 @@ check("...and the arithmetic behind it, stated as a window rather than as one " "fixture: `delinquent()` reads an ABSOLUTE gap, so a unit weighted to a " "fraction f of the site target is invisible while `f * target < min_w`. " "Equal weights suppress below 1000 W; these weights suppress below " - "6500 W of a 9600 W site. That is the regression, and it is a property " + "6500 W of a 9600 W site. ⚠️ BOTH ARE PROPORTIONAL-REGIME BOUNDS AND " + "NEITHER IS REACHABLE: the healthy pack's own rating binds first, so the " + "alarm returns BELOW 6500 W (swept: 0/200 to a 3000 W load, 41/200 at " + "4900 W, 195/200 at 5000 W) and the equal arm never suppresses at all. " + "That is the regression, and it is a property " "of the threshold being absolute rather than of any number chosen here", abs(_window_equal - 1000.0) < 1.0 and abs(_window_opt09 - 6500.0) < 1.0)