"""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 sys.path.insert(0, str(Path(__file__).resolve().parent / "tools")) import main_files # noqa: E402 from app import arbitrage, packshare from named_absence import first, nth, present 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.delivery import ( DELIVERY_SHARE_FRACTION, UnitDelivery, account, delinquent, ) 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 = first((p for p in _r_opt.allocation.plans if p.unit == A), "no plan for 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", present(_plan_a) and _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 len(_broken) > 2 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", nth(_broken, 2)[1] == {A: 97, Bee: 903} and nth(_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; 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. ⚠️ THE WEIGHT, HOWEVER, " "DOES MOVE - this check used to say it did not - and part 8b grades what " "that cost the alarm, and what SAFETY-43 did about it", not packshare.envelope(PE(Bee, 55.0, B_KWH)).binding) # ============================================================================== print() print("8b. ⚠️ WHAT THE WEIGHTS DID TO SAFETY-42's DETECTION WINDOW, AND WHAT " "SAFETY-43 DID ABOUT IT - GRADED UNDER BOTH RULES, ON ONE FIXTURE") # ⚠️ THIS PART USED TO PIN THE UNFLATTERING ANSWER ON PURPOSE, AND SAFETY-43 IS # THE CARD THAT INVERTED IT. What it said - and it was true when it was written # - was: a wedged unit's weight MOVES whenever its energy headroom differs from # its sibling's (it is a pack fact, not a delivery fact), `delivery.delinquent()` # named a unit only while its `short_by()` gap exceeded `min_w` # (`Tuning.saturation_w`, 500 W) IN ABSOLUTE WATTS, and shrinking a unit's share # shrinks its gap. Shrink it below 500 W and the alarm stopped firing on a unit # that was still delivering nothing. The old text ended "if it ever goes red # because the alarm fired, read the card that made it fire before 'fixing' this # line". SAFETY-43 is that card, this is that red, and the checks below are the # same three measurements with their answers the other way round. # # ⚠️ AND THE EVIDENCE IS THE DIFF BETWEEN TWO RULES ON ONE RUN, NOT A NUMBER # THAT CHANGED IN A COMMIT. `delinquent()` takes `share_frac`, and # `share_frac=None` is SAFETY-42's rule EXACTLY - the absolute arm alone. So # `wedged_loop` grades every cycle twice, once under each rule, and the red and # the green sit side by side in one output: # # load 1500 W, OPT-09 weights, absolute arm only named on 0/200 cycles # load 1500 W, OPT-09 weights, the shipped rule named on 200/200 cycles # # A future reader who wants to know what this card did runs this file. # # ⚠️ WHAT SAFETY-43 DID NOT DO IS LOWER `min_w`, and the check on the window # arithmetic below is what holds that open: the ABSOLUTE arm's suppression # window is still 500 W wide and still widens to 6500 W under these weights. # Nothing that was quiet became noisy; a SECOND way to be named was added, and # it is a fraction of the unit's OWN ask, so it fires on a unit delivering none # of 125 W exactly as it fires on one delivering none of 1500 W. # # ⚠️ AND THE FIXTURE IS A SAFETY-42 TRUE POSITIVE BY THAT CARD'S OWN DEFINITION, # checked below before anything is concluded from it: B is WEDGED - it accepts # its share and produces nothing - at a HEALTHY 15 % against its own 10 % floor, # so `envelope()` does not close and `account()` explains nothing. This is # exactly the case §5 says the envelope cannot reach, which is why it is the # case where the weights' side effect mattered. # # ⚠️ TWO COPIES OF A. Inverter B does not exist; the plant below is modelled in # this file the way part 8's is, and B's ratings are datasheet. WEDGED_A_SOC, WEDGED_B_SOC = 90.0, 15.0 # both healthy, both above their floor MIN_W = Tuning().saturation_w # what `delinquent()` is handed ABSOLUTE_ONLY = None # `share_frac=None` - SAFETY-42's rule def wedged_loop(load_w, weighted, cycles=200, share_frac=DELIVERY_SHARE_FRACTION, b_delivers=0.0, b_soc=WEDGED_B_SOC, no_account=False): """The closed loop with unit B WEDGED at a healthy SoC. Real everything. Real `compute()`, real `plan()`, real `packshare.weights()`, real `delivery.account()` and real `delivery.delinquent()`. The only model is the plant: A obeys within one cycle inside its rating, B delivers `b_delivers` of whatever it is told - 0.0 is the wedged box, and is what makes this a SAFETY-42 case rather than an OPT-09 one. `weighted=False` is equal weights - what shipped. Envelopes are left OPEN in both arms deliberately: neither pack's SoC closes one here, so filing them would change nothing and would blur which half of the card is being graded. `share_frac` is SAFETY-43's relative arm. `ABSOLUTE_ONLY` grades the same cycles under SAFETY-42's rule, which is how the red and the green below come out of ONE run over ONE tree instead of out of two commits. `no_account=True` DELETES the account gate - every unit is handed `accounted=None` - and it exists to answer "would this arm still be silent if `account()` did nothing at all?". A control that answers the same way with the gate removed is not grading the gate, and the cut-out arm below is exactly that case. See it there; it is the point of that block. Returns `(cycles on which delinquent() named somebody, last cycle)`. """ packs = [PE(A, WEDGED_A_SOC, A_KWH), PE(Bee, b_soc, B_KWH)] tuning = Tuning(gain=1.0, slew_w=1000.0, max_w=A_W + B_W) prev, i_w, sat = 0.0, None, 0 power = [0.0, 0.0] grid = load_w named, last = 0, None 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 w = packshare.weights(packs, d.target_w) if weighted else None r = plan(d.target_w, {A: _B(-A_W, A_W), Bee: _B(-B_W, B_W)}, weights=w) assert r.ok, r.refused # THE MODELLED PLANT: A within one cycle inside its rating; B WEDGED. power = [max(-A_W, min(A_W, float(r.shares[A]))), b_delivers * max(-B_W, min(B_W, float(r.shares[Bee])))] grid = load_w - sum(power) who = delinquent( [UnitDelivery(p.name, r.shares[p.name], m, None if no_account else account(live=True, parked=False, soc=p.soc, bms_floor=p.bms_floor)) for p, m in zip(packs, power)], MIN_W, share_frac) named += bool(who) last = (d.target_w, dict(r.shares), grid, who) return named, last _WEDGED_PACK = PE(Bee, WEDGED_B_SOC, B_KWH) check("PRECONDITION: the wedged unit is a SAFETY-42 TRUE POSITIVE and NOT an " "OPT-09 case - at 15 % against its own 10 % floor no envelope closes and " "`account()` explains nothing, so anything the weights do to the alarm " "here is done to a fault the operator is entitled to be told about", not packshare.envelope(_WEDGED_PACK).binding and account(live=True, parked=False, soc=WEDGED_B_SOC, bms_floor=FLOOR) is None) check("PRECONDITION: the shipped fraction is a fraction, and it is STRICTLY " "below 1.0 - at 1.0 the relative arm could only fire on a unit IMPORTING " "while asked to discharge, which is SAFETY-30's fault and not this one, " "and this card's own case (a unit delivering NONE of its share, gap over " "share exactly 1.0) would be missed", 0.0 < DELIVERY_SHARE_FRACTION < 1.0) _wedge = {} for _load in (1500.0, 6000.0): for _weighted in (False, True): for _frac in (ABSOLUTE_ONLY, DELIVERY_SHARE_FRACTION): _n, _t = wedged_loop(_load, _weighted, share_frac=_frac) _wedge[(_load, _weighted, _frac)] = (_n, _t) print(f" load {_load:.0f} W, B wedged at 15 %, " f"{'OPT-09 weights' if _weighted else 'equal weights ':<14} " + ("absolute arm only" if _frac is ABSOLUTE_ONLY else f"frac {_frac:.2f} ") + f", named on {_n:3d}/200 cycles, 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, named=" + (str(sorted(_t[3])) if _t[3] else "(nobody)")) check("⚠️ THE CARD, RED: UNDER SAFETY-42's RULE AND OPT-09's WEIGHTS THE WEDGED " "UNIT IS NAMED ON NONE OF THE 200. Same wedged box, same healthy SoC, " "same load, same detector: the split moves to 1505/125, the gap on the " "wedged unit falls to 125 W, and 125 W < 500 W so the absolute arm " "returns {}. This is the defect SAFETY-43 was filed for, and it is " "measured here rather than remembered", _wedge[(1500.0, True, ABSOLUTE_ONLY)][0] == 0 and _wedge[(1500.0, True, ABSOLUTE_ONLY)][1][3] == {} and _wedge[(1500.0, True, ABSOLUTE_ONLY)][1][1] == {A: 1505, Bee: 125}) check("⚠️ THE CARD, GREEN: THE SHIPPED RULE NAMES IT ON ALL 200, ON THE SAME " "RUN. The unit is standing at +125 W and delivering 0 W - 100 % of its " "ask is not arriving - and `gap > frac * share` does not care that 125 W " "is a small number. The ONLY input that moved between this check and the " "one above is `share_frac`", _wedge[(1500.0, True, DELIVERY_SHARE_FRACTION)][0] == 200 and sorted(_wedge[(1500.0, True, DELIVERY_SHARE_FRACTION)][1][3]) == [Bee] and _wedge[(1500.0, True, DELIVERY_SHARE_FRACTION)][1][1] \ == {A: 1505, Bee: 125}) check("AT EQUAL WEIGHTS - WHAT SHIPPED BEFORE OPT-09 - the wedged unit was " "already named under the absolute rule on every cycle but the first, at " "a load its sibling can carry on its own. The loop winds the site target " "to twice the load, the 50/50 split stands +1500 W on a box producing " "nothing, and 1500 W > 500 W is what the absolute arm reads", _wedge[(1500.0, False, ABSOLUTE_ONLY)][0] == 199 and sorted(_wedge[(1500.0, False, ABSOLUTE_ONLY)][1][3]) == [Bee] and _wedge[(1500.0, False, ABSOLUTE_ONLY)][1][1] == {A: 1500, Bee: 1500}) check("...and the shipped rule adds exactly the FIRST cycle to that arm and " "nothing else - 199 becomes 200. On cycle one the slew limit holds the " "split at 500/500, a 500 W gap is NOT over a 500 W threshold (the " "absolute bound is strict), and the relative arm names a box delivering " "none of its 500 W. That one cycle is the whole of what this card costs " "an arm that was already firing", _wedge[(1500.0, False, DELIVERY_SHARE_FRACTION)][0] == 200 and _wedge[(1500.0, False, DELIVERY_SHARE_FRACTION)][0] - _wedge[(1500.0, False, ABSOLUTE_ONLY)][0] == 1) check("...and this is the regime where NOTHING ELSE TELLS THE OPERATOR " "ANYTHING: the meter is satisfied (grid within 5 W of zero), `unmet_w` is " "0 because both units accepted their shares, no envelope closed and no " "action-log line fires. The site is quietly running one inverter, and " "before this card the one instrument built to say so had gone quiet with " "it", abs(_wedge[(1500.0, True, DELIVERY_SHARE_FRACTION)][1][2]) <= 5.0 and abs(_wedge[(1500.0, False, DELIVERY_SHARE_FRACTION)][1][2]) <= 5.0) _w = packshare.weights([PE(A, WEDGED_A_SOC, A_KWH), _WEDGED_PACK], 1000.0) _window_equal = MIN_W * 2.0 _window_opt09 = MIN_W * sum(_w.values()) / _w[Bee] print(f" the ABSOLUTE arm's suppression window is UNCHANGED by SAFETY-43: " f"equal weights, site target < {_window_equal:.0f} W; OPT-09 weights, " f"site target < {_window_opt09:.0f} W of a {A_W + B_W:.0f} W site") check("⚠️ AND `min_w` WAS NOT LOWERED, WHICH IS THE HALF OF THIS CARD THAT IS " "EASIEST TO GET WRONG. The absolute arm still suppresses below 1000 W at " "equal weights and below 6500 W at these ones - the same arithmetic this " "part has always printed - because SAFETY-43 ADDED a second way to be " "named rather than loosening the first. Lowering `saturation_w` would " "have reported normal saturation on every unit at once, which is the " "false-trip regime SAFETY-41 spent four attempts in", abs(_window_equal - 1000.0) < 1.0 and abs(_window_opt09 - 6500.0) < 1.0 and MIN_W == Tuning().saturation_w == 500.0) check("⚠️ AND THE ABSOLUTE ARM WAS NEVER SILENT EVERYWHERE - which is what a " "check that only ever ran the quiet load would be pinning. Push the load " "past the healthy pack's OWN RATING and `_fill()`'s last-resort pass " "hands the wedged unit 3000 W anyway, the gap goes back over 500 W, and " "both arms named it under SAFETY-42's rule too. The window was real and " "it had an upper edge; the hole was the quiet regime below that edge, " "and the shipped rule now names the unit on every cycle of all four runs", _wedge[(6000.0, True, ABSOLUTE_ONLY)][0] == 195 and _wedge[(6000.0, False, ABSOLUTE_ONLY)][0] == 199 and _wedge[(6000.0, True, DELIVERY_SHARE_FRACTION)][0] == 200 and _wedge[(6000.0, False, DELIVERY_SHARE_FRACTION)][0] == 200 and sorted(_wedge[(6000.0, True, DELIVERY_SHARE_FRACTION)][1][3]) == [Bee]) print() print(" THE POSITIVE CONTROL: what the relative arm must STILL NOT say") # ⚠️ A CARD THAT ADDS A WAY TO RAISE AN ALARM HAS TO SHOW WHAT IT DID NOT ADD, # or it has traded a missed alarm for a false one - which in this system is not # a rhetorical risk. SAFETY-41 took four attempts on notification cadence and # the owner's words were "i'm getting really annoyed by the notifications in my # HA". So: the same fixture, the same weights, the same tiny 125 W share, with # the ONE fact that makes the shortfall legitimate changed. # ⚠️ AND THE FIRST DRAFT OF THIS BLOCK WAS VACUOUS, WHICH IS WHY IT NOW PRINTS # ITS SHARES. It ran the cut-out arm, saw 0/200, and concluded "`account()` # explains it" and "identical trace to the wedged arm". BOTH WERE FALSE. At its # own cut-out the pack's usable energy ABOVE that cut-out is zero, so # `weights()` gives it 0.0, `_fill()`'s preferred pass gives it nothing, the # healthy sibling covers the load alone so the LAST-RESORT pass never runs, and # its share is 0 W on ALL 200 CYCLES. A unit with a 0 W share is not in `asked`, # so `len(asked) < 2` returns {} BEFORE EITHER ARM IS EVALUATED and `account()` # is never consulted - measured below by deleting the gate and getting the same # answer. The wedged arm's share is the 125 W printed on its own line above; # this one's is 0, printed and asserted below. Nothing was identical and nothing # reached the comparison. # # ⚠️ THE FACT THAT REPLACES IT IS BETTER, NOT WEAKER: at its own cut-out OPT-09 # STOPS ASKING THE PACK AT ALL, so SAFETY-43's relative arm is never reached. # That is a SECOND, INDEPENDENT protection sitting UPSTREAM of `account()` - the # allocator declines to ask before the detector has to decline to alarm. The # account gate itself is graded where it is REAL, on a positive 125 W share, in # `test_delivery.py` part 1 ("THE ACCOUNT GATE IS UNTOUCHED BY IT"). # # ⚠️ AND IT WAS INVISIBLE FOR ONE REASON: this was the only line in part 8b that # did not print its shares. `inverter-b+0` would have been on screen from the # first run. Every line in this part prints them now. _sat_named, _sat_last = wedged_loop(1500.0, True, b_soc=FLOOR) _sat_nogate, _sat_nogate_last = wedged_loop(1500.0, True, b_soc=FLOOR, no_account=True) print(f" load 1500 W, B AT ITS OWN {FLOOR:.0f} % CUT-OUT, OPT-09 weights, " f"frac {DELIVERY_SHARE_FRACTION:.2f}: named on {_sat_named:3d}/200 " f"cycles, shares " + ", ".join(f"{u}{v:+d}" for u, v in sorted(_sat_last[1].items())) + ", named=" + (str(sorted(_sat_last[3])) if _sat_last[3] else "(nobody)") + f"; with account() DELETED: {_sat_nogate:3d}/200") # ⚠️ AND THE `no_account=True` SWITCH ITSELF WAS GRADED BY NOTHING. It is used # in exactly one place - the fixture above - where it PROVABLY cannot change the # answer, because a cut-out pack is never asked and the gate is never reached. # Review neutered the switch so it removed nothing: the file stayed GREEN and # printed "with account() DELETED: 0/200" while `account()` had not been # deleted. A line asserting a mechanism that did not happen - B1's shape, one # level down, in the fix for B1. # # The switch DOES bite, on a fixture already reachable from this helper: at the # cut-out with EQUAL weights, `plan()` hands B a 50/50 share, so the gate is the # only thing keeping it silent. Measured: gate on 0/200, gate deleted 200/200. # That proves the switch removes the gate, AND gives this part the end-to-end # account-gate control on a POSITIVE share that it otherwise has to outsource to # test_delivery.py part 1. _eq_gate, _eq_last = wedged_loop(1500.0, False, b_soc=FLOOR) _eq_nogate, _ = wedged_loop(1500.0, False, b_soc=FLOOR, no_account=True) print(f" ...and the SWITCH is real: same cut-out, EQUAL weights so B IS " f"asked ({_eq_last[1][Bee]:+d} W last cycle) - gate on {_eq_gate:3d}/200, " f"gate DELETED {_eq_nogate:3d}/200") check("⭐ THE `no_account` SWITCH ACTUALLY REMOVES THE GATE, on a fixture where " "the gate is the only thing keeping the pack silent. Without this the " "line above prints 'with account() DELETED' about a run in which nothing " "was deleted - which is exactly the defect this block was rewritten for", _eq_gate == 0 and _eq_nogate == 200 and _eq_last[1][Bee] > 0) check("A PACK AT ITS OWN CUT-OUT IS NEVER ASKED, SO THE RELATIVE ARM IS NEVER " "REACHED. Its usable energy above its own cut-out is zero, `weights()` " "answers 0.0 for it, the healthy sibling covers the load so `_fill()`'s " "last-resort pass never runs, and its share is 0 W on every one of the " "200 cycles - so it is not in `asked` and `len(asked) < 2` returns {} " "before either arm is compared. ⚠️ THE SHARE IS ASSERTED, not inferred " "from the silence: a control that cannot say WHY it is quiet is a control " "that cannot fail", _sat_named == 0 and _sat_last[3] == {} and _sat_last[1][Bee] == 0) check("⚠️ AND THAT IS A SECOND, INDEPENDENT PROTECTION - NOT THE ACCOUNT GATE, " "WHICH THIS ARM DOES NOT TOUCH. Delete `account()` entirely, hand every " "unit `accounted=None`, and this arm is STILL silent on all 200 cycles, " "because the protection is UPSTREAM of it: the allocator declines to ask " "before the detector has to decline to alarm. ⚠️ SO DO NOT READ THIS " "BLOCK AS EVIDENCE ABOUT `account()` - it is evidence about the ask. The " "account gate on a POSITIVE share is graded in test_delivery.py part 1", _sat_nogate == 0 and _sat_nogate_last[3] == {}) _derate_named, _derate_last = wedged_loop(1500.0, True, b_delivers=0.5) print(f" load 1500 W, B delivering HALF its share, OPT-09 weights, frac " f"{DELIVERY_SHARE_FRACTION:.2f}: named on {_derate_named:3d}/200 cycles, " f"shares " + ", ".join(f"{u}{v:+d}" for u, v in sorted(_derate_last[1].items())) + ", named=" + (str(sorted(_derate_last[3])) if _derate_last[3] else "(nobody)")) check("...and ORDINARY TRACKING ON A SMALL SHARE IS NOT DELINQUENCY EITHER. " "The same unit at the same weights delivering HALF of what it is asked " "for is short by 50 % - well inside the shipped fraction - and its gap " "in watts is far under `min_w`, so NEITHER arm names it. A relative " "criterion that fired here would be the saturation-shaped alarm this " "whole module is written against", _derate_named == 0 and _derate_last[3] == {}) check("...and the DERATED control is not vacuous, which is asserted on the ONE " "thing that could make it so: unlike the cut-out arm above, its unit IS " "asked - a positive share on every cycle - so it really does reach both " "arms and is turned away by the size of its shortfall. ⚠️ THIS GUARD USED " "TO RE-ASSERT THE WEDGED ARM's 200/200 INSTEAD, WHICH IS WHY IT DID NOT " "CATCH THE CUT-OUT ARM BEING VACUOUS: a check on a DIFFERENT run cannot " "say anything about whether THIS one reached the comparison", _derate_last[1][Bee] > 0 and _wedge[(1500.0, True, DELIVERY_SHARE_FRACTION)][0] == 200) print() print(" N=1 IS BYTE-IDENTICAL, AND THAT IS ASSERTED RATHER THAN ARGUED") # ⚠️ THE LIVE INSTALL IS N=1 AND THIS CARD MUST NOT REACH IT. Two independent # statements, and each is checked against the thing that would break it: # 1. `plan()` RETURNS BEFORE `weights` IS READ AT ALL, so no weighting - and # no change to how one is derived - can move a one-inverter share. Graded # with a `weights` object that RAISES if anything reads it. # 2. `delinquent()` returns {} at a roster of one for every input, including # the input the RELATIVE arm was built for. The proper-subset algebra runs # before either arm, so the fraction is never even multiplied. class _Poison(dict): """A `weights` that cannot be read without saying so. Not a mock: a tripwire.""" def get(self, *a, **k): raise AssertionError("plan() read weights at a roster of one") def __getitem__(self, k): raise AssertionError("plan() read weights at a roster of one") _n1_plain = plan(1234.5, {A: _B(-A_W, A_W)}) _n1_poison = plan(1234.5, {A: _B(-A_W, A_W)}, weights=_Poison()) check("`plan()` at a roster of ONE returns the site target unread, and it " "returns it WITHOUT TOUCHING `weights` - handed a mapping that raises on " "any read, it answers identically to the same call with no weights at " "all. The N=1 pass-through is above the weighting, so OPT-09 could not " "move it and SAFETY-43 does not either", _n1_poison.shares == _n1_plain.shares == {A: 1234.5} and _n1_poison.unmet_w == _n1_plain.unmet_w) try: plan(1234.5, {A: _B(-A_W, A_W), Bee: _B(-B_W, B_W)}, weights=_Poison()) except AssertionError: _poison_bites = True else: _poison_bites = False check("...and the tripwire is a real one: the SAME poisoned mapping at a roster " "of TWO is read and raises. A check that could not fail would be " "measuring nothing", _poison_bites) _n1_rel = [delinquent([UnitDelivery(A, s, m)], MIN_W, f) for s in (0, 125, 500, 2000, 5000, -2000, None) for m in (0, 60, 125, 2000, -2000, None) for f in (ABSOLUTE_ONLY, DELIVERY_SHARE_FRACTION)] check(f"...and `delinquent()` answers {{}} at a roster of one on all " f"{len(_n1_rel)} of (share, measured, rule) - INCLUDING 0 of 125 W, the " f"exact input this card added an arm for. `len(asked) < 2` returns " f"before either threshold is compared, so the one-inverter install this " f"add-on is running today cannot tell that SAFETY-43 happened", all(r == {} for r in _n1_rel)) # ============================================================================== 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)) # SAFETY-45's `headroom_w` is the last positional. It is set to a value that # CANNOT BIND on either call - larger than every sizing below - so this part # goes on grading OPT-09's per-pack sizing and not the capacity-tariff cap that # now sits beside it. test_arbitrage.py section 3b is where it is made to bind. _CLEAR_W = 1e9 _sell = arbitrage._act(_Plan(_here), _NOW, _V(_other, _here), 60.0, 0.0, 0.0, 20.0, 90.0, _CLEAR_W) _sell_ok = arbitrage._act(_Plan(_here), _NOW, _V(_other, _here), 60.0, 0.0, 4600.0, 20.0, 90.0, _CLEAR_W) _buy = arbitrage._act(_Plan(_here), _NOW, _V(_here, _other), 60.0, 0.0, 0.0, 20.0, 90.0, _CLEAR_W) _buy_ok = arbitrage._act(_Plan(_here), _NOW, _V(_here, _other), 60.0, 5000.0, 0.0, 20.0, 90.0, _CLEAR_W) 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 = main_files.source() _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 = first( f for cls in ast.walk(main_files.tree()) 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 BY THE ENVELOPE. Its SoC reads") print(" healthy, so nothing closes, and SAFETY-42 owns the detection.") print(" ⚠️ BUT IT IS REACHABLE BY THE WEIGHTS, WHICH MOVE ON ENERGY") print(" HEADROOM AND NOT ON DELIVERY. That narrowed SAFETY-42's") print(" detection window - a regression this file PINNED rather than") print(" fixed - and SAFETY-43 closed it by making delinquency relative") print(" to the ask as well as absolute. Part 8b now grades both rules") print(" on one fixture: the absolute arm still says nothing, and the") print(" shipped one names the unit on all 200 cycles.") # ============================================================================== 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"), ("M11 weights(): keep the shape, force every weight to 1.0 <- THE PROOF " "THAT PART 8b CAN GO RED", "test_packshare 8b, FIVE named checks - re-run for SAFETY-43 rather than " "carried across, because the part it grades was rewritten: the split " "reverts to 1500/1500 so the absolute arm names the unit on 199/200 " "(taking the card's RED check down), the shipped rule's 200/200 comes " "from the wrong shares (taking the GREEN one down), the OPT-09 window " "collapses from 6500 W to 1000 W, the 6000 W arm's 195 becomes 199, and " "the derated positive control moves from 0/200 to 3/200. ⚠️ RUN, NOT " "REASONED ABOUT - a check that pins a behaviour has to be shown capable " "of failing, or it is indistinguishable from a check that pins nothing. " "(It also takes parts 1, 3, 7 and 8 down, which is M3's territory; 8b is " "the part that names the SAFETY-42 interaction)"), # --- SAFETY-43's three, and they pin the new arm from BOTH sides -------- ("M12 delinquent(): drop the relative arm, keep `gap > min_w` <- THE " "CARD ITSELF REVERTED", "SEVEN named checks: test_delivery 1 (THREE - the 0-of-125 W naming, the " "proportion in the sentence, and the strict relative bound) and " "test_packshare 8b (FOUR - the GREEN check goes back to 0/200 under " "OPT-09's weights, the equal arm's 200 back to 199, the 6000 W arm back " "to 195/199, and the derated control's vacuity guard falls with them). " "test_alloc and test_control stay green, which is the point - nothing " "outside this check moves"), ("M13 DELIVERY_SHARE_FRACTION 0.85 -> 1.0 <- THE TOO-LOOSE DIRECTION", "SEVEN named checks: test_delivery 1 (TWO) and test_packshare 8b (FIVE), " "including the PRECONDITION that the fraction is strictly below 1.0. At " "1.0 the arm can only fire on a unit IMPORTING while asked to discharge, " "so the card's own case - 0 of 125 W, gap over share exactly 1.0 - is " "missed and the whole card is a no-op"), ("M14 DELIVERY_SHARE_FRACTION 0.85 -> 0.05 <- THE TOO-SENSITIVE ONE", "SIX named checks: test_delivery 1 (TWO - the strict bound, and 'IT TOOK " "NOTHING AWAY' stops being true) and 4b (THREE - the `saturation_w` pin " "goes VACUOUS, because the relative arm names B at every threshold), plus " "test_packshare 8b's DERATED positive control, where a unit delivering " "half its share starts being named. ⚠️ THIS IS THE ROW THAT MATTERS - the " "fraction is pinned from BOTH directions, so it is a derived number with " "checks around it rather than a literal nobody can move"), ] 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. ⚠️ M12-M14 ARE " "SAFETY-43's and were run the same way, on trees copied fresh and " "mounted :ro so no `.pyc` could go stale - MUTATION.md's defect. All " "three are REDS, which is the direction that defect cannot fabricate. " "⚠️ AND THEIR COUNTS WERE RE-RUN IN REVIEW AND TWO OF THEM MOVED: " "test_delivery.py part 1 used to subscript `delinquent(...)['b']` for a " "print, which raised KeyError under M12 and M13 and ABORTED THE FILE at " "part 1, leaving parts 2-8 ungraded and the recorded coverage smaller " "than the truth. It is a `.get` now. M14's count was simply wrong: five " "named checks in test_delivery, not four", len(MUT) == 14) print() if fails: print(f"{len(fails)} FAILED: {', '.join(fails)}") sys.exit(1) print("all checks passed")