SAFETY-25: record the owner's decision - option a, with alloc-01 as the revisit trigger
checks / unit (pull_request) Successful in 3m43s
checks / unit (pull_request) Successful in 3m43s
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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
1c59cfeaf5
commit
ab6fbe2f9e
@@ -2,6 +2,64 @@
|
||||
|
||||
## Unreleased
|
||||
|
||||
**SAFETY-25's open question is ANSWERED: the BMS cut-out is the DELIBERATE and
|
||||
only backstop for demand-driven discharge, and a loop-side reserve floor is
|
||||
DEFERRED to `ALLOC-01` rather than rejected (SAFETY-25).** Decided by the
|
||||
product owner on 2026-09-02. **Option A is the shipped behaviour, so `app/`
|
||||
changed only where a comment claimed the question was still open** - no
|
||||
setpoint, no claim, no option, no default moved, and the 17-suite run is green
|
||||
on the same numbers as before.
|
||||
|
||||
* **Why deferring is the safe direction, and it is a property of the shape
|
||||
rather than a preference.** Option B's proof - `bms_soc_floor <
|
||||
loop_soc_floor <= peak_soc_floor`, verified 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` while 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** - the failure the memo itself marks
|
||||
fatal. **No config-ordering validator can catch it**, because it is a runtime
|
||||
divergence between two readings and not a config ordering, so CFG-01's shape
|
||||
of check would pass 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 recorded explicitly, and it is `ALLOC-01`.** 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** - which is 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, and the memo says so at the point a future reader will look.
|
||||
* **What will have to be RE-EARNED at N=2 is stated plainly 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
|
||||
`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, 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.** A memo that recorded the
|
||||
verdict and deleted the reasoning is the failure mode this `DECISIONS/`
|
||||
directory exists to prevent, so §1-§7 stand exactly as written - including
|
||||
§5's *"(recommended)"* on Option B and §6's recommendation - with the new §0
|
||||
saying what was decided and §6 marked as answered. 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 stays, and "deferred" is why it stays.** Part 10 and mutation
|
||||
13 are unchanged in substance: a ninth claim source in `Controller.cycle()`
|
||||
still takes 10f red on the line it is written. Deferred, not rejected, is
|
||||
precisely the state in which someone builds Option B by accident.
|
||||
|
||||
Operator-facing surfaces follow the decision and nothing else changed on them:
|
||||
`DOCS.md`'s SAFETY-25 section now says a choice was made and points at the
|
||||
trigger, and `app/freeze.py`'s finding and `app/peakguard.py`'s point 2 say
|
||||
"decided, on purpose" where they said "open".
|
||||
|
||||
**`PeakStore.record_block()` now has its POLICY ARGUMENT graded, and the
|
||||
SAFETY-20 import check now looks at all three ways of writing an import
|
||||
(TEST-13).** Two survivors from TARIFF-01's review, both closed in
|
||||
|
||||
Reference in New Issue
Block a user