The product owner answered SAFETY-25's §1 on 2026-09-02: OPTION A. The BMS cut-out is the deliberate and only backstop for demand-driven discharge on the grid-following loop, and Option B — a loop-side reserve floor — is DEFERRED, not rejected, with an explicit revisit trigger.
Option A is the shipped behaviour, so this is a recording and not a change.app/ is touched in exactly two places and both are comments that claimed the question was still open (freeze.py's finding, peakguard.py's point 2). No setpoint, no claim, no option, no default moved. All 17 suites green on the same numbers as before.
Why deferring is the safe direction
Option B's proof — bms_soc_floor < loop_soc_floor <= peak_soc_floor, run through the real arbiter in test_peakguard.py part 10d — has an unstated premise: the reserve predicate and discharge_permitted() read the SAME state of charge. At N=1 they do, which is why the proof reads as airtight.
With two packs they read different numbers. The reserve binds on the emptier pack and files hi = 0; the peak guard, reading the other pack or an average, is not floor-limited and files lo = excess > 0. lo > hi is arbiter clause 3 and the answer is 0 W — the battery parked in the billed quarter, which is the failure the memo itself marks fatal. It arrives by a route the ordering rule cannot close, because loop_soc_floor <= peak_soc_floor is still perfectly satisfied while it happens.
And no config-ordering validator can catch it. It is a runtime divergence between two readings, not a config ordering, so CFG-01's shape of check would pass every time on the exact tree that parks the battery.
Option A's cost is energy. Option B's cost, built too early, is a parked battery in the one quarter of the month that sets the bill.
The revisit trigger is ALLOC-01 (§0c)
Under ARCH-01's host-side allocator the two claims stop sharing a claim list:
today (N=1)
under ALLOC-01
the loop reserve
a site hi at P_LOOP
a per-inverter hi, consumed as that unit's interval at stage A
the peak floor
a site lo at P_PEAK
still a site lo, at stage B
Different claim lists, different stages, structurally unable to cross. That is a stronger argument than the ordering rule: §5 makes the crossing harmless, ALLOC-01 makes it unreachable. That is the shape in which Option B becomes safe to build.
What has to be re-earned at N=2 (§0d)
Stated plainly in the memo rather than left for someone to discover:
SAFETY-27 now names five floors across two packs. "The SoC" stops being a scalar the config can bound — the same divergence that makes the contradiction above possible.
test_peakguard.py part 10f — the pin that made SAFETY-25 mergeable — CANNOT EXPRESS TWO PACKS. Its dynamic half is a scalar 0–100 % SoC sweep against a single-series capture; a two-pack exposure lives in the difference between two readings, which that sweep visits at no resolution. Its static half (the AST scan pinning the eight claim sources) still bites at N=2; the dynamic half goes silent. Merging a two-pack reserve on 10f's current green would be merging on a check that cannot see the failure.
What is deliberately NOT changed
The options analysis stands exactly as written — §1–§7, including §5's "(recommended)" on Option B and §6's recommendation. A memo that recorded the verdict and deleted the reasoning is the failure this DECISIONS/ directory exists to prevent. The new §0 says what was decided; §6 is marked as answered.
The tripwire stays. A ninth claim source in Controller.cycle() still takes 10f red on the line it is written. Deferred is precisely the state in which someone builds Option B by accident.
Part 10e moved with the memo, not away from it. It used to pin the honest Status: OPEN line; it now pins that the memo names the option, the date and the trigger and still carries both options and the recommendation — plus that the memo states 10f's two-pack limit.
The product owner answered SAFETY-25's §1 on **2026-09-02: OPTION A.** The BMS cut-out is the **deliberate** and only backstop for demand-driven discharge on the grid-following loop, and Option B — a loop-side reserve floor — is **DEFERRED, not rejected**, with an explicit revisit trigger.
**Option A is the shipped behaviour, so this is a recording and not a change.** `app/` is touched in exactly two places and both are comments that claimed the question was still open (`freeze.py`'s finding, `peakguard.py`'s point 2). No setpoint, no claim, no option, no default moved. All 17 suites green on the same numbers as before.
## Why deferring is the safe direction
Option B's proof — `bms_soc_floor < loop_soc_floor <= peak_soc_floor`, run through the real arbiter in `test_peakguard.py` part 10d — has an unstated premise: **the reserve predicate and `discharge_permitted()` read the SAME state of charge.** At N=1 they do, which is why the proof reads as airtight.
With two packs they read different numbers. The reserve binds on the emptier pack and files `hi = 0`; the peak guard, reading the other pack or an average, is not floor-limited and files `lo = excess > 0`. `lo > hi` is arbiter clause 3 and the answer is **0 W — the battery parked in the billed quarter**, which is the failure the memo itself marks fatal. It arrives by a route the ordering rule cannot close, because `loop_soc_floor <= peak_soc_floor` is still perfectly satisfied while it happens.
**And no config-ordering validator can catch it.** It is a runtime divergence between two readings, not a config ordering, so CFG-01's shape of check would pass every time on the exact tree that parks the battery.
Option A's cost is energy. Option B's cost, built too early, is a parked battery in the one quarter of the month that sets the bill.
## The revisit trigger is ALLOC-01 (§0c)
Under ARCH-01's host-side allocator the two claims stop sharing a claim list:
| | today (N=1) | under ALLOC-01 |
|---|---|---|
| the loop reserve | a site `hi` at `P_LOOP` | a **per-inverter `hi`**, consumed as that unit's **interval at stage A** |
| the peak floor | a site `lo` at `P_PEAK` | still a **site `lo`**, at **stage B** |
Different claim lists, different stages, **structurally unable to cross.** That is a stronger argument than the ordering rule: §5 makes the crossing harmless, ALLOC-01 makes it unreachable. **That is the shape in which Option B becomes safe to build.**
## What has to be re-earned at N=2 (§0d)
Stated plainly in the memo rather than left for someone to discover:
1. **SAFETY-27 now names five floors across two packs.** "The SoC" stops being a scalar the config can bound — the same divergence that makes the contradiction above possible.
2. **`test_peakguard.py` part 10f — the pin that made SAFETY-25 mergeable — CANNOT EXPRESS TWO PACKS.** Its dynamic half is a **scalar 0–100 % SoC sweep against a single-series capture**; a two-pack exposure lives in the *difference* between two readings, which that sweep visits at no resolution. Its **static** half (the AST scan pinning the eight claim sources) still bites at N=2; the dynamic half goes silent. **Merging a two-pack reserve on 10f's current green would be merging on a check that cannot see the failure.**
## What is deliberately NOT changed
* **The options analysis stands exactly as written** — §1–§7, including §5's *"(recommended)"* on Option B and §6's recommendation. A memo that recorded the verdict and deleted the reasoning is the failure this `DECISIONS/` directory exists to prevent. The new §0 says what was decided; §6 is marked as answered.
* **The tripwire stays.** A ninth claim source in `Controller.cycle()` still takes 10f red on the line it is written. *Deferred* is precisely the state in which someone builds Option B by accident.
* **Part 10e moved with the memo, not away from it.** It used to pin the honest `Status: OPEN` line; it now pins that the memo names the option, the date and the trigger **and still carries both options and the recommendation** — plus that the memo states 10f's two-pack limit.
## Verification
`docker run --rm -v /home/homelab/work-SAFETY-25:/w:ro goodwe-addon-ci ...` — **17/17 suites pass.**
Card 6a94b2b98b73123d520e69f2.
The product owner answered SAFETY-25's §1 on 2026-09-02: OPTION A. The BMS
cut-out is the DELIBERATE and only backstop for demand-driven discharge on the
grid-following loop, and Option B - a loop-side reserve floor - is DEFERRED
rather than rejected.
Option A is the shipped behaviour, so this is a recording, not a change. `app/`
is touched in exactly two places and both are comments that claimed the question
was still open (freeze.py's finding, peakguard.py's point 2). No setpoint, no
claim, no option, no default moved. The 17-suite run is green on the same
numbers as before.
WHY DEFERRING IS THE SAFE DIRECTION. Option B's proof - bms_soc_floor <
loop_soc_floor <= peak_soc_floor, run through the real arbiter in
test_peakguard.py part 10d - has an unstated premise: the reserve predicate and
discharge_permitted() read the SAME state of charge. With two packs they read
different numbers. The reserve binds on the emptier pack and files hi = 0; the
peak guard, reading the other pack or an average, files lo = excess > 0; lo > hi
is arbiter clause 3 and the answer is 0 W - the battery parked in the billed
quarter, which is the failure the memo itself marks fatal. And no config
validator can catch it: it is a runtime divergence between two readings, not a
config ordering, so CFG-01's shape of check passes on the exact tree that parks
the battery. Option A's cost is energy; Option B's cost built too early is the
one quarter of the month that sets the bill.
THE REVISIT TRIGGER IS ALLOC-01, recorded in §0c. Under ARCH-01's host-side
allocator the two claims stop sharing a list: the loop floor becomes a
per-inverter `hi` consumed as an INTERVAL at stage A, while the peak floor stays
a site `lo` at stage B. Different claim lists, different stages, structurally
unable to cross - a stronger argument than the ordering rule, because it makes
the crossing unreachable rather than harmless. That is the shape in which Option
B becomes safe to build.
WHAT HAS TO BE RE-EARNED AT N=2 is stated plainly in §0d rather than left for
someone to discover. SAFETY-27 now names five floors across two packs, so "the
SoC" stops being a scalar the config can bound. And part 10f - the pin that made
SAFETY-25 mergeable - CANNOT EXPRESS TWO PACKS: its dynamic half is a scalar
0-100 % SoC sweep against a single-series capture, and a two-pack exposure lives
in the DIFFERENCE between two readings, which that sweep visits at no
resolution. Its static half, the AST scan pinning the eight claim sources, still
bites at N=2; the dynamic half goes silent. Merging a two-pack reserve on 10f's
current green would be merging on a check that cannot see the failure.
The options analysis is kept intact on purpose - §1-§7 stand exactly as written,
including §5's "(recommended)" on Option B and §6's recommendation, with a new
§0 saying what was decided and §6 marked as answered. A memo that recorded the
verdict and deleted the reasoning is the failure this DECISIONS/ directory
exists to prevent.
Part 10e's check moved with the memo: it used to pin the honest "Status: OPEN"
line and now pins that the memo names the option, the date and the trigger AND
still carries both options and the recommendation. The tripwire itself stays -
a ninth claim source in Controller.cycle() still takes 10f red on the line it is
written, and "deferred" is precisely the state in which someone builds Option B
by accident.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
admin
merged commit 0c7e786ed5 into release/1.02026-09-02 00:58:15 +02:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
The product owner answered SAFETY-25's §1 on 2026-09-02: OPTION A. The BMS cut-out is the deliberate and only backstop for demand-driven discharge on the grid-following loop, and Option B — a loop-side reserve floor — is DEFERRED, not rejected, with an explicit revisit trigger.
Option A is the shipped behaviour, so this is a recording and not a change.
app/is touched in exactly two places and both are comments that claimed the question was still open (freeze.py's finding,peakguard.py's point 2). No setpoint, no claim, no option, no default moved. All 17 suites green on the same numbers as before.Why deferring is the safe direction
Option B's proof —
bms_soc_floor < loop_soc_floor <= peak_soc_floor, run through the real arbiter intest_peakguard.pypart 10d — has an unstated premise: the reserve predicate anddischarge_permitted()read the SAME state of charge. At N=1 they do, which is why the proof reads as airtight.With two packs they read different numbers. The reserve binds on the emptier pack and files
hi = 0; the peak guard, reading the other pack or an average, is not floor-limited and fileslo = excess > 0.lo > hiis arbiter clause 3 and the answer is 0 W — the battery parked in the billed quarter, which is the failure the memo itself marks fatal. It arrives by a route the ordering rule cannot close, becauseloop_soc_floor <= peak_soc_flooris still perfectly satisfied while it happens.And no config-ordering validator can catch it. It is a runtime divergence between two readings, not a config ordering, so CFG-01's shape of check would pass every time on the exact tree that parks the battery.
Option A's cost is energy. Option B's cost, built too early, is a parked battery in the one quarter of the month that sets the bill.
The revisit trigger is ALLOC-01 (§0c)
Under ARCH-01's host-side allocator the two claims stop sharing a claim list:
hiatP_LOOPhi, consumed as that unit's interval at stage AloatP_PEAKlo, at stage BDifferent claim lists, different stages, structurally unable to cross. That is a stronger argument than the ordering rule: §5 makes the crossing harmless, ALLOC-01 makes it unreachable. That is the shape in which Option B becomes safe to build.
What has to be re-earned at N=2 (§0d)
Stated plainly in the memo rather than left for someone to discover:
test_peakguard.pypart 10f — the pin that made SAFETY-25 mergeable — CANNOT EXPRESS TWO PACKS. Its dynamic half is a scalar 0–100 % SoC sweep against a single-series capture; a two-pack exposure lives in the difference between two readings, which that sweep visits at no resolution. Its static half (the AST scan pinning the eight claim sources) still bites at N=2; the dynamic half goes silent. Merging a two-pack reserve on 10f's current green would be merging on a check that cannot see the failure.What is deliberately NOT changed
DECISIONS/directory exists to prevent. The new §0 says what was decided; §6 is marked as answered.Controller.cycle()still takes 10f red on the line it is written. Deferred is precisely the state in which someone builds Option B by accident.Status: OPENline; it now pins that the memo names the option, the date and the trigger and still carries both options and the recommendation — plus that the memo states 10f's two-pack limit.Verification
docker run --rm -v /home/homelab/work-SAFETY-25:/w:ro goodwe-addon-ci ...— 17/17 suites pass.Card 6a94b2b98b73123d520e69f2.