SAFETY-32 fix round: grade the reduction on packs whose floors DIFFER
checks / unit (pull_request) Successful in 4m47s

AC 6 was not met for the reduction main.py actually hands decide(). The
reviewer removed the per-pack grading twice and the 19-file suite stayed green
both times. No shipped behaviour changes here - app/ is untouched. What changes
is what observes it.

WHY BOTH SURVIVED, and it is this repository's recurring defect landing on this
card's own Decision 2: under any rule-8-valid config every pack's
pack_floor_pct() equals peak_soc_floor, so `floors` is a list of IDENTICAL
numbers and max/min/first/last are four spellings of one answer. The only check
using a cut-out ABOVE the EMS floor - 12c's `_n1_deep` - ran on a roster of ONE,
where every reduction agrees. So no check ever evaluated binding_soc_floor() on
two packs whose floors differ. And the degeneracy is not a defence: this PR's own
argument ("unreachable is a claim about today's rule ordering, and
validate_startup_config() is also sim/inject.py's entry point") justifies writing
the reduction and equally obliges grading it.

  * test_peakguard.py part 12c2 - NEW. binding_soc_floor() on a roster whose
    per-pack floors ALL DIFFER: four SIMULATED cut-outs 30/25/40/35 %, each
    strictly above peak_soc_floor, so max (40), min (25), first (30), last (35)
    and the mean (32.5) are five different numbers and the extremes sit at
    INTERIOR roster positions. Two packs structurally cannot separate those -
    there max==last and min==first - and neither can three; four is the smallest
    roster that can. Precondition is its own check (habit 1); the counterfactual
    raises ONE pack - the last, neither extreme - past the old maximum and the
    answer follows (habit 7); peak_soc_floor is then lifted above every cut-out
    to grade the other side of the conjunction.
  * test_peakguard.py part 12g - REWRITTEN as behaviour (habit 4). The old
    assertion was `"self.packs" in <the unparsed call site>`, which `self.packs[:1]`
    still satisfies. It now drives REAL Controller.cycle()s on a two-pack roster,
    both packs at the same SoC, and reads PeakDecision.soc_floor - the argument,
    echoed back by the shipped decide(). The cut-outs are the only thing varied,
    and the SAME PAIR is carried in BOTH ORDERS.
  * ⚠️ TWO MORE SURVIVORS, found by this round's own battery and recorded rather
    than quietly repaired. (a) The first cut of 12g varied only pack B's cut-out,
    so `self.packs[-1:]` (+5 bytes) agreed with the roster on both runs and stayed
    green across all 19 files; the swapped pair closes it, and between the two
    directions no single-position slice passes all three runs. (b)
    binding_soc_floor()'s EMPTY-roster branch rewritten to `return None`
    (-43 bytes) was green too - a defensive branch nothing in the suite reached.
    Both directions of it are pinned now.
  * wired_over_limit() gained a prepare(ctl) hook, inside its retry loop so every
    attempt grades the same site.
  * In-process mutation rows 22, 22b and 23 record all four survivors.

Nine further source mutations went through the harness on the fixed tree
(binding_soc_floor -> floors[0] / floors[-1], pack_floor_pct conjoining with
min, pack_permissions grading against packs[0], `any` -> `all` on the unknown
floor, Controller.packs reversed, decide(soc=) taking inverters[0].soc, and the
two survivors above). Seven were red on first run; the two survivors are now red
as well. Three of the nine have a ZERO byte delta and still went red, which is
the direction stale bytecode cannot fabricate - and the harness purged anyway.

MANDATORY PROOF, all in the CI image over a tree rsynced to a plain path and
bind-mounted :ro, bytecode entry count printed before and after the purge:
each of the four mutations now takes test_peakguard.py RED, and the unmutated
tree is 19/19 green (twice, plus test_peakguard.py five times in isolation -
card CI-06's flake did not reappear). The zero-byte one was proved to have RUN rather than
assumed: dis.get_instructions LOAD_GLOBALs were [float, pack_floor_pct, any,
min] and binding_soc_floor() returned 25.0 on the 12c2 roster where the shipped
code returns 40.0.

No measurement of inverter B appears anywhere. It does not physically exist.
Every second-pack and fourth-pack figure added here is SIMULATED and labelled at
its definition; only inverter A's 10 % is read off hardware. No hardware was
touched.

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-04 17:09:58 +02:00
co-authored by Claude Opus 5
parent d9388c81b0
commit 8501e055f2
3 changed files with 493 additions and 10 deletions
+45
View File
@@ -58,6 +58,51 @@ thing that stops. Rule 8's own failure mode, reached through rule 8.
weakness of its own: `_flat_bms_floor()` had the default in two places and each
covered for the other, so deleting either survived — masking, in three lines of
option reading. One defaulting line now.
* ⚠️ **FIX ROUND — that battery was INCOMPLETE, and the review found what it
missed.** Neither of the two mutations below appears in the twelve rows above:
the ledger graded "the reduction dropped" and "the bare option handed over"
and never asked what happens when the reduction is simply the *wrong* one, or
when the roster handed to it is *sliced*. No shipped behaviour changed here;
two *checks* whose data could not reach the failure they named did. `max(floors)` → `min(floors)` in
`binding_soc_floor()` (zero byte delta) and `self.packs` → `self.packs[:1]` at
the `decide()` call site (+4 bytes) were each green across all 19 files. Both
survived for the same reason: **under any rule-8-valid config every pack's
`pack_floor_pct()` equals `peak_soc_floor`**, so `floors` is a list of
identical numbers and `max`, `min`, first and last are four spellings of one
answer — and the one check using a cut-out above the EMS floor ran on a roster
of ONE, where every reduction agrees. The second also walked through part 12g,
which asserted `"self.packs" in <the unparsed call site>`: a slice still
contains that substring, so the pin passed on the mutation that grades the
whole site against pack A. MUTATION.md habit 4 — a source-text test is a check
on how code is spelled, not on what it does.
* **New part 12c2** evaluates `binding_soc_floor()` on a roster whose per-pack
floors **all differ** — four simulated cut-outs of 30/25/40/35 %, every one
above `peak_soc_floor`, chosen so that `max` (40), `min` (25), first (30),
last (35) and the mean (32.5) are five different numbers and the two extremes
sit at *interior* roster positions. Two packs cannot separate those four;
four is the smallest roster that can. The precondition is its own check
(habit 1) and one pack — the last, neither deepest nor shallowest — is then
raised past the old maximum and the answer follows it (habit 7).
* **Part 12g is now behaviour, not source text.** Real `Controller.cycle()`s on
a two-pack roster, both packs at the same SoC, with the cut-outs as the only
thing varied: move the **non-first** pack's cut-out and the floor reaching
`decide()` moves 20 % → 35 %, with the site's verdict flipping from a sized
discharge to FLOOR-LIMITED. `self.packs[:1]` answers 20 % both times. Which
option key the floor comes from stays pinned on the source where it already
was, because that claim genuinely is about the code's shape.
* ⚠️ **Two MORE survivors, found by this round's own battery and recorded
rather than quietly repaired.** (a) The first cut of part 12g varied only
pack B's cut-out, so `self.packs[-1:]` — grade the site against the **last**
pack — agreed with the full roster on both runs and stayed green across all
19 files (+5 bytes). A two-run fixture pins one slice and not the other.
Part 12g now runs the **same pair of cut-outs in both orders**, so the
deeper one is on the second pack in one run and on the first in another and
the site floor is 35 % in both; no single-position slice passes all three
runs. (b) `binding_soc_floor()`'s empty-roster branch rewritten to
`return None` (−43 bytes) was also green — a defensive branch nothing in the
suite reached. Both directions of it are pinned now, with the note that it
cannot permit anything either way because `pack_floor_soc` is `None` at an
empty roster.
⚠️ **No measurement of inverter B appears anywhere in this change.** It does not
exist yet. Every second-pack figure — in fixtures, in the probe, in
@@ -81,6 +81,116 @@ grading each pack separately would be. The converse is not true and is not
claimed — a site can be refused while every pack individually has headroom, which
costs a shave.
### 3a. ⚠️ FIX ROUND — the reduction was written and argued, and NOT GRADED
PR #48 was rejected because the argument in §4 below justified *writing* the
reduction and was then not carried into a check. Neither of the two mutations
below appears anywhere in §6's twelve-row battery: it graded *"the reduction
dropped"* and *"the bare option handed over"*, and never asked what happens when
the reduction is the **wrong one**, or when the roster handed to it is
**sliced**. Both were applied to `app/` by the reviewer and **all 19 files
stayed green**:
| mutation | Δ | why it survived |
|---|---|---|
| `binding_soc_floor()`: `max(floors)` → `min(floors)` | **0 bytes** | every roster in the suite was rule-8-valid or one pack long, so `floors` held **identical numbers** and `max`/`min`/first/last were four spellings of one answer |
| the `decide()` call site: `self.packs` → `self.packs[:1]` | **+4 bytes** | part 12g asserted `"self.packs" in <the unparsed expression>` — and `self.packs[:1]` **contains that substring** |
The zero-byte one was proved to have actually run rather than assumed:
`dis.get_instructions` showed the `LOAD_GLOBAL`s as `[float, pack_floor_pct,
any, min]`, and `binding_soc_floor([Pack("a",50,10), Pack("b",50,35)], 20.0)`
returned `20.0` where the shipped code returns `35.0`.
**The common cause is this repository's recurring defect** — a check whose DATA
cannot reach the failure it names. The one check that used a cut-out **above**
the EMS floor (12c's `_n1_deep`) ran on a roster of **ONE**, where every possible
reduction agrees. So no check in the suite ever evaluated
`binding_soc_floor()` on two packs whose floors **differ**.
**And the rule-8 degeneracy is not a defence, by §4's own words.** If
"unreachable is a claim about today's rule ordering, not a property of this
function, and `validate_startup_config()` is also `sim/inject.py`'s entry point"
is enough to *write* the reduction, it is enough to *grade* it. Through
`sim/inject.py`, which does not run rule 8 at all, `min(floors)` lets the guard
command a forced discharge to 20 % on a pack whose BMS cuts out at 35 %.
**What was added — no shipped behaviour changed, only what observes it:**
* **`test_peakguard.py` part 12c2** evaluates `binding_soc_floor()` on a roster
whose per-pack floors **all differ**. Four **simulated** cut-outs —
`30, 25, 40, 35` %, every one strictly above `peak_soc_floor: 20`, so
`pack_floor_pct()` selects the cut-out for each and `floors` *is* that tuple in
that order. The four candidate reductions are then four different numbers:
max = 40 <- the shipped answer
min = 25
first = 30 <- "grade the site against pack A", inside the function
last = 35
mean = 32.5
The extremes sit at **interior** roster positions (index 2 is the max, index 1
the min). A two-pack roster structurally cannot separate these — there `max`
and `last` coincide and so do `min` and `first` — and neither can three. Four
is the smallest roster that separates all four. The precondition is its own
check (habit 1), one pack is then raised past the old maximum (habit 7: the
**last** pack, which was neither extreme, so the line can only move if the
reduction read it), and `peak_soc_floor` is lifted above every cut-out to grade
the other side of the conjunction.
* **Part 12g is behaviour now, not source text** (MUTATION.md habit 4). Real
`Controller.cycle()`s on a two-pack roster, both packs held at the same SoC and
everything else identical, with the packs' own cut-outs as the only thing
varied:
cut-outs A 10 / B 18 % -> decide() got soc_floor=20 %, floor_limited=False
cut-outs A 10 / B 35 % -> decide() got soc_floor=35 %, floor_limited=True
The observable is `PeakDecision.soc_floor` — the argument, echoed back by the
shipped `decide()` — so the whole path from `Controller.inverters` to the
guard's argument is under test, and the site's **verdict flips** as well as the
number. `self.packs[:1]` answers 20 % both times. **Which** option key the floor
comes from stays pinned on the source in part 4, because that claim genuinely
is about the code's shape; that the option still governs is now graded
behaviourally too.
* ⚠️ **A THIRD SURVIVOR, FOUND WHILE FIXING THE FIRST TWO, AND RECORDED RATHER
THAN QUIETLY REPAIRED.** The first cut of part 12g varied only **pack B's**
cut-out — so `self.packs[-1:]`, grade the site against the **last** pack,
agreed with the full roster on both runs and stayed green across all 19 files
(+5 bytes). A two-run fixture that only ever moves the second pack pins `[:1]`
and not `[-1:]`; it is the same defect one step along, and it was in the fix
for this defect. Part 12g now runs the **same pair of cut-outs in both
orders**:
cut-outs A 10 / B 18 % -> soc_floor=20 %, floor_limited=False
cut-outs A 10 / B 35 % -> soc_floor=35 %, floor_limited=True
...the SAME PAIR SWAPPED, A 35 / B 18 % -> soc_floor=35 %, floor_limited=True
`[:1]` answers 20 % on the second run, `[-1:]` answers 20 % on the third, and
the roster answers 35 % on both. No single-position slice passes all three.
This is the mirror `12h` already ran on the SoC axis, which is where the shape
was borrowed from.
* ⚠️ **AND A FOURTH, from the same battery: the EMPTY-roster branch.** Rewriting
`binding_soc_floor([])`'s `float(soc_floor)` to `return None` (−43 bytes) was
green across all 19 files. It is defensive code — `Controller.inverters` is
never empty on a started controller, and `inputs_ok` refuses the cycle before
the guard is reached if it were — so nothing in the suite went near it. It is
**pinned rather than argued**, in both directions (the option when there is
one, `None` when there is not), because a branch with no check is a branch
that can be rewritten under a green suite. The check also records why it
cannot permit anything either way: `pack_floor_soc` is `None` at an empty
roster, and `discharge_permitted()` refuses on a `None` SoC whatever the floor
is.
* **`wired_over_limit()` gained a `prepare(ctl)` hook**, inside its retry loop so
every attempt grades the same site, which is the only window in which a second
inverter can join the roster before the telegrams are fed.
* **In-process mutation rows 22, 22b and 23** record all four survivors, so a
later edit that re-degenerates any of these fixtures goes red on a row rather
than on nothing.
## 4. ⚠️ THE HONEST LIMITATION, and it is the reason this record exists
**Under any configuration rule 8 accepts, `binding_soc_floor()` answers
+338 -10
View File
@@ -1063,9 +1063,15 @@ check("...and the status page says NOT COVERED rather than 'idle' or a green "
# Controller rather than left to flake. It is bounded at three attempts: a fourth
# would mean something is wrong that is not a boundary.
def wired_over_limit(db, want_floor_w, opts=None, store=None, batt_w=0.0,
standing_w=None, soc=55.0):
standing_w=None, soc=55.0, prepare=None):
"""A real Controller driven one real cycle over a real over-limit quarter.
`prepare(ctl)` runs after the Controller is built and before the telegrams
are fed, which is the only window in which a SECOND inverter can be put on
the roster and given its own readings. It is inside the retry loop on
purpose: each attempt builds a fresh Controller, so a hook that mutated only
the first one would leave attempts 2-4 grading a different site.
⚠️ THE LOAD IS DERIVED FROM WHERE IN THE QUARTER THE WALL CLOCK LANDS, and
it has to be. `excess = 900 × (projected − ceiling) ÷ remaining`, so a fixed
5200 W fixture asks for 1700 W with ten minutes left and 300 kW with five
@@ -1079,6 +1085,8 @@ def wired_over_limit(db, want_floor_w, opts=None, store=None, batt_w=0.0,
store=_Store(store) if store is not None else None)
ctl.auto, ctl.soc, ctl.batt = True, soc, batt_w
ctl.dev_min, ctl.dev_max = -LIVE_MAX_W, LIVE_MAX_W
if prepare is not None:
prepare(ctl)
if standing_w is not None:
# The command STANDING ON THE WIRE, which is what B3 below needs to
# differ from the measured battery power.
@@ -3291,6 +3299,130 @@ check("...and on the pair rule 8 REFUSES - a cut-out above the EMS floor - the "
"would be satisfied by `soc_floor` typed straight through",
_n1_deep == B_PACK_FLOOR_DEEP_SIM and _n1_deep != SOC_FLOOR)
# --- 12c2. THE REDUCTION ITSELF, ON A ROSTER WHOSE FLOORS ALL DIFFER ---------
# ⚠️ THIS IS THE CHECK PR #48 DID NOT HAVE, AND ITS ABSENCE IS THIS REPOSITORY'S
# RECURRING DEFECT LANDING ON THIS CARD. Every other fixture above grades
# `binding_soc_floor()` on a roster of ONE (12c) or on a config rule 8 accepts,
# where `pack_floor_pct()` answers `peak_soc_floor` for EVERY pack - so `floors`
# is a list of IDENTICAL numbers and `max`, `min`, `floors[0]` and `floors[-1]`
# are four spellings of one answer. A reviewer changed `max(floors)` to
# `min(floors)` and all 19 files stayed GREEN. The data could not reach the
# failure the check names.
#
# ⚠️ AND THE DEGENERACY IS NOT A DEFENCE, BY THIS CARD'S OWN ARGUMENT.
# `pack_floor_pct()`'s docstring, main.py's call-site comment and DECISIONS §4
# all say "unreachable is a claim about today's rule ordering, not a property of
# this function, and `validate_startup_config()` is also `sim/inject.py`'s entry
# point". That argument justifies WRITING the reduction; it equally OBLIGES
# grading it. Via `sim/inject.py`, which does not run rule 8 at all, `min` here
# lets the guard command a forced discharge down to 20 % on a pack whose BMS
# cuts out at 35 %. So the reduction is graded on a roster rule 8 would refuse -
# deliberately, because that is the only roster on which it does anything.
#
# ⚠️ THE FOUR NUMBERS, AND WHY THESE FOUR. Every one is ABOVE `peak_soc_floor`,
# so `pack_floor_pct()` selects the CUT-OUT for each and `floors` IS this tuple
# in this order. Read as a list, the four candidate reductions are four
# DIFFERENT numbers:
#
# floors = (30, 25, 40, 35)
# max(floors) = 40 <- the shipped answer, the deepest cut-out
# min(floors) = 25
# floors[0] = 30 <- "grade the site against pack A", survivor 2's
# defect expressed inside the function
# floors[-1] = 35
# mean = 32.5
#
# so no two of them can be confused, and the extremes sit at INTERIOR roster
# positions (index 2 is the max, index 1 the min) - which is what a two-pack
# fixture structurally cannot do, since there `max` and `last` coincide and so
# do `min` and `first`. Three packs cannot do it either. Four is the smallest
# roster that separates all four.
#
# ⚠️ ALL FOUR ARE SIMULATED. There is ONE pack on this site; inverter B is ~8
# days out and does not physically exist, and a roster of four certainly does
# not. Nothing here is a measurement of anything. `binding_soc_floor()` is a
# pure function over a list and these are the inputs that make its answer
# observable - the only measured cut-out in this file is inverter A's 10 %.
_MANY_FLOORS_SIM = (30.0, 25.0, 40.0, 35.0)
_many_packs = [Pack(f"sim-{i}", 50.0, f) for i, f in enumerate(_MANY_FLOORS_SIM)]
_many_derived = [pack_floor_pct(pk, SOC_FLOOR) for pk in _many_packs]
_many_answers = {"max": max(_MANY_FLOORS_SIM), "min": min(_MANY_FLOORS_SIM),
"first": _MANY_FLOORS_SIM[0], "last": _MANY_FLOORS_SIM[-1],
"mean": sum(_MANY_FLOORS_SIM) / len(_MANY_FLOORS_SIM)}
print(f" a SIMULATED roster of {len(_many_packs)} with cut-outs "
f"{[f'{f:.0f}' for f in _MANY_FLOORS_SIM]} % over peak_soc_floor "
f"{SOC_FLOOR:.0f} % -> per-pack floors {[f'{f:.0f}' for f in _many_derived]} %")
print(" candidate reductions: "
+ ", ".join(f"{k}={v:g}" for k, v in _many_answers.items())
+ f"; binding_soc_floor() -> {binding_soc_floor(_many_packs, SOC_FLOOR):g}")
check("PRECONDITION (MUTATION.md habit 1): on this roster max, min, first, last "
"and the mean are FIVE DIFFERENT numbers, and every cut-out is strictly "
"above peak_soc_floor so the per-pack floors ARE the cut-outs. A later "
"edit that makes any two of them equal takes this line red rather than "
"leaving the claim below quietly vacuous - which is exactly what a "
"rule-8-valid roster does to it",
len(set(_many_answers.values())) == 5
and all(f > SOC_FLOOR for f in _MANY_FLOORS_SIM)
and _many_derived == list(_MANY_FLOORS_SIM))
check("⚠️ `binding_soc_floor()` answers the MAXIMUM of the packs' own floors - "
"the DEEPEST cut-out on the roster - and on this roster that is not the "
"minimum, not the first pack's, not the last pack's and not the mean. "
"This is the conservative pairing with the MINIMUM in `pack_floor_soc`: "
"`min(soc) <= max(floor)` is implied by 'some pack is at or below its own "
"floor', so the site verdict is never more permissive than grading each "
"pack separately. `min(floors)` here would let a forced discharge be "
"sized down to 25 % on the pack whose BMS cuts out at 40 %",
binding_soc_floor(_many_packs, SOC_FLOOR) == _many_answers["max"]
and binding_soc_floor(_many_packs, SOC_FLOOR) != _many_answers["min"]
and binding_soc_floor(_many_packs, SOC_FLOOR) != _many_answers["first"]
and binding_soc_floor(_many_packs, SOC_FLOOR) != _many_answers["last"]
and binding_soc_floor(_many_packs, SOC_FLOOR) != _many_answers["mean"])
# ⚠️ THE COUNTERFACTUAL, ONE THING VARIED FROM AN ACCEPTED BASELINE (habit 7).
# The pack that is raised is the LAST one, which is neither the max nor the min
# above - so this line can only move if the reduction really looked at it. It is
# raised past the old maximum, and the answer follows it.
_deeper = list(_many_packs)
_deeper[-1] = Pack("sim-3", 50.0, 45.0)
print(f" ...raise ONLY the last pack 35 % -> 45 %: "
f"{binding_soc_floor(_many_packs, SOC_FLOOR):g} -> "
f"{binding_soc_floor(_deeper, SOC_FLOOR):g}")
check("...and the counterfactual: raise ONE pack - the LAST, which was neither "
"the deepest nor the shallowest - past the old deepest, and the answer "
"follows it to 45 %. So the reduction reads every entry, and the 40 % "
"above was not a coincidence of position",
binding_soc_floor(_deeper, SOC_FLOOR) == 45.0
and binding_soc_floor(_many_packs, SOC_FLOOR) == 40.0)
# ...and the EMS floor is still the other half of the conjunction: lift it above
# every cut-out on the roster and it wins for all of them.
_over = SOC_FLOOR + 40.0
check("...and with `peak_soc_floor` lifted above every cut-out on this roster "
"the answer is the OPTION, for all four - `pack_floor_pct()` is a "
"conjunction and this is its other side, so a reduction that dropped the "
"EMS floor entirely goes red here as well as in the N=1 sweep",
_over > max(_MANY_FLOORS_SIM)
and binding_soc_floor(_many_packs, _over) == float(_over))
# ...and the EMPTY roster, the SECOND thing this fix round's own battery found
# unguarded: `return None` in place of `float(soc_floor)` there (-43 bytes) was
# GREEN across all 19 files. It is a defensive branch - `Controller.inverters` is never
# empty on a started controller, and `inputs_ok` refuses the cycle before the
# guard is reached if it were - so nothing in the suite went near it. Pinned
# rather than argued, because a branch with no check is a branch that can be
# rewritten under a green suite, and BOTH directions of it are asserted: the
# option when there is one, and None when there is not.
print(f" an EMPTY roster: binding_soc_floor([], {SOC_FLOOR:.0f}) -> "
f"{binding_soc_floor([], SOC_FLOOR)}, binding_soc_floor([], None) -> "
f"{binding_soc_floor([], None)}")
check("an EMPTY roster answers the OPTION - there is no pack to conjoin with, "
"so there is nothing to raise the floor above it - and answers None when "
"the option itself is None. ⚠️ AND IT CANNOT PERMIT ANYTHING EITHER WAY: "
"`Controller.pack_floor_soc` is None at an empty roster, and "
"`discharge_permitted()` refuses on a None SoC whatever the floor is",
binding_soc_floor([], SOC_FLOOR) == float(SOC_FLOOR)
and binding_soc_floor([], None) is None
and discharge_permitted(None, binding_soc_floor([], SOC_FLOOR))
== FLOOR_SOC_UNREADABLE)
# --- 12d. AC 4: pack_floor_soc's None-on-missing is untouched ---------------
_c32, _h32 = built(TMP / "s32.db")
_b32, _restore32 = second_inverter(_c32)
@@ -3436,17 +3568,147 @@ check("...and with the key absent entirely the one pack takes the SHIPPED "
and BMS_FLOOR == PEAK_OPTION_DEFAULTS["bms_soc_floor"])
# --- 12g. the call site: main.py hands decide() the per-pack reduction -------
# ⚠️ REWRITTEN. THIS CHECK USED TO BE A SUBSTRING TEST OVER main.py's SOURCE -
# `"self.packs" in _floor_call` - AND IT WAS NOT A PIN. The mutation that grades
# the whole site against pack A's floor is `self.packs[:1]`, which still
# CONTAINS that substring: +4 bytes, all 19 files green, including this one.
# MUTATION.md habit 4, in the same costume CFG-04's `if entity:` wore: the check
# asserted against the TEXT of the thing under test instead of its behaviour.
# The AST is still unparsed and PRINTED (habit 3) because a reader wants to see
# the expression, but nothing below asserts on its spelling. Which OPTION KEY
# the floor comes from is a claim genuinely about the code's shape and is pinned
# on the source in part 4, where it already lives; what is graded here is the
# VALUE that reaches `decide()`.
_floor_call = ast.unparse(next(k.value for k in _call.keywords
if k.arg == "soc_floor"))
print(f" main.py's decide(soc_floor=...) is `{_floor_call}`")
check("main.py hands `decide()` the PER-PACK reduction of `peak_soc_floor`, "
"not the bare option: `binding_soc_floor(self.packs, ...)`, so the floor "
"the site is graded against can never sit below the deepest cut-out on "
"the roster. The key and the one defaults mirror are still in that "
"expression - graded three parts above - because this card moved the "
"REDUCTION and not the source",
"binding_soc_floor" in _floor_call and "self.packs" in _floor_call
and '"peak_soc_floor"' in _floor_call.replace("'", '"'))
def _floor_reaching_decide(tag, a_floor, b_floor, peak_floor=None):
"""Drive ONE REAL `Controller.cycle()` on a TWO-pack roster and report the
floor `peakguard.decide()` was actually handed, read back off the decision
main.py itself filed (`PeakDecision.soc_floor` is the argument, echoed).
⚠️ NOT A SPY AND NOT A GREP. The shipped `cycle()` runs, the shipped
`binding_soc_floor()` runs, and the observable is a field of the decision
the controller stored - so the whole path from `Controller.inverters` to the
guard's argument is under test. Both packs are held at the SAME SoC and the
same everything else; the only things that vary between calls are the packs'
own cut-outs.
"""
opts = {} if peak_floor is None else {"peak_soc_floor": peak_floor}
def _prep(ctl):
second_inverter(ctl)
for inv, fl in zip(ctl.inverters, (a_floor, b_floor)):
inv.soc, inv.batt, inv.bms_soc_floor = SOC_BETWEEN, 0.0, fl
ctl, hs, _net, _rem = wired_over_limit(
TMP / f"s32g_{tag}.db", LIVE_MAX_W * 0.3, opts=opts, prepare=_prep)
return ctl, ctl.peak_guard
# BASELINE: neither cut-out is above the EMS floor, so the conjunction selects
# the option for both packs and the site floor is `peak_soc_floor`. At
# SOC_BETWEEN (5 points above it) the guard is permitted and sizes a real
# discharge. Both runs below vary one thing from THIS baseline (habit 7).
_g_ctl_shallow, _g_shallow = _floor_reaching_decide(
"shallow", A_PACK_FLOOR, B_PACK_FLOOR_SIM)
# The SECOND pack's cut-out moves above the EMS floor. Nothing else changes -
# not pack A, not either SoC, not the option.
_g_ctl_deep, _g_deep = _floor_reaching_decide(
"deepb", A_PACK_FLOOR, B_PACK_FLOOR_DEEP_SIM)
# ⚠️ AND THE SAME TWO CUT-OUTS SWAPPED BETWEEN THE PACKS, which is the mirror
# 12h already runs on the SoC axis and which THIS part needed and did not have.
# Without it, `self.packs[-1:]` - grade the site against the LAST pack - answers
# the same two numbers as the full roster on the two runs above, because pack A
# is never the deeper one there. It SURVIVED the first cut of this fix round,
# +5 bytes, all 19 files green. With the pair swapped, `[:1]` and `[-1:]`
# disagree with the roster in opposite runs, and no single-position slice is
# left that can pass all three.
_g_ctl_deepa, _g_deepa = _floor_reaching_decide(
"deepa", B_PACK_FLOOR_DEEP_SIM, B_PACK_FLOOR_SIM)
print(f" REAL two-pack cycles, both packs at {SOC_BETWEEN:.0f} %, "
f"peak_soc_floor {SOC_FLOOR:.0f} % (every cut-out but inverter-a's own "
f"{A_PACK_FLOOR:.0f} % is SIMULATED):")
print(f" cut-outs A {A_PACK_FLOOR:.0f} / B {B_PACK_FLOOR_SIM:.0f} % -> "
f"decide() got soc_floor={_g_shallow.soc_floor:g} %, floor_limited="
f"{_g_shallow.floor_limited}")
print(f" cut-outs A {A_PACK_FLOOR:.0f} / B {B_PACK_FLOOR_DEEP_SIM:.0f} % -> "
f"decide() got soc_floor={_g_deep.soc_floor:g} %, floor_limited="
f"{_g_deep.floor_limited}")
print(f" ...the SAME PAIR SWAPPED, A {B_PACK_FLOOR_DEEP_SIM:.0f} / B "
f"{B_PACK_FLOOR_SIM:.0f} % -> decide() got "
f"soc_floor={_g_deepa.soc_floor:g} %, floor_limited="
f"{_g_deepa.floor_limited}")
check("⚠️ THE CALL-SITE PIN, AS BEHAVIOUR. Change the cut-out of a NON-FIRST "
"pack and the floor that reaches `decide()` MOVES with it: 20 % (the "
"option, because B's cut-out is under it) becomes 35 % (B's own cut-out, "
"because it is over it). A call site that graded the site against "
"`self.packs[:1]` - pack A alone - answers 20 % both times and cannot "
"produce this difference, and neither can `min(floors)`; that is the "
"+4-byte mutation that walked through the substring test this check "
"replaces",
_g_shallow.soc_floor == float(SOC_FLOOR)
and _g_deep.soc_floor == float(B_PACK_FLOOR_DEEP_SIM)
and _g_deep.soc_floor != _g_shallow.soc_floor)
check("⚠️ ...AND THE SAME PAIR SWAPPED GIVES THE SAME SITE FLOOR, so what is "
"reduced is the ROSTER and not a POSITION in it. `self.packs[-1:]` - the "
"LAST pack alone - agrees with the roster on both runs above and answers "
"20 % here, where the deeper cut-out sits on the FIRST pack. That "
"mutation SURVIVED the first cut of this fix round; this is the line "
"that closes it, and between them the two directions leave no "
"single-position slice that can pass all three runs",
_g_deepa.soc_floor == float(B_PACK_FLOOR_DEEP_SIM)
and _g_deepa.soc_floor == _g_deep.soc_floor
and _g_deepa.soc_floor != _g_shallow.soc_floor
and [pk.bms_floor for pk in _g_ctl_deepa.packs]
== [B_PACK_FLOOR_DEEP_SIM, B_PACK_FLOOR_SIM])
check("...and it is not just a number in a report - THE SITE'S VERDICT FLIPS. "
"With both packs at 25 % the shallow site is permitted and the guard "
"sizes a positive forced discharge; BOTH deep sites are FLOOR-LIMITED, "
"because 25 % is at or below the 35 % cut-out wherever on the roster it "
"sits. Grading the roster against one position would spend a pack its "
"BMS stops - through `sim/inject.py`, which does not run rule 8 - and "
"this line is what goes red when it does",
_g_shallow.floor_limited is False and _g_shallow.floor_w > 0.0
and _g_deep.floor_limited is True and _g_deep.floor_w == 0.0
and _g_deep.soc_floor_reason == FLOOR_AT_SOC_FLOOR
and _g_deepa.floor_limited is True and _g_deepa.floor_w == 0.0
and _g_deepa.soc_floor_reason == FLOOR_AT_SOC_FLOOR)
check("...and the precondition that makes the three runs comparable (habit 1): "
"the two deep runs carry THE SAME PAIR of cut-outs in opposite orders, "
"those two numbers really are different, and all six packs are at the "
"SAME state of charge - so neither the SoC reduction nor a roster "
"position can be what separated the answers",
B_PACK_FLOOR_SIM != B_PACK_FLOOR_DEEP_SIM
and A_PACK_FLOOR != B_PACK_FLOOR_DEEP_SIM
and sorted(pk.bms_floor for pk in _g_ctl_deepa.packs)
== sorted([B_PACK_FLOOR_SIM, B_PACK_FLOOR_DEEP_SIM])
and [pk.soc for pk in _g_ctl_deep.packs] == [SOC_BETWEEN, SOC_BETWEEN]
and [pk.soc for pk in _g_ctl_deepa.packs] == [SOC_BETWEEN, SOC_BETWEEN]
and [pk.bms_floor for pk in _g_ctl_deep.packs]
== [A_PACK_FLOOR, B_PACK_FLOOR_DEEP_SIM]
and [pk.bms_floor for pk in _g_ctl_shallow.packs]
== [A_PACK_FLOOR, B_PACK_FLOOR_SIM]
and _g_ctl_deep.pack_floor_soc == _g_ctl_shallow.pack_floor_soc
== _g_ctl_deepa.pack_floor_soc)
# ...and the OTHER half of the conjunction still reaches the same argument, so
# the reduction did not replace the source: move `peak_soc_floor` itself, with
# both cut-outs held under it, and the floor follows the OPTION.
_G_OPT_ODD = SOC_FLOOR + 22.0
_, _g_opt = _floor_reaching_decide("opt", A_PACK_FLOOR, B_PACK_FLOOR_SIM,
peak_floor=_G_OPT_ODD)
print(f" peak_soc_floor moved to {_G_OPT_ODD:.0f} % with both cut-outs "
f"under it -> decide() got soc_floor={_g_opt.soc_floor:g} %")
check("...and with both cut-outs held below it, moving `peak_soc_floor` moves "
"the floor that reaches `decide()` too - so this card moved the "
"REDUCTION and not the SOURCE. WHICH key that is, is a claim about the "
"code's shape and stays pinned on the source in part 4; that the option "
"still governs is a claim about behaviour and is graded here",
_g_opt.soc_floor == float(_G_OPT_ODD)
and _G_OPT_ODD > B_PACK_FLOOR_SIM and _G_OPT_ODD > A_PACK_FLOOR
and _g_opt.soc_floor != _g_shallow.soc_floor)
# --- 12h. the operator-facing half, driven through the shipped method -------
# ⚠️ RUN, NOT GREPPED. CFG-04's review found a substring test over main.py that
@@ -3729,6 +3991,61 @@ MUT.append(("the per-pack notification naming a fixed roster position",
and "inverter-b" not in _body32b.split("WHICH PACK")[-1],
"part 12h: the same two readings swapped between the packs"))
# ---- the two PR #48 SURVIVORS, and why they survived -------------------------
# ⚠️ BOTH OF THESE WERE APPLIED TO app/ AND ALL 19 FILES STAYED GREEN. They are
# this repository's recurring defect - a check whose DATA cannot reach the
# failure it names - landing on this card's own reduction, and they are recorded
# here as rows so that a later edit which re-degenerates the fixtures goes red
# on the row rather than on nothing.
# 22. `binding_soc_floor()` reduced with `min()` instead of `max()`. ZERO byte
# delta, and the reviewer proved by disassembly that it really ran. It
# survived because every roster in the suite was rule-8-valid or one pack
# long, so `floors` held IDENTICAL numbers and max/min/first/last were four
# spellings of one answer. 12c2's four simulated cut-outs are the data that
# reaches it.
_mut22 = [min(_MANY_FLOORS_SIM), _MANY_FLOORS_SIM[0], _MANY_FLOORS_SIM[-1],
sum(_MANY_FLOORS_SIM) / len(_MANY_FLOORS_SIM)]
MUT.append(("`binding_soc_floor()` reduced with `min()`, `first` or `last` "
"instead of `max()` - PR #48 SURVIVOR 1, zero byte delta",
binding_soc_floor(_many_packs, SOC_FLOOR) == max(_MANY_FLOORS_SIM)
and all(binding_soc_floor(_many_packs, SOC_FLOOR) != m
for m in _mut22),
"part 12c2: 'max, min, first, last and the mean are FIVE DIFFERENT "
"numbers'. ⚠️ The four cut-outs sit on a roster rule 8 would REFUSE, "
"deliberately: on one it accepts all four reductions are equal, "
"which is precisely why this mutation survived"))
# 22b. `binding_soc_floor()`'s EMPTY-roster branch rewritten to `return None`.
# -43 bytes, green across all 19 files until 12c2's last block pinned it.
# A defensive branch nothing reaches is still a branch.
MUT.append(("`binding_soc_floor([])` answering None instead of the option",
binding_soc_floor([], SOC_FLOOR) == float(SOC_FLOOR)
and binding_soc_floor([], None) is None,
"part 12c2's last block: 'an EMPTY roster answers the OPTION ... "
"and answers None when the option itself is None'"))
# 23. the CALL SITE grading the whole site against ONE ROSTER POSITION -
# `self.packs[:1]` (+4 bytes, PR #48's survivor) or `self.packs[-1:]`
# (+5 bytes, which survived the FIRST CUT OF THIS FIX ROUND until the
# swapped pair was added). 12g's old substring assertion
# (`"self.packs" in _floor_call`) passes on both, because a slice still
# contains that substring. Three runs close both directions at once: the
# deep cut-out is on the SECOND pack in one and on the FIRST in another, and
# the site floor is the same 35 % in both.
MUT.append(("main.py grading the site against ONE roster position - "
"`self.packs[:1]` (PR #48 SURVIVOR 2) or `self.packs[-1:]`",
_g_deep.soc_floor == float(B_PACK_FLOOR_DEEP_SIM)
and _g_deepa.soc_floor == float(B_PACK_FLOOR_DEEP_SIM)
and _g_shallow.soc_floor == float(SOC_FLOOR)
and _g_deep.floor_limited and _g_deepa.floor_limited
and not _g_shallow.floor_limited,
"part 12g, REWRITTEN as behaviour: real two-pack cycles, the SAME "
"pair of cut-outs carried in both orders, and both the floor "
"reaching decide() and the site's verdict follow the ROSTER rather "
"than a position. ⚠️ A substring test over main.py cannot see a "
"slice; MUTATION.md habit 4"))
for label, caught, where in MUT:
print(f" {'caught' if caught else 'NOT CAUGHT':>10} {label}")
print(f" {where}")
@@ -3748,7 +4065,18 @@ check("...and SAFETY-32's own eight defeats are each caught too - the flat "
"first pack's floor borrowed for all, the reduction dropped, an absent "
"cut-out defaulted to 0 %, rule 8 grading one unit, rule 8 naming a "
"hardwired one, and the alarm naming a position instead of a pack",
all(m[1] for m in MUT[13:21]) and len(MUT) == 21)
all(m[1] for m in MUT[13:21]))
check("...⚠️ AND SO ARE THE FOUR DEFEATS THAT SURVIVED A GREEN 19-FILE SUITE, "
"which is why this fix round exists: `min(floors)` in place of "
"`max(floors)` (zero byte delta) and `self.packs[:1]` at the call site "
"(+4 bytes), both green on d9388c8; `self.packs[-1:]` (+5 bytes), green "
"on the FIRST CUT OF THE FIX ITSELF; and the empty-roster branch "
"rewritten (-43 bytes). None of the four is a new guard - no shipped "
"behaviour changed - they are checks whose DATA could not reach the "
"failure they named, which is the defect this repository has now shipped "
"in six consecutive cards. The last two were found by this round's own "
"battery and are recorded rather than quietly repaired",
all(m[1] for m in MUT[21:24]) and len(MUT) == 24)
check("...and so is the last one, which is not a defeat at all but the EDGE OF "
"THIS CARD: the grid-following loop can still take the pack past this "
"floor, because the floor bounds the peak guard's own forced discharge "