SAFETY-32 fix round: grade the reduction on packs whose floors DIFFER
checks / unit (pull_request) Successful in 4m47s
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:
co-authored by
Claude Opus 5
parent
d9388c81b0
commit
8501e055f2
@@ -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
|
||||
|
||||
@@ -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 "
|
||||
|
||||
Reference in New Issue
Block a user