OPT-09: the weights parameter nothing ever passed, and what it cannot do

`allocator.plan()` has taken a `weights` argument since ALLOC-01 and NOTHING
had ever passed one - main.py called `resolve()` without it and the arbiter
defaulted it to 1.0 per unit - so the site target was split 50/50 regardless
of what either pack could do. Two things now feed it, and they are different
kinds of thing:

  * a `pack` UNIT claim at P_SAFETY that CLOSES a unit's envelope at its own
    `bms_soc_floor` or at 100 % - a PERMISSION, obeyed absolutely, so an empty
    pack is not asked to discharge and its share goes to the sibling;
  * SoC-aware weights by ENERGY headroom in kWh - a PREFERENCE, which
    `_fill()`'s last-resort pass will override the moment the sibling saturates.

Measured (test_packshare.py part 8): at a load one pack cannot cover, the
weighted arm and the shipped arm command the IDENTICAL watts into an empty pack
and deliver the IDENTICAL watts. Only the envelope stops the ask, and what it
buys is not delivery - it is a reported `unmet_w` where the record used to read
impeccable. Weights are a comfort choice for STABILITY and NOT for DELIVERY.

Also: an optional per-entry `capacity_kwh` (schema change, all-or-nothing across
the roster, rule 9 refuses a partial one); per-pack arbitrage sizing, with the
sell side's no-op stated rather than hidden; and a 0 W trade refused by name,
because a 0 W arbitrage claim is a park order that outranks the loop.

SAFETY-42 and this card meet at one hazard from opposite sides - OPT-09 stops
the site ASKING a pack whose SoC says it cannot deliver, SAFETY-42 alarms when a
pack that said it could does not - and its end-to-end fixture moved as a result.
Its finding is unchanged and is now stated on the two quantities its rejected
discriminator actually reads.

Not filed at all at a roster of one: at N=1 every source routes to the site, so
the interval would bound the SITE TARGET, and SAFETY-25's decided Option A
stands untouched on the live install.

⚠️ INVERTER B DOES NOT EXIST. Its 4600 W / 20.48 kWh / 10 % are FROM DATASHEET
AND SERIAL, and `sim/plantsim.py`'s INVERTER_B is inverter A copied
(`measured=False`), so every two-inverter figure here is arithmetic or two
copies of A. 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-06 22:55:20 +02:00
co-authored by Claude Opus 5
parent 3d2999337d
commit f8e501393f
20 changed files with 2318 additions and 81 deletions
+875
View File
@@ -0,0 +1,875 @@
"""Runnable check for OPT-09. `python3 test_packshare.py`
THE CARD IN ONE SENTENCE: the architecture's one free parameter - the allocator's
`weights` - was never passed by anything, so the site target was split 50/50
regardless of what either pack could do, and a pack that could not deliver its
share parked the site with a standing grid error and no alarm.
⚠️ INVERTER B DOES NOT EXIST (expected ~2026-09-12), AND EVERY TWO-UNIT NUMBER
IN THIS FILE IS THEREFORE ARITHMETIC AND NOT HARDWARE. B's figures - 4600 W,
20.48 kWh, 10 % floor - are FROM THE DATASHEET AND THE SERIAL (NOTES.md 1.1b);
A's 5000 W / 15.36 kWh / 10 % are measured. `sim/plantsim.py`'s INVERTER_B is
inverter A copied with `measured=False` on it, so anything driven through a
modelled plant here - part 8 - is TWO COPIES OF A given different numbers. What
this file grades is the SPLIT, which is arithmetic over intervals and weights
and needs no plant at all; where a plant appears it is modelled IN THIS FILE
(`lag_s=0`, obeys within one cycle, bounded by its rating) exactly as
test_alloc.py part 8's is, and the claim is about the control law rather than
about a GW5000S-BP.
⚠️ THE FIXTURE IS THE HARD PART, AND PART 1 IS WHERE IT IS EARNED. Two packs
that differ in ONLY ONE of SoC and capacity cannot tell a SoC-aware weighting
from a capacity-aware one, and two identical packs cannot tell ANY weighting
from any other - every scheme agrees on equal packs, so a weighting bug is
invisible and the checks stay green. That is this repository's recurring defect
(MUTATION.md: "a check whose data cannot reach the failure it names") and part 1
grades the fixture itself against it before anything else is asserted.
⚠️ MUTATING THE MODULES THIS FILE GRADES? PURGE `__pycache__` FIRST - or run
`tools/mutation_run.sh`, which does. `python3 -B` alone does NOT save you.
"""
import ast
import sys
from datetime import datetime, timedelta, timezone
from math import inf
from pathlib import Path
from app import arbitrage, packshare
from app.allocator import plan
from app.arbiter import (
ACTUATOR_SITE, CLAIM_SCOPE, P_LOOP, P_SAFETY, SCOPE_UNIT, Claim, resolve,
)
from app.control import Tuning, compute
from app.peakguard import discharge_permitted
fails = []
def check(name, cond):
print(f" {'ok ' if cond else 'FAIL'} {name}")
if not cond:
fails.append(name)
# --- the two packs, and every number's provenance -----------------------------
A, Bee = "inverter-a", "inverter-b"
A_W, B_W = 5000.0, 4600.0 # A measured; B FROM DATASHEET
A_KWH, B_KWH = 15.36, 20.48 # A measured; B FROM DATASHEET
FLOOR = 10.0 # both, register 0x0560 / datasheet
A_SOC, B_SOC = 20.0, 80.0 # the fixture's own choice - see part 1
def PE(name, soc, kwh, floor=FLOOR, max_w=0.0):
return packshare.PackEnergy(name, soc, floor, kwh, max_w)
PACKS = [PE(A, A_SOC, A_KWH), PE(Bee, B_SOC, B_KWH)]
def envelopes(packs):
"""`{unit: (lo, hi)}` for a roster, the way main.py files them."""
return {p.name: packshare.envelope(p) for p in packs}
def site_claims(packs, target_w, ratings=(A_W, B_W), envelope=True):
"""The claim list main.py builds for one cycle, at a roster of two.
`envelope=False` is WHAT SHIPPED: the `pack` claim is the half of this card
that constrains, so a baseline that kept it would be comparing OPT-09
against itself.
"""
out = []
for p, rating in zip(packs, ratings):
out.append(Claim.limit("supervised", P_SAFETY, -rating, rating, "max_w",
actuator=p.name))
env = packshare.envelope(p)
if envelope and env.binding:
out.append(Claim.limit("pack", P_SAFETY, env.lo, env.hi, env.why,
actuator=p.name))
out.append(Claim.set("loop", P_LOOP, target_w, "grid",
actuator=ACTUATOR_SITE))
return out
def cycle(packs, target_w, weights="soc", ratings=(A_W, B_W), envelope=True):
"""One `resolve()`, the way main.py calls it. `weights` is the variable.
`weights="shipped"` reverts BOTH halves - no weighting and no envelope -
which is the baseline every "before this card" line below is measured
against.
"""
if weights == "soc":
w = lambda t: packshare.weights(packs, t) # noqa: E731 - OPT-09
elif weights in ("equal", "shipped"):
w = None
envelope = envelope and weights != "shipped"
else:
w = weights
return resolve(site_claims(packs, target_w, ratings, envelope),
units=tuple(p.name for p in packs), weights=w, tick=1)
# ==============================================================================
print("1. THE FIXTURE, GRADED BEFORE ANYTHING IS ASSERTED WITH IT")
# Four weighting schemes anybody might write. On the RIGHT fixture they give
# four different splits; on a wrong one they collapse into each other and every
# check downstream is green whatever the code does.
def split(weights, target=1000.0, packs=None):
r = plan(target, {p.name: _B(-A_W, A_W) for p in (packs or PACKS)},
weights=weights)
assert r.ok, r.refused
return r.shares
class _B:
"""One unit's interval, the way ARB-03 stage 1 hands it over."""
def __init__(self, lo=-inf, hi=inf):
self.lo, self.hi = lo, hi
self.gate = self.pinned_w = self.pinned_by = None
self.contradiction = False
def schemes(packs, ratings=(A_W, B_W)):
"""The four candidate weightings, as `{name: weights dict}`."""
return {
"equal": {p.name: 1.0 for p in packs},
"power": {p.name: r for p, r in zip(packs, ratings)},
"soc only": {p.name: max(0.0, p.soc - p.bms_floor) for p in packs},
"energy": packshare.weights(packs, 1000.0),
}
# ⚠️ THE PRECONDITION, WRITTEN BEFORE THE FIXTURE IS USED FOR ANYTHING. A
# `weights()` that answered None on this roster would make every scheme below
# collapse to `equal` - or, as an earlier draft found, take this file down with a
# TypeError instead of a FAIL. A mutation that crashes is a weaker gate than one
# that fails a named check, so the named check is written first.
check("PRECONDITION: `packshare.weights()` HAS AN OPINION about this roster - "
"both packs state a capacity and a reading, so the answer is a mapping "
"over both units and not the None that means 'weigh everything 1.0'",
isinstance(packshare.weights(PACKS, 1000.0), dict)
and set(packshare.weights(PACKS, 1000.0)) == {A, Bee}
and set(packshare.weights(PACKS, -1000.0)) == {A, Bee})
_sch = schemes(PACKS)
_splits = {k: split(v) for k, v in _sch.items()}
for _k, _v in _splits.items():
print(f" {_k:<9} weights "
+ ", ".join(f"{u}={_sch[_k][u]:g}" for u in sorted(_sch[_k]))
+ " -> " + ", ".join(f"{u}{s:+d}" for u, s in sorted(_v.items())))
check("⚠️ THE FIXTURE SEPARATES ALL FOUR SCHEMES. Two packs at 20 %/15.36 kWh "
"and 80 %/20.48 kWh - DIFFERENT IN BOTH SoC AND CAPACITY - give four "
"distinct splits of the same 1000 W, so a weighting bug has somewhere to "
"show. This is AC 2, and it is the check the other parts rest on",
len({tuple(sorted(v.items())) for v in _splits.values()}) == 4)
_ident = [PE(A, 50.0, A_KWH), PE(Bee, 50.0, A_KWH)]
_ident_splits = {k: split(v, packs=_ident)
for k, v in schemes(_ident, (A_W, A_W)).items()}
print(" the SAME four schemes on two IDENTICAL packs (50 %, 15.36 kWh, "
"5000 W each): "
+ "; ".join(f"{k}: " + ", ".join(f"{u}{s:+d}" for u, s in sorted(v.items()))
for k, v in _ident_splits.items()))
check("...and the counterfactual that says why the fixture had to be built this "
"way: on two IDENTICAL packs every one of the four schemes gives the SAME "
"split, so a fixture of equal packs cannot distinguish the shipped "
"50/50 from any weighting whatever. That is the defect this repository "
"has found eleven times, and it is why the two packs here differ in both "
"dimensions rather than in one",
len({tuple(sorted(v.items())) for v in _ident_splits.values()}) == 1)
_same_soc = [PE(A, 50.0, A_KWH), PE(Bee, 50.0, B_KWH)]
_same_cap = [PE(A, A_SOC, A_KWH), PE(Bee, B_SOC, A_KWH)]
check("...and ONE dimension is not enough either, in EITHER direction, which "
"is the sharper half. Two packs at the SAME SoC cannot tell an "
"energy-aware weighting from a SoC-only one being wrong about capacity - "
"`soc only` collapses onto `equal` while `energy` follows the capacity "
"alone. Two packs of the SAME CAPACITY cannot tell `energy` from "
"`soc only` AT ALL: with capacity constant the energy headroom IS the SoC "
"headroom scaled, so the two give the identical split and a capacity bug "
"is invisible. Only differing in BOTH separates them",
split(schemes(_same_soc)["soc only"], packs=_same_soc)
== split(schemes(_same_soc)["equal"], packs=_same_soc)
!= split(schemes(_same_soc)["energy"], packs=_same_soc)
and split(schemes(_same_cap)["energy"], packs=_same_cap)
== split(schemes(_same_cap)["soc only"], packs=_same_cap)
and split(schemes(PACKS)["energy"]) != split(schemes(PACKS)["soc only"]))
# ==============================================================================
print()
print("2. THE ENVELOPE: what a pack's own state of charge FORBIDS")
_empty = PE(A, FLOOR, A_KWH) # exactly ON its own cut-out
_below = PE(A, 5.0, A_KWH)
_full = PE(Bee, 100.0, B_KWH)
_mid = PE(Bee, 55.0, B_KWH)
for _p in (_empty, _below, _full, _mid):
_e = packshare.envelope(_p)
print(f" {_p.name} at {_p.soc:g} % -> "
f"[{_e.lo:g}, {_e.hi:g}] {_e.why or '(no opinion)'}")
check("a pack AT its own BMS cut-out is not asked to discharge: `hi` closes at "
"0 W. ⚠️ AT, not below - `discharge_permitted()`'s boundary is `<=`, "
"because a pack sitting exactly on the floor has already spent everything "
"there was, and permitting one more cycle there makes the floor mean 'one "
"cycle below the floor'",
packshare.envelope(_empty).hi == 0.0
and packshare.envelope(_below).hi == 0.0),
check("...and it may still be CHARGED - `lo` is untouched. A closed envelope is "
"a direction, not a park: parking the pack would be a claim about what it "
"may do in the direction that would fix it",
packshare.envelope(_empty).lo == -inf)
check("a pack at 100 % is not asked to charge: `lo` closes at 0 W, and it may "
"still discharge",
packshare.envelope(_full).lo == 0.0
and packshare.envelope(_full).hi == inf)
check("a pack with room in both directions gets NO OPINION - `(-inf, +inf)`, "
"`binding` False - and main.py files no claim for it. A limit filed on "
"every unit every tick would put an open interval in the arbiter's "
"considered list forever and bury the one cycle that means something",
packshare.envelope(_mid) == packshare.PackEnvelope()
and not packshare.envelope(_mid).binding
and packshare.envelope(_empty).binding)
check("⚠️ AND THE DISCHARGE HALF ASKS `peakguard.discharge_permitted()` RATHER "
"THAN WRITING `soc <= floor` A THIRD TIME. It is the predicate SAFETY-32 "
"made public and SAFETY-42's `account()` already reuses; a third "
"transcription is two future disagreements about what it means - and in "
"particular the BOUNDARY (`<=`) is that function's, so a change to it "
"moves both. Graded by agreement over the whole percent range, on two "
"different floors, at every readable input",
all((packshare.envelope(PE(A, s, A_KWH, f)).hi == 0.0)
== (discharge_permitted(s, f) is not None)
for f in (10.0, 25.0) for s in [x / 2.0 for x in range(0, 201)]))
check("⚠️ AND A MISSING READING OR A MISSING FLOOR WITHHOLDS THE CLOSURE, NOT "
"THE DISCHARGE - THE OPPOSITE DIRECTION FROM `discharge_permitted()`'s "
"OWN, AND DELIBERATELY SO. That predicate decides whether to SPEND a pack "
"for money, where a full battery is exactly the value that would disarm "
"the floor. This decides whether to take a unit's share AWAY and hand it "
"to a sibling, and an absent measurement is not evidence that a battery "
"is flat: closing on it would park a healthy unit's discharge on every "
"tick for as long as its sensor was missing. Both absences have a "
"stronger answer arriving first - `inputs_ok` stops the whole site on a "
"missing SoC, and rule 8 plus the schema refuse an entry with no "
"`bms_soc_floor` at startup",
not packshare.envelope(PE(A, None, A_KWH)).binding
and not packshare.envelope(PE(A, 5.0, A_KWH, None)).binding
and packshare.envelope(PE(A, 5.0, A_KWH, 10.0)).hi == 0.0)
check("⚠️ AND THE FLOOR IS THE PACK'S OWN `bms_soc_floor`, NOT "
"`peakguard.pack_floor_pct()`'s conjunction with `peak_soc_floor`. The "
"EMS floor bounds the PEAK GUARD ONLY (SAFETY-25, decided Option A on "
"2026-09-02): ordinary self-consumption discharges straight past it, and "
"closing an envelope at it would turn an operator preference into a hard "
"stop on the grid-following loop - the memo's DEFERRED Option B, built by "
"accident. A pack at 15 % with a 10 % cut-out is NOT closed here, though "
"a 20 % `peak_soc_floor` would have closed it",
not packshare.envelope(PE(A, 15.0, A_KWH, 10.0)).binding
and discharge_permitted(15.0, 20.0) is not None)
# ==============================================================================
print()
print("3. THE WEIGHTS: energy headroom, and the direction it is measured in")
_dis = packshare.weights(PACKS, +1000.0)
_chg = packshare.weights(PACKS, -1000.0)
print(f" discharging: " + ", ".join(f"{u}={v:.3f} kWh" for u, v in sorted(_dis.items())))
print(f" charging: " + ", ".join(f"{u}={v:.3f} kWh" for u, v in sorted(_chg.items())))
check("discharging, the weight is the energy ABOVE the pack's own cut-out: "
"15.36 kWh x (20-10) % = 1.536 kWh for A, 20.48 kWh x (80-10) % = "
"14.336 kWh for B",
abs(_dis[A] - 1.536) < 1e-9 and abs(_dis[Bee] - 14.336) < 1e-9)
check("charging, it is the room BELOW full: 15.36 x 80 % = 12.288 kWh for A, "
"20.48 x 20 % = 4.096 kWh for B",
abs(_chg[A] - 12.288) < 1e-9 and abs(_chg[Bee] - 4.096) < 1e-9)
check("⚠️ SO THE TWO DIRECTIONS DISAGREE ABOUT WHICH PACK SHOULD CARRY MORE - "
"B by 9:1 on the way out, A by 3:1 on the way in - which is why the "
"weighting cannot be computed before the site target is known and why "
"`resolve()` takes a CALLABLE. A direction-free weighting is not a "
"compromise between these two, it is wrong in one of them on every tick",
(_dis[Bee] > _dis[A]) and (_chg[A] > _chg[Bee]))
check("⚠️ AND IT IS ENERGY, NOT POWER, WHICH IS THE WHOLE OF DELIVERABLE 2. The "
"packs are asymmetric in OPPOSITE directions - B has ~33 % more energy "
"and ~8 % less power - so a power-rating weighting hands the discharge to "
"A (5000 > 4600) and an energy weighting hands it to B. The question the "
"split answers is 'which pack can sustain its share for the hour'",
A_W > B_W and B_KWH > A_KWH
and split(_sch["power"])[A] > split(_sch["power"])[Bee]
and split(_dis)[Bee] > split(_dis)[A])
check("a roster where ANY pack cannot state its headroom gets NO OPINION for "
"the WHOLE roster - None, i.e. 1.0 each, which is what shipped. A pack "
"weighed in kWh against a sibling weighing a dimensionless 1.0 is not a "
"comparison, and half a weighting is worse than none",
packshare.weights([PE(A, A_SOC, None), PE(Bee, B_SOC, B_KWH)], 1000.0) is None
and packshare.weights([PE(A, None, A_KWH), PE(Bee, B_SOC, B_KWH)], 1000.0) is None
and packshare.weights([PE(A, A_SOC, A_KWH, None), PE(Bee, B_SOC, B_KWH)],
1000.0) is None)
check("...and so does a roster on which NOBODY has headroom in this direction. "
"All-zero weights would drop the whole roster into `_fill()`'s LAST-RESORT "
"pass, where the shares come out equal anyway and every plan reads 'last "
"resort: the preferred units were full' - a true sentence about the wrong "
"thing",
packshare.weights([PE(A, FLOOR, A_KWH), PE(Bee, FLOOR, B_KWH)],
+1000.0) is None
and packshare.weights([PE(A, 100.0, A_KWH), PE(Bee, 100.0, B_KWH)],
-1000.0) is None)
check("...and at a roster of one there is no opinion to have: `plan()` is a "
"pass-through at N<=1 and a weight there is arithmetic nobody reads",
packshare.weights([PE(A, A_SOC, A_KWH)], 1000.0) is None
and packshare.weights([], 1000.0) is None)
check("every weight is non-negative and finite, which `allocator.plan()` "
"REFUSES to accept otherwise - a pack below its own floor is at zero "
"usable energy, not at a negative preference",
all(v >= 0.0 for v in packshare.weights(
[PE(A, 2.0, A_KWH), PE(Bee, B_SOC, B_KWH)], 1000.0).values()))
# ==============================================================================
print()
print("4. AC 1, END TO END: a pack at its floor is not allocated discharge")
_at_floor = [PE(A, FLOOR, A_KWH), PE(Bee, B_SOC, B_KWH)]
_r_now = cycle(_at_floor, 1500.0, weights="shipped")
_r_opt = cycle(_at_floor, 1500.0)
print(f" shipped (no weights, no envelope): {_r_now.allocation.explain()}")
print(f" OPT-09: {_r_opt.allocation.explain()}")
check("⚠️ AC 1. A pack sitting on its own BMS cut-out is allocated NO "
"discharge, and its share moves to the sibling WITHIN THAT SIBLING'S "
"ENVELOPE. Before this card the same cycle handed it half the site "
"target - 750 W into a battery that answers no watts to any command",
_r_opt.shares[A] == 0 and _r_opt.shares[Bee] == 1500
and _r_opt.allocation.unmet_w == 0)
check("...and the shipped behaviour really was the other one, so the line "
"above is not vacuous: an equal split of 1500 W is 750/750, and 750 W is "
"what a pack at its cut-out was being told to produce",
_r_now.shares == {A: 750, Bee: 750})
_plan_a = next(p for p in _r_opt.allocation.plans if p.unit == A)
print(f" {_plan_a.explain()}")
check("...and the RECORD says why, in the line an operator is already reading. "
"`UnitPlan.within_envelope` is asked rather than assumed, and SAFETY-28's "
"write gate refuses a record that contradicts itself",
_plan_a.within_envelope and _plan_a.share_w == 0
and _plan_a.hi == 0.0 and _r_opt.allocated)
check("a pack at 100 % is not allocated a CHARGE either, and the sibling takes "
"the whole of it - the same sentence in the other direction",
cycle([PE(A, 100.0, A_KWH), PE(Bee, B_SOC, B_KWH)], -1200.0).shares
== {A: 0, Bee: -1200})
check("⚠️ AND THE SIGN-COHERENCE REFUSAL IS NOT TRIPPED BY ANY OF THIS. One "
"envelope closed at 0 and the other open cannot produce two shares of "
"opposite sign, because the fill moves every share in ONE direction from "
"a base that is still 0 for the closed unit. Circulating power stays "
"inexpressible",
all(cycle([PE(A, s, A_KWH), PE(Bee, 100.0 - s, B_KWH)], t).allocated
for s in (0.0, FLOOR, 50.0, 100.0) for t in (-2000.0, 0.0, 2000.0)))
check("a unit whose envelope is closed is still IN the split - it is not "
"dropped. `dropped` means the master is stale and the record says so; a "
"full pack is present, commandable, and told 0 W",
not cycle(_at_floor, 1500.0).allocation.dropped
and A in cycle(_at_floor, 1500.0).shares)
# ==============================================================================
print()
print("5. AC 3: unmet_w when NEITHER unit can take the remainder")
_both = [PE(A, FLOOR, A_KWH), PE(Bee, 8.0, B_KWH)]
_r_both = cycle(_both, 1500.0)
print(f" both packs at or under their cut-out: {_r_both.allocation.explain()}")
check("⚠️ AC 3. With every pack at or under its own cut-out the discharge has "
"nowhere to go: both shares are 0 and the whole 1500 W is reported as "
"`unmet_w`. Not refused - `Allocation` is explicit that fusing `unmet_w` "
"into `ok` would let one saturated inverter park the site - and not "
"silently absorbed either",
_r_both.allocated and _r_both.shares == {A: 0, Bee: 0}
and _r_both.allocation.unmet_w == 1500)
check("...and the identity still holds over integers: shares + unmet == sum_w, "
"which is what makes `unmet_w` a measurement rather than an estimate",
sum(_r_both.shares.values()) + _r_both.allocation.unmet_w
== _r_both.allocation.sum_w)
check("...and it is VISIBLE: `explain()` prints it in the line the action log "
"carries, and main.py edge-logs the transition (`_unmet_last`). Before "
"this card nothing anywhere reported a watt that could not be placed",
"UNMET" in _r_both.allocation.explain())
_partial = [PE(A, FLOOR, A_KWH), PE(Bee, B_SOC, B_KWH)]
check("...and a partial shortfall is reported partially rather than all-or- "
"nothing: one pack closed and the sibling's own rating short of the ask "
"places what it can and names the rest",
cycle(_partial, 6000.0).allocation.unmet_w == 6000 - int(B_W))
# ==============================================================================
print()
print("6. AC 4: at N=1 the allocation is BYTE-IDENTICAL to what shipped")
# ⚠️ THE WHOLE OF AC 4 IS THAT `main.py` DOES NOT FILE THE `pack` CLAIM AT N=1,
# and the reason is `actuator_for()`: at one inverter every source routes to the
# SITE, so a unit interval filed there would bound THE SITE TARGET rather than a
# share. That is a behaviour change on the live single-inverter install and it is
# SAFETY-25's decided Option A. What this part grades is the arithmetic on both
# sides of that `if`.
_one = [PE(A, FLOOR, A_KWH)]
_n1_claims = [Claim.limit("supervised", P_SAFETY, -A_W, A_W, "max_w"),
Claim.set("loop", P_LOOP, 1234.5, "grid")]
_n1_now = resolve(_n1_claims, units=(A,), tick=1)
_n1_opt = resolve(_n1_claims, units=(A,),
weights=lambda t: packshare.weights(_one, t), tick=1)
print(f" N=1 shipped: {_n1_now.shares} OPT-09: {_n1_opt.shares}")
check("the pass-through is untouched: the share IS the site target, as a float, "
"unrounded and unclamped, with or without the weights argument",
_n1_now.shares == _n1_opt.shares == {A: 1234.5}
and _n1_opt.allocation.passthrough)
check("...and that holds across the whole range, sign for sign, because "
"`weights()` answers None at a roster of one and `plan()` never reaches "
"its weighted path there anyway",
all(resolve([Claim.limit("supervised", P_SAFETY, -A_W, A_W, "max_w"),
Claim.set("loop", P_LOOP, t, "grid")], units=(A,),
weights=lambda x: packshare.weights(_one, x),
tick=1).shares
== resolve([Claim.limit("supervised", P_SAFETY, -A_W, A_W, "max_w"),
Claim.set("loop", P_LOOP, t, "grid")], units=(A,),
tick=1).shares
for t in (-5000.0, -1999.5, -1.0, 0.0, 1.0, 999.25, 5000.0)))
check("⚠️ AND A CALLABLE THAT ANSWERS None IS THE SAME AS NO ARGUMENT AT ALL, "
"at every roster size. That is what makes the new parameter inert on any "
"install that has not stated its capacities",
resolve(site_claims(PACKS, 1000.0), units=(A, Bee),
weights=lambda t: None, tick=1).shares
== resolve(site_claims(PACKS, 1000.0), units=(A, Bee), tick=1).shares)
# ==============================================================================
print()
print("7. AC 5: verified BY BREAKING IT - equal weights on unequal packs")
# ⚠️ THE CARD ASKS FOR THIS EXPLICITLY: "force equal weights on unequal packs and
# observe red". The point is that the checks above are not passing for a reason
# unrelated to the weighting - so here the SAME cycles are run with the weighting
# reverted to what shipped, and what they produce is printed beside what OPT-09
# produces.
_cases = [
("1500 W out, A on its cut-out", _at_floor, 1500.0),
("1200 W in, A full", [PE(A, 100.0, A_KWH), PE(Bee, B_SOC, B_KWH)], -1200.0),
("1000 W out, both with room", PACKS, 1000.0),
]
_broken = []
for _why, _pk, _t in _cases:
_o, _e = cycle(_pk, _t), cycle(_pk, _t, weights="shipped")
_broken.append((_why, _o.shares, _e.shares))
print(f" {_why:<32} OPT-09 "
+ ", ".join(f"{u}{s:+d}" for u, s in sorted(_o.shares.items()))
+ " equal-weights "
+ ", ".join(f"{u}{s:+d}" for u, s in sorted(_e.shares.items())))
check("every one of these cycles gives a DIFFERENT answer with BOTH halves "
"reverted to what shipped - equal weights, no envelope - so each check "
"above is separated from its own mutation rather than passing for a "
"reason unrelated to the weighting",
all(o != e for _w, o, e in _broken))
_env_only = cycle(_at_floor, 1500.0, weights="equal")
print(f" and with the ENVELOPE alone (equal weights, `pack` claim filed): "
+ ", ".join(f"{u}{s:+d}" for u, s in sorted(_env_only.shares.items())))
check("...and the two halves are separated from EACH OTHER as well: on a pack "
"sitting at its cut-out the envelope alone already gives the whole ask to "
"the sibling, because a closed `cap` is obeyed absolutely, so this case "
"grades the ENVELOPE. Case three - both packs with room, nothing closed - "
"is the one that can only be the WEIGHTS",
_env_only.shares == {A: 0, Bee: 1500}
and _broken[2][1] != _broken[2][2])
check("...and the third case is the one that isolates the WEIGHTS from the "
"ENVELOPE: both packs have room, no interval is closed, nothing is "
"refused - and the split still moves from 500/500 to 97/903 because the "
"energy headroom says so",
_broken[2][1] == {A: 97, Bee: 903} and _broken[2][2] == {A: 500, Bee: 500})
# ==============================================================================
print()
print("8. ⚠️ WHAT THE WEIGHTS CANNOT DO, WHICH IS THE HALF THAT IS EASY TO "
"OVER-READ")
# ARCH-01: the gain seen at the meter is `sum(a_i * g_i)`, a CONVEX COMBINATION
# of the per-unit gains, so a bad split cannot DESTABILISE the site. That says
# nothing about whether the site can REGULATE. A unit at its energy limit has
# g_i = 0; the combination is still inside [min g, max g] and the load it was
# asked to carry simply does not move.
RATING = 2000.0 # per unit, so the SITE rail is 4000 W
def close_loop(load_w, mode, cycles=200, rating=RATING):
"""One integrator on the meter, two units, each told its OWN share.
`mode` is what varies. "shipped" is equal weights and no envelope; "weights"
is OPT-09's preference with the envelope left open; "envelope" is OPT-09
entire. Unit B is AT ITS FLOOR throughout and therefore answers 0 W to any
discharge command - the benign case `delivery.py` measures as
indistinguishable, IN THE POWER DOMAIN, from a wedged box.
⚠️ `max_w` IS THE SITE RAIL - THE SUM OF THE UNIT RATINGS - because that is
what main.py builds `Tuning` with (`max_w=self.site_max_w`). Handing the
loop one unit's rating would model an add-on that does not exist and would
hide the wind-up this part is about: the first draft of this check did that
and reported a 500 W standing error where the shipped law reaches zero.
"""
tuning = Tuning(gain=1.0, slew_w=1000.0, max_w=2.0 * rating)
prev, i_w, sat = 0.0, None, 0
power = [0.0, 0.0]
grid = load_w
packs = [PE(A, 50.0, A_KWH), PE(Bee, FLOOR, B_KWH)]
trace = []
for _c in range(cycles):
d = compute(prev, grid, sum(power), tuning, sat, i_w)
prev, i_w, sat = d.target_w, d.i_w, d.sat_count
bounds = {p.name: _B(-rating, rating) for p in packs}
if mode == "envelope":
for p in packs:
e = packshare.envelope(p)
bounds[p.name] = _B(max(-rating, e.lo), min(rating, e.hi))
w = None if mode == "shipped" else packshare.weights(packs, d.target_w)
r = plan(d.target_w, bounds, weights=w)
assert r.ok, r.refused
# THE MODELLED PLANT. A obeys within one cycle inside its rating; B is at
# its floor and produces NOTHING in the discharge direction whatever it
# is told - which is `plantsim.Plant.limit()`'s own answer for a pack at
# its floor, and is why SAFETY-42 cannot read this case from power alone.
power = [max(-rating, min(rating, float(r.shares[A]))),
min(0.0, float(r.shares[Bee]))]
grid = load_w - sum(power)
trace.append((round(d.target_w, 1), dict(r.shares), round(grid, 1),
r.unmet_w, d.reason))
return trace
_MODES = ("shipped", "weights", "envelope")
_row = {}
for _load in (1500.0, 3000.0):
for _m in _MODES:
_t = close_loop(_load, _m)[-1]
_row[(_load, _m)] = _t
print(f" load {_load:.0f} W, B at its floor, {_m:<9} "
f"site target {_t[0]:+.0f} W, shares "
+ ", ".join(f"{u}{v:+d}" for u, v in sorted(_t[1].items()))
+ f", grid {_t[2]:+.0f} W, unmet {_t[3]:+d} W, {_t[4]}")
check("AT A LOAD THE HEALTHY PACK CAN COVER, ALL THREE ARMS REGULATE TO ZERO "
"GRID ERROR - and the shipped one is NOT thereby fine. It gets there by "
"winding the SITE TARGET to 3000 W for a 1500 W load and standing a "
"+1500 W command on a battery sitting on its own cut-out, permanently "
"saturation-frozen; OPT-09 reaches the same meter reading with a 1500 W "
"target, nothing standing on the empty pack, and the loop in its "
"deadband. The improvement here is not watts, it is that the site has "
"stopped lying to itself - a loop wound to twice the load has no headroom "
"left for the next one",
abs(_row[(1500.0, "shipped")][2]) < 1.0
and abs(_row[(1500.0, "weights")][2]) < 1.0
and abs(_row[(1500.0, "envelope")][2]) < 1.0
and _row[(1500.0, "shipped")][0] == 3000.0
and _row[(1500.0, "shipped")][1][Bee] == 1500
and _row[(1500.0, "shipped")][4] == "saturated-freeze"
and _row[(1500.0, "envelope")][0] == 1500.0
and _row[(1500.0, "envelope")][1][Bee] == 0)
check("⚠️ AND AT A LOAD IT CANNOT COVER, THE WEIGHTS STOP WORKING ALTOGETHER. "
"The weighted arm and the shipped arm command the SAME +2000 W into the "
"empty pack and deliver the SAME 2000 W, because once the preferred unit "
"is at its cap `_fill()`'s LAST-RESORT PASS hands the remainder to the "
"zero-weighted one - deliberately, so that a preference can never "
"manufacture a standing error at the meter. A weight of 0 is 'please do "
"not', never 'you may not'",
_row[(3000.0, "weights")][1] == _row[(3000.0, "shipped")][1]
== {A: 2000, Bee: 2000}
and _row[(3000.0, "weights")][2] == _row[(3000.0, "shipped")][2])
check("⚠️ AND THE ENVELOPE DOES NOT RESCUE THE DELIVERY EITHER - IT NAMES THE "
"SHORTFALL. Same 2000 W delivered, same 1000 W standing at the meter, "
"because the sibling is already at its rating and there is nowhere for "
"the rest to go; what changes is that the empty pack is commanded 0 W "
"instead of 2000 W and 2000 W of site target is REPORTED as unplaceable "
"instead of appearing to have been placed. THIS IS THE WHOLE OF "
"DELIVERABLE 3: ARCH-01's convex combination guarantees a bad split "
"cannot DESTABILISE the site and says nothing about whether the site can "
"REGULATE. A unit at its energy limit has g_i = 0, `compute()` freezes on "
"the resulting error, and the load DOES NOT SHIFT to the sibling by any "
"mechanism in this add-on",
_row[(3000.0, "envelope")][2] == _row[(3000.0, "weights")][2]
and _row[(3000.0, "envelope")][1] == {A: 2000, Bee: 0}
and _row[(3000.0, "envelope")][3] == 2000
and _row[(3000.0, "weights")][3] == 0)
check("...so: WEIGHTS ARE A COMFORT CHOICE FOR STABILITY AND NOT FOR DELIVERY, "
"and the same sentence stated as arithmetic on one plan rather than as a "
"200-cycle run",
plan(3000.0, {A: _B(-RATING, RATING), Bee: _B(-RATING, RATING)},
weights={A: 1.0, Bee: 0.0}).shares == {A: 2000, Bee: 1000})
check("...and SAFETY-42 is where the case this card CANNOT reach is handled. A "
"WEDGED unit - one that accepts its share and does not produce it - has a "
"healthy state of charge, so no envelope closes and no weight moves; that "
"module detects it, latches and notifies, and deliberately does not act. "
"OPT-09 stops the site ASKING a pack whose own SoC says it cannot "
"deliver; SAFETY-42 alarms when a pack that said it could does not. Same "
"hazard, opposite sides, and the seam is the state of charge",
not packshare.envelope(PE(Bee, 55.0, B_KWH)).binding)
# ==============================================================================
print()
print("9. PER-PACK ARBITRAGE SIZING")
# ⚠️ THE FUNCTION UNDER TEST IS THE SHIPPED ONE, NOT A TRANSCRIPTION OF IT.
# `Controller.trade_power_w()` is one line - the roster view handed to
# `packshare.trade_power_w()` - and this calls that same function. An earlier
# draft of this part re-implemented the two sums here, which is the defect this
# repository keeps finding: the copy that drifts is always the grading one.
trade_power_w = packshare.trade_power_w
_near_full = [PE(A, 50.0, A_KWH, max_w=A_W), PE(Bee, 95.0, B_KWH, max_w=B_W)]
_c, _d = trade_power_w(_near_full, 20.0, 90.0)
print(f" A 50 %, B 95 %, arbitrage bounds 20/90 %: charge_w {_c:.0f} W, "
f"discharge_w {_d:.0f} W (site rating {A_W + B_W:.0f} W)")
check("⚠️ THE BUY SIDE IS THE ONE THAT BITES. With B over the arbitrage ceiling "
"the trade is sized at A's rating alone, where before this card it was "
"the sum of the ratings - the site would buy at 9600 W with one pack able "
"to absorb it, and the watts that could not be placed would come back as "
"`unmet_w` for the site integrator to chase for the rest of the hour",
_c == A_W and _c < A_W + B_W)
check("...and the sell side is unchanged here, because B is well above the "
"floor. Sizing is a REDUCTION of the ask and never an increase",
_d == A_W + B_W)
check("⚠️ AND THE SELL SIDE IS A NO-OP ON EVERY ROSTER TODAY, WHICH IS SAID "
"RATHER THAN LEFT FOR A REVIEWER TO FIND. `arbitrage.decide()` is handed "
"`pack_floor_soc` - the EMPTIEST pack - so a sell is refused outright "
"whenever ANY pack is at or under the floor, i.e. in exactly the states "
"where this sum could be smaller than the site rating. The arithmetic is "
"graded here; it becomes reachable the day that gate becomes per-pack, "
"and moving it is NOT this card's - it would let a sell be sized off the "
"fullest pack while the weights, which are only a preference, decided "
"where the watts went",
trade_power_w([PE(A, 15.0, A_KWH, max_w=A_W),
PE(Bee, B_SOC, B_KWH, max_w=B_W)], 20.0, 90.0)[1] == B_W
and min(15.0, B_SOC) <= 20.0)
check("a pack whose SoC did not read counts on NEITHER side - withholding is "
"the direction that cannot over-size, and `inputs_ok` has already stopped "
"the site by then",
trade_power_w([PE(A, None, A_KWH, max_w=A_W),
PE(Bee, B_SOC, B_KWH, max_w=B_W)], 20.0, 90.0)
== (B_W, B_W))
# ⚠️ AND THE SIZING MUST NEVER REACH `claim()` AS A ZERO, which is a hazard the
# sizing itself created: `arbitrage.claim()`'s own note says a `Claim.set(..., 0)`
# at P_ARBITRAGE is a PARK ORDER that outranks the loop, i.e. it would silently
# disable self-consumption for the whole hour. Graded on `_act()` directly - the
# branch that builds the decision - because a fixture that reached it through a
# real `TariffPlan` would be grading the plan.
_NOW = datetime(2026, 9, 6, 12, 0, tzinfo=timezone.utc)
class _Pt:
def __init__(self, start, price=0.30):
self.start, self.eur_per_kwh, self.provisional = start, price, False
class _Plan:
"""The smallest object `_point_at()` accepts: `plan.read.match(series=...)`."""
def __init__(self, pt):
self.read = self
self._pt = pt
def match(self, series, no_data):
return series(self)
def at(self, when):
return self._pt
class _V:
def __init__(self, cheapest, dearest):
self.spread_eur_kwh, self.required_spread_eur_kwh = 0.20, 0.05
self.cheapest, self.dearest = cheapest, dearest
_here = _Pt(_NOW)
_other = _Pt(_NOW - timedelta(hours=3))
_sell = arbitrage._act(_Plan(_here), _NOW, _V(_other, _here), 60.0,
0.0, 0.0, 20.0, 90.0)
_sell_ok = arbitrage._act(_Plan(_here), _NOW, _V(_other, _here), 60.0,
0.0, 4600.0, 20.0, 90.0)
_buy = arbitrage._act(_Plan(_here), _NOW, _V(_here, _other), 60.0,
0.0, 0.0, 20.0, 90.0)
_buy_ok = arbitrage._act(_Plan(_here), _NOW, _V(_here, _other), 60.0, 5000.0,
0.0, 20.0, 90.0)
print(f" sized at 0 W: sell -> {_sell.action}, buy -> {_buy.action}; "
f"sized at a rating: sell -> {_sell_ok.action}, buy -> {_buy_ok.action}")
check("⚠️ A TRADE NOTHING CAN TAKE IS NOT A TRADE. A 0 W sizing IDLES on both "
"sides rather than becoming a 0 W claim, because a `Claim.set(..., 0.0)` "
"at P_ARBITRAGE is a PARK ORDER that outranks the loop - the failure "
"`arbitrage.claim()`'s own note is written against. The sizing above can "
"never be zero on a tick that trades TODAY, and this makes that "
"structural instead of a coincidence of the sell gate",
not _sell.trading and not _buy.trading
and arbitrage.claim(_sell) is None and arbitrage.claim(_buy) is None
and arbitrage.IDLE_NO_SIZE in _sell.reason
and arbitrage.IDLE_NO_SIZE in _buy.reason)
check("...and the SAME two calls with a real sizing DO trade, so the check "
"above is separated from a fixture that simply never reaches the branch",
_sell_ok.action == arbitrage.DISCHARGE
and _buy_ok.action == arbitrage.CHARGE
and _sell_ok.power_w == 4600.0 and _buy_ok.power_w == -5000.0)
# ==============================================================================
print()
print("10. THE CLAIM, THE SCOPE, AND WHAT THIS FILE CANNOT SEE")
check("the `pack` source is classified in `CLAIM_SCOPE` as a UNIT claim, so "
"`scope_violations()` complains if it is ever filed on the site while the "
"site has named units. It is about one battery in one cupboard, exactly "
"as `device`'s rating is about one box",
CLAIM_SCOPE[packshare.SOURCE] == SCOPE_UNIT)
_sc = resolve(site_claims(_at_floor, 1500.0), units=(A, Bee),
weights=lambda t: packshare.weights(_at_floor, t), tick=1)
check("...and a real cycle files it without a scope complaint",
_sc.scope_errors == [])
_mis = resolve([Claim.limit("pack", P_SAFETY, -inf, 0.0, "misfiled",
actuator=ACTUATOR_SITE),
Claim.limit("supervised", P_SAFETY, -A_W, A_W, "max_w",
actuator=A),
Claim.set("loop", P_LOOP, 1000.0, "grid")],
units=(A, Bee), tick=1)
check("...and misfiling it ON THE SITE is caught and REFUSED, not consumed. A "
"per-pack interval in the site list is the cross-inverter `lo > hi` "
"ARB-03's partition exists to make inexpressible",
_mis.scope_errors and not _mis.allocated)
# ⚠️ AND THE ARGUMENT IS ACTUALLY PASSED, WHICH IS THE WHOLE CARD AND WHICH
# NOTHING BEHAVIOURAL IN THIS FILE CAN SEE. Every cycle above builds its own
# claim list and calls `resolve()` itself, so deleting `weights=` from main.py's
# ONE call site would leave this entire file green - which is exactly the state
# release/1.0 was in, for the whole life of the parameter. It is pinned on
# main.py's own AST, the way test_peakguard.py part 12i pins the arguments of
# `arbitrage.decide()`, and for the same reason: a call site that stopped
# passing it is invisible to every fixture.
_main_src = (Path(__file__).resolve().parent / "app" / "main.py").read_text(
encoding="utf-8")
_resolve_calls = [n for n in ast.walk(ast.parse(_main_src))
if isinstance(n, ast.Call) and isinstance(n.func, ast.Name)
and n.func.id == "resolve"]
_w_arg = [ast.unparse(k.value) for c in _resolve_calls for k in c.keywords
if k.arg == "weights"]
print(f" main.py has {len(_resolve_calls)} `resolve(` call site(s); "
f"weights= is {_w_arg}")
check("⚠️ MAIN.PY ACTUALLY PASSES `weights=`, AND IT PASSES THE METHOD RATHER "
"THAN A DICT. THIS IS THE CARD: the parameter has existed since ALLOC-01 "
"and nothing ever filled it, so the split was 50/50 whatever either pack "
"could do - and no fixture anywhere could see that, because every check "
"calls `resolve()` for itself. A method, not a dict, because the "
"weighting is direction-dependent and the direction is stage 2's output",
len(_resolve_calls) == 1 and _w_arg == ["self.pack_weights"])
# ⚠️ AC 3 SAYS `unmet_w` MUST BE **VISIBLE**, AND `Allocation.explain()` IS ONLY
# HALF OF THAT. The other half is main.py putting it in the action log, which no
# check in this file can reach without building a whole Controller - so it is
# pinned STATICALLY, on `Controller.cycle()`'s own AST, the way test_peakguard.py
# part 10f pins the claim sources. What this grades is that the branch EXISTS,
# reads the allocation's own `unmet_w`, is EDGE-TRIGGERED against the previous
# cycle's value, and logs. What it does NOT grade is the sentence it logs or
# that a real cycle reaches it; that is stated here rather than implied.
_cycle_src = next(
f for cls in ast.walk(ast.parse(
(Path(__file__).resolve().parent / "app" / "main.py")
.read_text(encoding="utf-8")))
if isinstance(cls, ast.ClassDef) and cls.name == "Controller"
for f in cls.body
if isinstance(f, (ast.FunctionDef, ast.AsyncFunctionDef)) and f.name == "cycle")
_unmet_ifs = [n for n in ast.walk(_cycle_src) if isinstance(n, ast.If)
and ast.unparse(n.test) == "_unmet != self._unmet_last"]
_unmet_src = [ast.unparse(n) for n in ast.walk(_cycle_src)
if isinstance(n, ast.Assign) and len(n.targets) == 1
and ast.unparse(n.targets[0]) == "_unmet"]
print(f" main.py computes `_unmet` as {_unmet_src} and branches on it "
f"{len(_unmet_ifs)} time(s)")
check("...and main.py REPORTS an unplaceable watt rather than only recording "
"it: `Controller.cycle()` reads `self.allocation.unmet_w`, compares it "
"against the previous cycle's value - edge-triggered, because an empty "
"pack stays empty and a line a second would flush the 40-row action log "
"of the context that explains it - and calls `log_event` on the change. "
"⚠️ THIS IS A SHAPE PIN: it says the branch exists and what it reads, "
"never what it says or that a live cycle reaches it",
len(_unmet_ifs) == 1
and len(_unmet_src) == 1 and "unmet_w" in _unmet_src[0]
and any(isinstance(n, ast.Call)
and ast.unparse(n.func) == "self.log_event"
for n in ast.walk(_unmet_ifs[0])))
print()
print(" ⚠️ WHAT THIS FILE CANNOT SEE, said plainly:")
print(" * NOTHING HERE IS EVIDENCE ABOUT INVERTER B. It does not exist;")
print(" every B figure is datasheet, and part 8's plant is modelled in")
print(" this file - two copies of A given different numbers.")
print(" * IT DOES NOT GRADE THE TAPER. `spare_kwh()` says a pack at 95 %")
print(" has room; a real pack in constant-voltage taper takes far less")
print(" than it is told for tens of minutes, and this add-on has no")
print(" per-pack charge ceiling to describe that with. `delivery.py`")
print(" says the same thing and calls it a card - it is why SAFETY-42")
print(" is armed on discharge shares only.")
print(" * THE CHARGE ENVELOPE CLOSES AT 100 % AND NOWHERE ELSE. That is")
print(" the only ceiling that is a property of a PACK rather than of a")
print(" policy; `arbitrage_soc_ceiling` is a trade preference and using")
print(" it here would be SAFETY-25's deferred option in the charge")
print(" direction. Everything between is left to the weights, which are")
print(" a preference and are honest about being one.")
print(" * A WEDGED UNIT IS NOT REACHABLE FROM HERE AT ALL. Its SoC reads")
print(" healthy, so no envelope closes. SAFETY-42 owns it.")
# ==============================================================================
print()
print("11. MUTATION: what defeats this card, and which check bites")
# ⚠️ EVERY ROW BELOW WAS RUN, on a tree rsynced from this branch with
# `__pycache__` purged and `python3 -B`, not reasoned about. MUTATION.md is the
# method. The two columns that matter are WHICH FILE went red and WHICH CHECK -
# a mutation caught only by a crash is recorded as such, because a traceback is
# a weaker gate than a named failure and the distinction is the point of the
# table.
MUT = [
("M1 envelope(): never close `hi` (the discharge end)",
"test_packshare 2 and 4, and test_delivery 4 - SAFETY-42's cold arm goes "
"back to being ASKED for a share it cannot deliver"),
("M2 envelope(): never close `lo` (the charge end)",
"test_packshare 2, and test_peakguard 10g - the two-inverter cycle's "
"resolved envelope for a full pack reverts to +/-rating"),
("M3 usable_kwh(): drop the capacity, weigh by SoC headroom alone",
"test_packshare 1 (the four schemes stop separating) and 3"),
("M4 weights(): ignore the direction, always use usable_kwh",
"test_packshare 3 - the charge side reports the discharge headroom"),
("M5 envelope(): `soc < floor` instead of discharge_permitted()'s `<=`",
"test_packshare 2 - the boundary and the agreement sweep"),
("M6 main.py: stop passing `weights=` to resolve() <- THE CARD ITSELF",
"test_packshare 10, the AST pin. ⚠️ NOTHING BEHAVIOURAL CATCHES THIS, in "
"this file or any other, because every fixture calls resolve() for "
"itself - which is exactly the state release/1.0 was in"),
("M7 main.py: file the `pack` claim at N=1 too",
"test_peakguard 10f's SoC sweep - and NOT test_packshare, which is "
"recorded rather than fixed: this file grades the arithmetic, and 'which "
"claims a real one-inverter cycle files' is that file's instrument"),
("M8 trade_power_w(): drop the SoC filter, size at the site rating",
"test_packshare 9"),
("M9 main.py: delete the unmet_w action-log branch",
"test_packshare 10, the AST pin - a shape check, and it says so"),
("M10 weights(): answer None for every roster",
"test_packshare 1's PRECONDITION. ⚠️ IT WAS A TRACEBACK BEFORE THAT LINE "
"EXISTED - the first draft of this file had no precondition and the "
"mutation took it down with a TypeError three lines into part 1"),
]
for _n, _w in MUT:
print(f" caught {_n}")
print(f" {_w}")
check("every mutation in the table above was RUN and every one of them is "
"caught by a named check on this branch. ⚠️ AND THE ROW WORTH READING IS "
"M6: the whole defect this card fixes - a parameter nothing passes - is "
"invisible to every behavioural fixture in this repository, because they "
"all build their own claim list and call `resolve()` themselves. That is "
"why the call site is pinned statically as well",
len(MUT) == 10)
print()
if fails:
print(f"{len(fails)} FAILED: {', '.join(fails)}")
sys.exit(1)
print("all checks passed")