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
@@ -1877,7 +1877,8 @@ print("10. SAFETY-25: the loop discharges past this floor, and how far past")
|
||||
# point. The exposure must not be able to change under a green suite; it has
|
||||
# to be closed deliberately, and closing it means answering AC 2 and
|
||||
# deleting or inverting these checks along with
|
||||
# DECISIONS/SAFETY-25-loop-soc-floor.md's open status.
|
||||
# DECISIONS/SAFETY-25-loop-soc-floor.md's status - OPEN when this was
|
||||
# written, DECIDED (Option A, deferred B) since 2026-09-02.
|
||||
#
|
||||
# ⚠️ AND IT IS 10f THAT MAKES THAT SENTENCE TRUE, NOT 10a-10c. The replay
|
||||
# below hand-builds main.py's claim list, so on its own it is blind to a
|
||||
@@ -2131,8 +2132,11 @@ check("THE ASYMMETRY, ON REAL DATA. On every cycle where the guard reports "
|
||||
and HOT_R["max_granted"] > 0.0)
|
||||
check("...which is why SAFETY-24's floor could not be widened into this one, "
|
||||
"and why the fix is not a bigger number. Recorded in "
|
||||
"DECISIONS/SAFETY-25-loop-soc-floor.md; AC 2 is the product owner's and "
|
||||
"is open", HOT_R["min_soc"] <= BMS_FLOOR + 0.05)
|
||||
"DECISIONS/SAFETY-25-loop-soc-floor.md, where AC 2 was DECIDED on "
|
||||
"2026-09-02: Option A, the BMS stays the only backstop, and the "
|
||||
"loop-side reserve is DEFERRED to ALLOC-01 - so this replay is not a "
|
||||
"pending exposure but the RECORDED behaviour, and part 10 stays as its "
|
||||
"tripwire", HOT_R["min_soc"] <= BMS_FLOOR + 0.05)
|
||||
|
||||
# --- 10d. the trap in the obvious fix, checked rather than asserted ---------
|
||||
# ⚠️ THE MEMO'S CENTRAL ENGINEERING CLAIM, RUN THROUGH THE REAL ARBITER. The
|
||||
@@ -2182,12 +2186,30 @@ check("...and DOCS.md says it in the options table AND in the section, with the
|
||||
"not a pack-wide reserve" in _docs_src
|
||||
and "IT BOUNDS THE PEAK GUARD ONLY" in _docs_src
|
||||
and "SAFETY-25" in _docs_src)
|
||||
check("...and the decision memo AC 1 asks for is COMMITTED, carries the options "
|
||||
"and a recommendation, and is honest that the decision is not taken",
|
||||
# ⚠️ THE DECISION LANDED (2026-09-02, Option A) AND THIS CHECK MOVED WITH IT.
|
||||
# It used to pin "Status: OPEN. This is a memo, not a decision." - the memo being
|
||||
# HONEST that AC 2 was unanswered. AC 2 is answered now, so the same check pins
|
||||
# the same property in its new form: the memo states WHICH option, WHEN, and its
|
||||
# REVISIT TRIGGER, and it still carries the options and the recommendation
|
||||
# UNCHANGED. A memo that recorded the verdict and deleted the reasoning would be
|
||||
# the failure this is guarding against, which is why "Recommendation" and both
|
||||
# rejected options are still asserted here rather than only the status line.
|
||||
_memo_txt = _memo.read_text(encoding="utf-8")
|
||||
check("...and the decision memo AC 1 asks for is COMMITTED, records AC 2 as "
|
||||
"DECIDED with a date, an option and a revisit trigger, and STILL carries "
|
||||
"the options analysis and the recommendation that were put to the owner",
|
||||
_memo.exists()
|
||||
and "Status: OPEN. This is a memo, not a decision." in _memo.read_text(
|
||||
encoding="utf-8")
|
||||
and "Recommendation" in _memo.read_text(encoding="utf-8"))
|
||||
and "Status: DECIDED 2026-09-02 — Option A." in _memo_txt
|
||||
and "ALLOC-01" in _memo_txt
|
||||
and "Recommendation" in _memo_txt
|
||||
and "### Option A" in _memo_txt and "### Option B" in _memo_txt)
|
||||
check("...and it says plainly that part 10f - the pin that made SAFETY-25 "
|
||||
"mergeable - is a SCALAR SoC sweep against a single-series capture and "
|
||||
"so cannot express two packs. The tripwire's own limits belong in the "
|
||||
"memo that leans on it, or the next reader merges a two-pack reserve on "
|
||||
"a green check that cannot see the failure",
|
||||
"10f" in _memo_txt and "SAFETY-27" in _memo_txt
|
||||
and "cannot express two packs" in _memo_txt.lower())
|
||||
check("...and DOCS.md sends the reader to it rather than leaving the open "
|
||||
"question implicit",
|
||||
"DECISIONS/SAFETY-25-loop-soc-floor.md" in _docs_src)
|
||||
@@ -2288,8 +2310,12 @@ check("...and so does the persistent notification, which outlives the page and "
|
||||
# The whole inventory of claim sources main.py can file, in any configuration.
|
||||
# ⚠️ ADDING TO THIS SET IS A DELIBERATE ACT. If you are here because you added a
|
||||
# claim to `Controller.cycle()` and this went red, that is the tripwire working:
|
||||
# read DECISIONS/SAFETY-25-loop-soc-floor.md, answer AC 2 in it, and update
|
||||
# part 10 and mutation 13 rather than this line alone.
|
||||
# read DECISIONS/SAFETY-25-loop-soc-floor.md and update part 10 and mutation 13
|
||||
# rather than this line alone. ⚠️ AC 2 IS DECIDED (2026-09-02, Option A) AND
|
||||
# THAT DOES NOT RETIRE THIS SET - Option B is DEFERRED, not rejected, and
|
||||
# "deferred" is exactly the state in which a ninth source gets built by
|
||||
# accident. Its revisit trigger is ALLOC-01, under which the reserve becomes a
|
||||
# PER-INVERTER interval at the allocator rather than a ninth claim here.
|
||||
CLAIM_SOURCES = {"device", "supervised", "failsafe", "safety", "peak",
|
||||
"arbitrage", "maintenance", "loop"}
|
||||
# The two appends that file a claim BUILT ELSEWHERE in the method. Named here so
|
||||
|
||||
Reference in New Issue
Block a user