SAFETY-32 round 4: the same family at the SIBLING ARGUMENT, and the family enumerated

Rounds 2 and 3 hardened the FLOOR axis - the max inside binding_soc_floor(),
the roster handed to it, Controller.packs, pack_floor_pct()'s conjunction.
peakguard.decide() is handed a SECOND reduction over the same roster beside it,
soc=self.pack_floor_soc, and peakguard.py's own docstring names the two as a
pair. Three rewrites of Controller.pack_floor_soc were green across all 19 files
on 52380c0 and again after the rebase onto 3934ad7:

    min(float(v) for v in vals)  ->  ... vals[:2]   +4 bytes
                                 ->  ... vals[-2:]  +5 bytes
                                 ->  ... vals[:3]   +4 bytes

while [:1], [-1:], [1:], [:-1], max() and the mean were all red. The suite's
separating power on the SoC axis stopped at exactly N=2 - the state the floor
axis was in before round 3.

It changes a real answer: three packs at 50/60/11 %, emptiest THIRD, answers
11.0 shipped and 50.0 mutated. The site is then graded for discharge PERMISSION
against a pack 39 points fuller than the emptiest and spends a pack its own BMS
stops, through sim/inject.py which runs no validator rule.

Round 3's machinery transfers directly. New test_peakguard.py part 12i:
- six REAL Controller.cycle()s, emptiest pack at every position of a three-pack
  roster and both positions of a two-pack one, observable read off
  PeakDecision.soc; cut-outs held flat so the floor axis cannot move the answers
- computed slice and non-slice tables, printed, red naming any live survivor
- a LENGTH SWEEP over the real property, 78 rosters of 1..12 real inverters,
  closing head/tail widths 1-11 and asserting width 12 is unreached
- a NONE-GUARD sweep over the same 78
- THREE exact structural pins: the vals comprehension iterates the bare
  self.inverters, and both any() and min() reduce the whole vals - closing every
  slice width at every roster length, which no bounded fixture can

SPIKE-04: pack B is 4 x Dyness DL5.0C, DoD 90 -> floor 10 % [FROM DATASHEET AND
SERIAL - inverter B does not exist and nothing about it is measured]. Pack A is
DoD 90 -> 10 % and that one IS measured. So production is DEGENERATE on the
floor axis and a differing floor exists ONLY in the fixtures - which is the
argument for keeping them, not for simplifying them. Recorded in CHANGELOG and
DECISIONS so nobody later collapses them to one number. The SoC axis has no such
degeneracy.

AND THE FAMILY, not the member. Every reduction over self.inverters/self.packs
in Controller was found from the AST and mutated to a slice against the full
suite. Eighteen were already graded. FOUR were not, and three of them are
SAFETY-30's, all any(i.sign_wd.tripped for i in self.inverters), each +4 bytes
and green across 19 files - because the only fixture that reached them was a
roster of ONE, where self.inverters[:1] IS self.inverters:

- cycle()'s stale-input hold: a LATCHED unit must take the roster past it or its
  failsafe SET stops being re-filed the moment a poll is missed
- publish()'s status vocabulary: a site holding an inverter at 0 W published
  "running"
- status_payload()'s banner: the one headline on the page

New test_watchdog.py part 8 grades all three with the latch at EACH position
plus a no-latch counterfactual. New part 12j pins all 26 roster comprehensions
in Controller to iterating the roster WHOLE, so a reduction added over a slice
anywhere in the class is red on the line it is written, with no fixture needed.

Two rows reported OPEN rather than closed: _grade_store_migration()'s second
disjunct (now red on 12j, argued equivalent because a unit tripped at startup
implies its namespaced key is set) and cycle()'s undeliverable-unit LOG LINE,
where sorted(...)[:1] is still green - it truncates names in one event message;
the refusal itself is a different expression and is graded.

MUTATION.md gains habits 10 and 11: a fixture of N members cannot separate two
reductions that agree on N, and mutate the SIBLINGS of anything you harden.

No shipped behaviour changed - tests and documentation only. 19/19 green;
all three named survivors and all round-3 mutations verified RED.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU
This commit is contained in:
2026-09-05 03:01:57 +02:00
committed by glenn schrooyen
co-authored by Claude Opus 5
parent a4c95f83ce
commit 8beba0a3e7
5 changed files with 1106 additions and 1 deletions
+667 -1
View File
@@ -4159,6 +4159,590 @@ check("⚠️ AC 5. At a roster of ONE the same notification carries NO per-pack
and "peak_soc_floor bounds THE PEAK GUARD ONLY" in _body32one)
# --- 12i. THE SIBLING ARGUMENT: `pack_floor_soc`, THE OTHER HALF OF THE PAIR --
# ⚠️ THIS IS ROUND 4's BLOCKER, AND IT IS THE SAME FAMILY ONE ARGUMENT OVER.
# Everything from 12c2 to 12h grades reductions on the FLOOR axis: the max
# inside `binding_soc_floor()`, the roster handed to it, `Controller.packs`, and
# `pack_floor_pct()`'s conjunction. `Controller.pack_floor_soc` is the MINIMUM
# over the same roster, handed to the SAME `peakguard.decide()` call as `soc=`,
# and until this block nothing graded its reduction past N=2.
#
# ⚠️ THE PAIRING IS DOCUMENTED, WHICH IS WHY HARDENING ONE HALF IS NOT ENOUGH.
# main.py's call site says "MAXIMUM here pairs with the MINIMUM in
# `pack_floor_soc`: both reductions move the verdict towards refusing a shave
# and away from spending a pack". A conjunction whose two halves have different
# amounts of evidence behind them is exactly as strong as its weaker half.
#
# THE SURVIVORS THIS BLOCK EXISTS FOR, all confirmed GREEN across all 19 files
# at 52380c0 and again on the rebase onto 3934ad7:
#
# min(float(v) for v in vals) -> ... for v in vals[:2] +4 bytes
# -> ... for v in vals[-2:] +5 bytes
# -> ... for v in vals[:3] +4 bytes
#
# while `vals[:1]`, `vals[-1:]`, `vals[1:]`, `vals[:-1]`, `max(vals)` and the
# mean were all killed. The suite's separating power on the SoC axis stopped at
# exactly N=2 - which is the state the FLOOR axis was in before round 3, and
# which round 3 named as its own blocker 2. The evidence is a fixture of two
# packs, and A FIXTURE OF N PACKS CANNOT SEPARATE REDUCTIONS THAT AGREE ON N.
#
# IT CHANGES A REAL ANSWER, which is why it is a defect and not a tidiness
# point: a three-pack roster at 50 / 60 / 11 % with the emptiest pack THIRD.
# The shipped property answers 11 %; `vals[:2]` answers 50 %. The site is then
# graded for discharge PERMISSION against a pack 39 points fuller than the
# emptiest one, and spends a pack its own BMS stops - through `sim/inject.py`,
# which runs no validator rule at all.
#
# ⚠️ AND PRODUCTION IS DEGENERATE ON THE FLOOR AXIS, WHICH MAKES THIS AXIS THE
# LIVE ONE. SPIKE-04 reads pack B as 4 x Dyness DL5.0C at DoD 90 [FROM THE
# DATASHEET AND THE SERIAL - inverter B does not exist and nothing about it has
# been measured], so its cut-out is 10 %, the same as pack A's MEASURED 10 %.
# Two packs that floor at the same value make `binding_soc_floor()` answer the
# option whatever it reduces. The SoC axis has no such degeneracy: two batteries
# at the same state of charge is a coincidence, not a configuration.
#
# THE TREATMENT IS ROUND 3's, TRANSFERRED: a roster SET with the extreme at
# every position, a LENGTH SWEEP, and an EXACT STRUCTURAL PIN on the reduction's
# argument. Nothing below is a measurement of a second or third pack.
_S_EMPTY_SIM, _S_MID_SIM, _S_FULL_SIM = 11.0, 50.0, 60.0
_S_TRIO_SIM = (_S_FULL_SIM, _S_MID_SIM, _S_EMPTY_SIM)
def _soc_reaching_decide(tag, *socs):
"""Drive ONE REAL `Controller.cycle()` on an N-pack roster and report the
SoC `peakguard.decide()` was actually handed, read back off the decision
main.py itself filed (`PeakDecision.soc` is the argument, echoed).
⚠️ THE EXACT MIRROR OF `_floor_reaching_decide` ABOVE, on the other axis.
Not a spy and not a grep: the shipped `cycle()` runs, the shipped
`pack_floor_soc` runs, and the observable is a field of the decision the
controller stored - so the whole path from `Controller.inverters` to the
guard's argument is under test. Every pack carries the SAME cut-out
(inverter A's measured 10 %, which is below `peak_soc_floor`, so
`binding_soc_floor()` answers the option for every roster here); the only
thing that varies between calls is the packs' states of charge. That is
habit 1: with the FLOOR axis held flat, nothing but the SoC reduction can
move these answers.
"""
def _prep(ctl):
for _i in range(len(socs) - 1):
_sfx = chr(ord("b") + _i)
second_inverter(ctl, name=f"inverter-{_sfx}",
setpoint=f"number.sp_{_sfx}")
for inv, so in zip(ctl.inverters, socs):
inv.soc, inv.batt, inv.bms_soc_floor = so, 0.0, A_PACK_FLOOR
ctl, _hs, _net, _rem = wired_over_limit(
TMP / f"s32s_{tag}.db", LIVE_MAX_W * 0.3, prepare=_prep)
return ctl, ctl.peak_guard
# BASELINE: three packs, all of them ABOVE `peak_soc_floor`, so the guard is
# permitted and sizes a real forced discharge. Every run below varies one thing
# from this baseline (habit 7) - which pack is the empty one, and nothing else.
_s_ctl_ok, _s_ok = _soc_reaching_decide("ok3", _S_FULL_SIM, _S_MID_SIM,
SOC_BETWEEN)
# THE SET: the SAME THREE readings, the emptiest at EACH position in turn. A
# reduction that reads a proper subset of the positions misses the empty pack on
# the run where it sits at the position that subset drops, and answers a fuller
# pack instead.
_s3_runs = []
for _pos in range(3):
_sc3 = [_S_FULL_SIM, _S_MID_SIM, _S_EMPTY_SIM]
_sc3.remove(_S_EMPTY_SIM)
_sc3.insert(_pos, _S_EMPTY_SIM)
_c3s, _d3s = _soc_reaching_decide(f"s3p{_pos}", *_sc3)
_s3_runs.append((f"3-pack, emptiest at index {_pos}", _sc3, _c3s, _d3s))
# ...and the two-pack mirror, which is the shape production reaches first.
_s2_runs = []
for _pos in range(2):
_sc2 = [_S_FULL_SIM, _S_EMPTY_SIM] if _pos else [_S_EMPTY_SIM, _S_FULL_SIM]
_c2s, _d2s = _soc_reaching_decide(f"s2p{_pos}", *_sc2)
_s2_runs.append((f"2-pack, emptiest at index {_pos}", _sc2, _c2s, _d2s))
print(f" REAL cycles, every cut-out held at inverter-a's measured "
f"{A_PACK_FLOOR:.0f} %, peak_soc_floor {SOC_FLOOR:.0f} % (every pack past "
f"the first is SIMULATED):")
print(f" 3-pack baseline, none empty: SoCs "
f"{[f'{s:g}' for s in (_S_FULL_SIM, _S_MID_SIM, SOC_BETWEEN)]} % -> "
f"decide() got soc={_s_ok.soc:g} %, floor_limited={_s_ok.floor_limited}")
for _rn, _sc, _ctl_s, _dec_s in _s3_runs + _s2_runs:
print(f" {_rn}: SoCs {[f'{s:g}' for s in _sc]} % -> decide() got "
f"soc={_dec_s.soc:g} %, floor_limited={_dec_s.floor_limited}")
check("⚠️ THE POSITIONAL PIN ON THE SoC AXIS, AND IT NEEDS THREE PACKS TO "
"EXIST. On a roster of THREE the SoC that reaches `decide()` is the "
"EMPTIEST pack wherever on the roster that pack sits - index 0, index 1 "
"and index 2 all answer 11 %, and all three rosters carry the SAME "
"multiset of readings. A property that reduced any PROPER SUBSET of the "
"positions would answer a fuller pack on the run whose empty one sits at "
"the position it dropped. `vals[:2]` and `vals[-2:]` - the +4- and "
"+5-byte mutations that walked through every fixture in this file up to "
"round 3, because at N=2 they ARE the roster - go red here",
[r[3].soc for r in _s3_runs] == [_S_EMPTY_SIM] * 3
and all(len(r[2].inverters) == 3 for r in _s3_runs)
and all(sorted(inv.soc for inv in r[2].inverters)
== sorted(_S_TRIO_SIM) for r in _s3_runs)
and [[inv.soc for inv in r[2].inverters] for r in _s3_runs]
== [r[1] for r in _s3_runs])
check("...and the TWO-pack mirror, which is what production reaches the moment "
"inverter B is installed: swap the same two readings between the packs "
"and the answer is the emptier one both times. `vals[:1]` and `vals[-1:]` "
"- grade the site against pack A alone, or against the last pack alone - "
"each agree with the roster on one of these two runs and disagree on the "
"other, so neither survives the pair",
[r[3].soc for r in _s2_runs] == [_S_EMPTY_SIM] * 2
and [[inv.soc for inv in r[2].inverters] for r in _s2_runs]
== [[_S_EMPTY_SIM, _S_FULL_SIM], [_S_FULL_SIM, _S_EMPTY_SIM]])
check("⚠️ ...AND THE SITE'S VERDICT FLIPS WITH IT, which is what makes this a "
"defect rather than a reporting difference. With the emptiest pack at "
"11 % - below the 20 % EMS floor - every one of those five sites is "
"FLOOR-LIMITED and sizes NO forced discharge at all, where the baseline "
"with its emptiest pack at 25 % is permitted and sizes a positive one. A "
"reduction that dropped the empty pack's position would grade this site "
"for PERMISSION against a pack 39 points fuller and spend, through "
"`sim/inject.py`, a pack its own BMS stops",
all(r[3].floor_limited is True and r[3].floor_w == 0.0
for r in _s3_runs + _s2_runs)
and _s_ok.floor_limited is False and _s_ok.floor_w > 0.0
and _s_ok.soc == SOC_BETWEEN)
check("...and the precondition that makes the six runs comparable (habit 1): "
"every pack in every roster carries the SAME cut-out, so "
"`binding_soc_floor()` answers the SAME site floor on all of them and the "
"FLOOR axis cannot be what separated the answers; the three readings "
"really are three different numbers; and the empty one is really below "
"the EMS floor while the other two are really above it - two readings on "
"the same side of it would make every line here vacuous",
len({r[3].soc_floor for r in _s3_runs + _s2_runs}) == 1
and _s_ok.soc_floor == _s3_runs[0][3].soc_floor == float(SOC_FLOOR)
and all(inv.bms_soc_floor == A_PACK_FLOOR
for r in _s3_runs + _s2_runs for inv in r[2].inverters)
and len({_S_EMPTY_SIM, _S_MID_SIM, _S_FULL_SIM}) == 3
and _S_EMPTY_SIM < SOC_FLOOR < SOC_BETWEEN < _S_MID_SIM < _S_FULL_SIM)
# ⚠️ AND THE ENUMERATION ITSELF, COMPUTED RATHER THAN CLAIMED - 12g's table on
# the other axis. Every positional slice of `vals`, applied to the REAL
# `Controller.inverters` each cycle above actually carried, compared against the
# SoC that REALLY reached `decide()` on that run. A slice with an empty kill
# list is a live survivor and this line goes red naming it.
#
# ⚠️ WHAT THIS DOES NOT CLAIM. It grades the members listed and nothing wider -
# a rewrite that is not a positional slice (a different reduction, a filter on
# pack NAME, a sort before the slice) is not covered here. `max` and the mean
# are graded on the same runs immediately below. The table is PRINTED so a
# reader can see exactly how far the claim reaches.
_S_RUNS = ([("3-pack baseline, none empty", _s_ctl_ok, _s_ok)]
+ [(rn, ctl, dec) for rn, _sc, ctl, dec in _s3_runs + _s2_runs])
def _soc_reduced(vals, fn):
"""`pack_floor_soc`'s body with ONE thing changed: what `min()` iterates.
The None guard still runs over the WHOLE list, because these mutations are
inside the `min()` generator and not in the guard - the guard is graded
separately below. An empty slice makes the real `min()` raise, which is a
crashing cycle and therefore a kill; it is reported as a distinct answer
rather than swallowed.
"""
if not vals or any(v is None for v in vals):
return None
sl = fn(vals)
return float(min(float(v) for v in sl)) if sl else "min() of an empty slice"
_SOC_SLICE_FAMILY = {
"vals[:1]": lambda vs: vs[:1],
"vals[-1:]": lambda vs: vs[-1:],
"vals[:2]": lambda vs: vs[:2],
"vals[-2:]": lambda vs: vs[-2:],
"vals[1:]": lambda vs: vs[1:],
"vals[:-1]": lambda vs: vs[:-1],
"vals[1:-1]": lambda vs: vs[1:-1],
"vals[::2]": lambda vs: vs[::2],
}
_SOC_REDUCTION_FAMILY = {
"max(vals)": lambda vs: max(vs),
"mean(vals)": lambda vs: sum(vs) / len(vs),
"median(vals), upper": lambda vs: sorted(vs)[len(vs) // 2],
"sorted(vals)[1] (second emptiest)": lambda vs: sorted(vs)[min(1, len(vs) - 1)],
}
_soc_slice_kills = {
_nm: [rn for rn, ctl, dec in _S_RUNS
if _soc_reduced([inv.soc for inv in ctl.inverters], _fn) != dec.soc]
for _nm, _fn in _SOC_SLICE_FAMILY.items()}
_soc_red_kills = {
_nm: [rn for rn, ctl, dec in _S_RUNS
if _fn([inv.soc for inv in ctl.inverters]) != dec.soc]
for _nm, _fn in _SOC_REDUCTION_FAMILY.items()}
print(" THE POSITIONAL-SLICE FAMILY inside `pack_floor_soc`, and which run "
"separates each from the whole roster:")
for _nm, _k in _soc_slice_kills.items():
print(f" {_nm:<12} killed by: "
+ ("; ".join(_k) if _k else "*** NOTHING HERE - SURVIVOR ***"))
print(" ...and the NON-slice rewrites of the reduction itself:")
for _nm, _k in _soc_red_kills.items():
print(f" {_nm:<34} killed by: "
+ ("; ".join(_k) if _k else "*** NOTHING HERE - SURVIVOR ***"))
check("⚠️ EVERY positional slice in the enumerated family answers something "
"OTHER than the SoC that really reached `decide()` on at least one of "
"the six runs above - and it is the THREE-pack runs that kill `[:2]` and "
"`[-2:]`, which the two-pack runs structurally could not, by exactly the "
"argument 12c2 makes about N packs and reductions that agree on N. The "
"kill list is computed against the REAL `Controller.inverters` each cycle "
"carried and the REAL `PeakDecision.soc` it produced, so a member with an "
"empty list is a live survivor and takes this line red naming it. This "
"grades the LISTED members only; the printed table is its extent",
all(_soc_slice_kills.values()))
check("...and the same for the rewrites that are not slices at all: `max` - "
"the FULLEST pack, which is `binding_soc_floor()`'s reduction pasted onto "
"the wrong axis and the single most likely wrong edit here - the mean, "
"the median and the second-emptiest each disagree with the shipped answer "
"on at least one run. `max(vals)` on the index-2 run answers 60 % where "
"the site is graded at 11 %",
all(_soc_red_kills.values()))
check("...and the baseline the tables are measured against is the shipped "
"property's OWN answer, not a re-derivation: on every run, the SoC that "
"reached `decide()` equals `Controller.pack_floor_soc` on the roster that "
"cycle carried. If it did not, every kill list above would be measuring "
"the wrong thing",
all(dec.soc == ctl.pack_floor_soc for _rn, ctl, dec in _S_RUNS)
and len(_S_RUNS) == 6
and sorted(len(ctl.inverters) for _rn, ctl, dec in _S_RUNS)
== [2, 2, 3, 3, 3, 3])
# ⚠️ AND THE HONEST LIMIT OF EVERY RUN ABOVE, FOUND BY THIS ROUND'S OWN BATTERY
# AND NOT ARGUED FROM: `vals[:3]` and `vals[-3:]` SURVIVE all six behavioural
# runs, for the same reason `[:2]` survived the two-pack ones - THE LONGEST
# ROSTER HERE IS THREE, and a slice that keeps three consecutive positions IS
# the roster at N=3. Adding a four-pack cycle would close those two and hand
# `[:4]` and `[-4:]` the same free pass. THE REGRESS DOES NOT TERMINATE IN A
# FIXTURE - round 3's lesson, arriving unchanged on this axis.
#
# So the same two halves, each saying what it reaches. First the SWEEP, which is
# cheap here because `pack_floor_soc` is a PROPERTY and needs no cycle to
# evaluate: rosters of 1 to 12 real `Inverter`s, the empty pack at EVERY
# position of each, and the property must answer the emptiest every time. That
# kills every fixed-width head or tail window narrower than the longest roster
# at once, rather than one mutation at a time.
_S_SWEEP_N = 12
def _roster_of(socs):
"""A real Controller carrying one real `Inverter` per reading given."""
ctl, _hs = built(TMP / f"s32sw{len(socs)}.db")
for _i in range(len(socs) - 1):
second_inverter(ctl, name=f"sweep-{_i}", setpoint=f"number.sw_{_i}")
for inv, so in zip(ctl.inverters, socs):
inv.soc, inv.batt, inv.bms_soc_floor = so, 0.0, A_PACK_FLOOR
return ctl
_s_sweep_rows = []
for _n in range(1, _S_SWEEP_N + 1):
for _pos in range(_n):
_sc = [_S_FULL_SIM] * _n
_sc[_pos] = _S_EMPTY_SIM
_s_sweep_rows.append((_n, _pos, tuple(_sc),
_roster_of(_sc).pack_floor_soc))
_s_sweep_bad = [(n, p, sc, got) for n, p, sc, got in _s_sweep_rows
if got != min(sc)]
_s_head_kill = {w: any(min(sc[:w]) != min(sc) for _n, _p, sc, _g in _s_sweep_rows)
for w in range(1, _S_SWEEP_N + 1)}
_s_tail_kill = {w: any(min(sc[-w:]) != min(sc) for _n, _p, sc, _g in _s_sweep_rows)
for w in range(1, _S_SWEEP_N + 1)}
print(f" LENGTH SWEEP on the real property (all SIMULATED past pack A): "
f"{len(_s_sweep_rows)} rosters, lengths 1..{_S_SWEEP_N}, the "
f"{_S_EMPTY_SIM:g} % pack at EVERY position of each among "
f"{_S_FULL_SIM:g} % siblings -> "
f"{len(_s_sweep_rows) - len(_s_sweep_bad)} answer the emptiest, "
f"{len(_s_sweep_bad)} do not")
print(" fixed-width windows killed by the sweep: head widths "
+ ",".join(str(w) for w in sorted(_s_head_kill) if _s_head_kill[w])
+ "; tail widths "
+ ",".join(str(w) for w in sorted(_s_tail_kill) if _s_tail_kill[w])
+ f"; NOT reached: head/tail width {_S_SWEEP_N} and wider")
check("⚠️ THE LENGTH SWEEP, on real `Controller.pack_floor_soc`. Over rosters "
"of 1 to 12 real inverters with the empty pack at every position of each, "
"the property answers the EMPTIEST every time - so every head window "
"`min(vals[:w])` and every tail window `min(vals[-w:])` of width 1 to 11 "
"disagrees with it on at least one roster here, and that is the whole "
"positional family below width 12 closed in one property rather than one "
"mutation at a time. ⚠️ THIS IS THE REAL PROPERTY ON A REAL ROSTER, not a "
"re-implementation: `_roster_of()` appends real `Inverter` objects and "
"reads `ctl.pack_floor_soc`, so it also sees a rewrite of the "
"comprehension that builds `vals`",
not _s_sweep_bad
and len(_s_sweep_rows) == _S_SWEEP_N * (_S_SWEEP_N + 1) // 2
and all(_s_head_kill[w] and _s_tail_kill[w]
for w in range(1, _S_SWEEP_N)))
check("⚠️ ...AND WIDTH 12 AND WIDER IS HONESTLY NOT REACHED, recorded rather "
"than papered over - the same ceiling 12c2's sweep records on the floor "
"axis. No roster in this file is longer than 12, so `min(vals[:12])` IS "
"`min(vals)` on every one of them and no check here can tell them apart. "
"A 13-pack roster would close width 12 and hand width 13 the same free "
"pass. That is why the STRUCTURAL PIN below exists alongside these runs, "
"and this line asserts the unreachedness itself so a later sweep that "
"lengthens the rosters takes it red and forces the sentence rewritten",
not _s_head_kill[_S_SWEEP_N] and not _s_tail_kill[_S_SWEEP_N]
and max(len(sc) for _n, _p, sc, _g in _s_sweep_rows) == _S_SWEEP_N)
# ...and the NONE GUARD is a reduction over roster length TOO, and it had
# exactly the same fixture problem: 12d sets the SECOND of two packs to None, so
# `any(v is None for v in vals[:1])` - grade only pack A for silence - answers
# "all present" there and hands a list containing None to `min()`. The sweep
# below moves the silent pack along the roster at every length, which is the
# data that reaches it. THE DIRECTION MATTERS: None must WIN, because a silent
# pack is a pack nothing can grade and `decide()` treats None as floor-limited.
def _pfs_answer(ctl):
"""`pack_floor_soc`, with a raise reported as an ANSWER rather than
ending the file. A guard narrowed to a slice does not return a wrong
number - it lets a None reach `min(float(v) ...)` and raises TypeError -
and a check that dies is a check that names nothing."""
try:
return ctl.pack_floor_soc
except Exception as exc: # noqa: BLE001 - reported
return f"raised {type(exc).__name__}"
_s_none_rows = []
for _n in range(1, _S_SWEEP_N + 1):
for _pos in range(_n):
_ctl_n = _roster_of([_S_FULL_SIM] * _n)
_ctl_n.inverters[_pos].soc = None
_s_none_rows.append((_n, _pos, _pfs_answer(_ctl_n)))
print(f" NONE-GUARD SWEEP: {len(_s_none_rows)} rosters, ONE silent pack at "
f"every position of every length 1..{_S_SWEEP_N} -> "
f"{sum(1 for _n, _p, g in _s_none_rows if g is None)} answer None")
check("⚠️ THE NONE GUARD IS THE SAME FAMILY. One silent pack at ANY position of "
"ANY roster length makes `pack_floor_soc` None - never a number, and "
"never the minimum of the packs that did answer. A guard narrowed to a "
"positional slice would let the site spend the packs that answered while "
"one sat unmeasured; 12d's two-pack fixture, with the SECOND pack silent, "
"cannot see a guard narrowed to the first",
all(g is None for _n, _p, g in _s_none_rows)
and len(_s_none_rows) == _S_SWEEP_N * (_S_SWEEP_N + 1) // 2)
# ⚠️ THE STRUCTURAL HALF: the reduction's ARGUMENT, pinned EXACTLY, at any N.
# This is 12g's treatment applied inside a property rather than at a call site,
# and it is NOT a substring test - `"vals" in src` is true of every slice of
# `vals`, whereas `ast.unparse(iter) == "vals"` is false of all of them.
# `vals[:3]` unparses to `vals[:3]`.
#
# THREE expressions are pinned, because there are three reductions over roster
# length in this one property and the runs above reach them unevenly:
# the LIST `[inv.soc for inv in self.inverters]` - the roster itself
# the GUARD `any(v is None for v in vals)`
# the REDUCTION `min(float(v) for v in vals)`
#
# ⚠️ AND THE PIN IS BLIND TO WHAT THE RUNS SEE, which is why both halves are
# here. It cannot see `min` become `max` (identical shape, different name -
# killed by the roster set), it cannot see `inv.soc` become `inv.bms_soc_floor`
# (killed by the runs' held-flat cut-outs), and it cannot see a change inside
# `Inverter.soc` at all.
_pfs_fn = next(n for n in ast.walk(ast.parse(_mainsrc))
if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef))
and n.name == "pack_floor_soc")
_pfs_vals = next(n.value for n in _pfs_fn.body
if isinstance(n, ast.Assign)
and [ast.unparse(t) for t in n.targets] == ["vals"])
def _gen_iter_of(fn_node, name):
"""The iterable of the comprehension inside the single `name(...)` call."""
call = next(n for n in ast.walk(fn_node)
if isinstance(n, ast.Call) and isinstance(n.func, ast.Name)
and n.func.id == name)
comp = call.args[0]
return comp.generators[0].iter if len(comp.generators) == 1 else None
_pfs_min_iter = _gen_iter_of(_pfs_fn, "min")
_pfs_any_iter = _gen_iter_of(_pfs_fn, "any")
print(f" pack_floor_soc reduces: list over `{ast.unparse(_pfs_vals.generators[0].iter)}`"
f", any() over `{ast.unparse(_pfs_any_iter)}`, min() over "
f"`{ast.unparse(_pfs_min_iter)}`")
check("⚠️ THE STRUCTURAL HALF, EXACT RATHER THAN A SUBSTRING. `min()` reduces "
"the WHOLE list `vals` - a bare Name, not a Subscript, not a call, not a "
"second comprehension - and `vals` is built by ONE comprehension over the "
"WHOLE property `self.inverters`, an attribute of self and not a slice of "
"one. So EVERY slice of either is red here at EVERY roster length, "
"including `vals[:3]` and `vals[-3:]`, which survive all six runs above "
"because the longest roster they drive is three packs long, and including "
"`self.inverters[:2]` in the comprehension, which the sweep sees only up "
"to length 12. This grades the code's SHAPE and nothing about the values; "
"what the expression MEANS is graded by the runs",
isinstance(_pfs_min_iter, ast.Name) and _pfs_min_iter.id == "vals"
and ast.unparse(_pfs_min_iter) == "vals"
and isinstance(_pfs_vals, ast.ListComp)
and len(_pfs_vals.generators) == 1
and not _pfs_vals.generators[0].ifs
and isinstance(_pfs_vals.generators[0].iter, ast.Attribute)
and isinstance(_pfs_vals.generators[0].iter.value, ast.Name)
and _pfs_vals.generators[0].iter.value.id == "self"
and _pfs_vals.generators[0].iter.attr == "inverters"
and ast.unparse(_pfs_vals.generators[0].iter) == "self.inverters")
check("...and the NONE GUARD reduces that same whole list, pinned the same way. "
"`any(v is None for v in vals[:1])` is +4 bytes and leaves every other "
"line in this file identical; the sweep above reaches it up to length 12 "
"and this reaches it at every length",
isinstance(_pfs_any_iter, ast.Name) and _pfs_any_iter.id == "vals"
and ast.unparse(_pfs_any_iter) == "vals")
check("...and the counterfactual that shows these pins are not vacuous (habit "
"7): the very slices they exist for really do unparse to something else, "
"so the equalities above are doing work rather than being trivially true "
"of anything shaped like an argument",
all(ast.unparse(ast.parse(f"vals{sl}", mode="eval").body) != "vals"
and ast.unparse(ast.parse(f"self.inverters{sl}",
mode="eval").body) != "self.inverters"
for sl in ("[:1]", "[-1:]", "[:2]", "[-2:]", "[:3]", "[-3:]",
"[:12]", "[-12:]", "[1:]", "[:-1]", "[1:-1]", "[::2]"))
and ast.unparse(ast.parse("list(reversed(vals))",
mode="eval").body) != "vals")
# ...and BOTH consumers of this property are pinned to it by name, so a reduction
# hardened here cannot be bypassed by a call site that stops using it. The peak
# guard's `soc=` is already pinned in part 4; the OTHER one is arbitrage's, and
# it is a safety consumer too - `soc_floor_pct` is what stops a sell.
_arb_calls = [n for n in ast.walk(ast.parse(_mainsrc))
if isinstance(n, ast.Call) and isinstance(n.func, ast.Attribute)
and n.func.attr == "decide"
and isinstance(n.func.value, ast.Name)
and n.func.value.id == "arbitrage"]
_arb_soc = ast.unparse(next(k.value for k in _arb_calls[0].keywords
if k.arg == "soc"))
print(f" main.py's arbitrage.decide(soc=...) is `{_arb_soc}` "
f"({len(_arb_calls)} call site)")
check("⚠️ AND THE SECOND CONSUMER OF THE SAME REDUCTION. `arbitrage.decide()` "
"is handed the SAME `self.pack_floor_soc` and grades it against "
"`soc_floor_pct` to decide whether to SELL - so the mutation this block "
"exists for spends a pack on the arbitrage path as well as the peak-guard "
"path, and hardening the property covers both. A call site that reverted "
"to a per-unit reading, or defaulted it, would take this line red",
len(_arb_calls) == 1 and _arb_soc == "self.pack_floor_soc")
# --- 12j. THE ENUMERATION ITSELF, AS A RUNNING CHECK -------------------------
# ⚠️ THIS CARD HAS NOW BEEN REJECTED THREE TIMES FOR THE SAME SHAPE ONE STEP
# OVER: a reduction over roster length whose fixture could not separate it from
# a slice of the roster. Round 2 hardened `binding_soc_floor()`; round 3 found
# the call site's argument; round 4's blocker was `pack_floor_soc`, the SIBLING
# ARGUMENT of the same `decide()` call. Closing them one at a time has cost four
# rounds, so this block stops enumerating them by hand.
#
# EVERY comprehension in `Controller` that iterates the roster is listed here,
# COMPUTED from main.py's AST, PRINTED, and required to iterate the roster
# WHOLE. That is the family: `min`, `max`, `sum`, `any`, `all`, `sorted` and the
# list builders all take their roster through one of these, so a `[:1]`, a
# `[-1:]` or a `[:2]` anywhere in the class takes this line red on the line it
# was written - at every roster length, in every configuration, whether or not
# any fixture in this repository reaches it.
#
# ⚠️ WHAT IT DOES NOT COVER, said plainly. It grades the ITERABLE and nothing
# else: not which attribute is read off each unit, not which reduction is
# applied, and not the direct index sites (`self.inverters[0]` in `_only()`,
# `actuator_of()` and the store migration, `self.inverters[self.maint_index]` in
# `maint_unit`), which are not comprehensions and are graded where they live.
# The behavioural halves above are what say what these reductions MEAN.
_ctrl_cls = next(n for n in ast.walk(ast.parse(_mainsrc))
if isinstance(n, ast.ClassDef) and n.name == "Controller")
_ROSTER_ATTRS = ("inverters", "packs")
def _is_roster(node):
return (isinstance(node, ast.Attribute) and node.attr in _ROSTER_ATTRS
and isinstance(node.value, ast.Name) and node.value.id == "self")
def _mentions_roster(node):
return any(_is_roster(n) for n in ast.walk(node))
_roster_comps = []
for _fn in ast.walk(_ctrl_cls):
if not isinstance(_fn, (ast.FunctionDef, ast.AsyncFunctionDef)):
continue
for _n in ast.walk(_fn):
if isinstance(_n, (ast.ListComp, ast.SetComp, ast.DictComp,
ast.GeneratorExp)):
for _g in _n.generators:
if _mentions_roster(_g.iter):
_roster_comps.append((_fn.name, _g.iter.lineno,
ast.unparse(_g.iter)))
_roster_comps.sort(key=lambda r: (r[1], r[2]))
def _slices_roster(node):
"""Is the roster SUBSCRIPTED anywhere inside this expression?
⚠️ THE TEST IS ON THE SUBSCRIPT, NOT ON THE SPELLING. `enumerate(...)`,
`zip(...)` and `pack_permissions(roster, floor)` all hand the WHOLE roster
on and are legitimate; `self.inverters[:1]` is the family this card has been
rejected over three times. Requiring the iterable to unparse to exactly
`self.inverters` would refuse the first three and grade the spelling instead
of the thing.
"""
return any(isinstance(n, ast.Subscript) and _is_roster(n.value)
for n in ast.walk(node))
_sliced = [r for r in _roster_comps
if _slices_roster(ast.parse(r[2], mode="eval").body)]
print(f" EVERY comprehension in Controller that iterates the roster "
f"({len(_roster_comps)} of them, in "
f"{len({m for m, _l, _s in _roster_comps})} methods), and what each "
f"iterates:")
for _mname, _ln, _src in _roster_comps:
print(f" main.py:{_ln:<5} {_mname:<26} for ... in {_src}"
+ ("" if _src in ("self.inverters", "self.packs")
else " [whole roster, wrapped]"))
check("⚠️ THE FAMILY, ENUMERATED FROM THE SOURCE RATHER THAN FROM MEMORY. Every "
"comprehension in `Controller` that iterates the roster iterates it "
"WHOLE - `self.inverters` or `self.packs`, bare. This is the check that "
"would have caught rounds 2, 3 and 4's blockers on the round they were "
"written instead of one review each: `min(... for v in vals[:2])` is not "
"here (it reduces a derived list, which 12i pins directly), but every "
"`for inv in self.inverters[:1]` in the class is. A new reduction added "
"over a slice of the roster is red on the line it is written, at every "
"roster length and with no fixture required. ⚠️ THE ITERABLE ONLY - what "
"is read off each unit and what reduction is applied are graded by the "
"runs, and the direct index sites are not comprehensions and not here",
len(_roster_comps) > 0 and not _sliced)
check("...and the enumeration is not vacuously empty and its predicate is not "
"trivially true (habit 7): it found more than twenty roster "
"comprehensions across more than fifteen methods, it REJECTS the same "
"expressions with a slice on them - bare and wrapped in `enumerate()` "
"alike - and it ACCEPTS the three legitimate wrapped forms this class "
"actually uses. A predicate that answered the same thing for both would "
"make the line above a description rather than a check",
len(_roster_comps) >= 20
and len({m for m, _l, _s in _roster_comps}) >= 15
and all(_slices_roster(ast.parse(f"self.{a}{sl}", mode="eval").body)
and _slices_roster(ast.parse(f"enumerate(self.{a}{sl})",
mode="eval").body)
for a in _ROSTER_ATTRS
for sl in ("[:1]", "[-1:]", "[:2]", "[1:]", "[:-1]", "[::2]"))
and not any(_slices_roster(ast.parse(e, mode="eval").body)
for e in ("self.inverters", "self.packs",
"enumerate(self.inverters)",
"peakguard.pack_permissions(self.packs, f)")))
# ⚠️ AND THE TWO THIS ENUMERATION ACTUALLY FOUND, recorded here because the
# lesson is the finding and not the fix. Mutating every roster reduction in
# `Controller` to `[:1]` one at a time turned up TWO that survived a green
# 19-file suite, both `any(i.sign_wd.tripped for i in self.inverters)` and both
# SAFETY-30's rather than this card's:
#
# `cycle()`'s stale-input hold +4 bytes, 19/19 green - a LATCHED unit must
# take the roster past the hold, or its failsafe SET stops being re-filed
# the moment a poll is missed. Graded now in test_watchdog.py part 8, on
# a two-inverter roster with the latch at EACH position in turn.
# `publish()`'s status vocabulary +4 bytes, 19/19 green - a site with one
# inverter parked by its own failsafe claim published `running`. Same
# part, same two positions.
#
# Both were invisible for the same reason every blocker on this card was
# invisible: the only fixture that reached them was a roster of ONE, where
# `self.inverters[:1]` IS `self.inverters`.
# ==============================================================================
print()
print("11. mutation: what defeats these guards, and which check bites")
@@ -4512,6 +5096,74 @@ MUT.append(("`binding_soc_floor()` reduced over a window 12 packs wide or "
"max is order- and duplicate-invariant; the second is nonetheless "
"red on 12g's structural pin"))
# --- ROUND 4: THE SIBLING ARGUMENT, AND THE ENUMERATION THAT FOUND THE REST --
# 28. `pack_floor_soc` reduced over a WINDOW of roster positions. THIS ROUND'S
# BLOCKER, and the same family as rows 25-26 one ARGUMENT over: the MINIMUM
# handed to the same `decide()` call as `soc=`. Green across all 19 files at
# 52380c0 and again on the rebase onto 3934ad7.
MUT.append(("`pack_floor_soc` reduced over a WINDOW - `vals[:2]` (+4 bytes) or "
"`vals[-2:]` (+5) - ROUND 4's BLOCKER, the SoC axis of the pair",
all(_soc_slice_kills[k] for k in ("vals[:2]", "vals[-2:]",
"vals[:1]", "vals[-1:]")),
"part 12i's THREE-pack runs and their POSITIONAL-SLICE TABLE: the "
"same three readings with the emptiest at index 0, 1 and 2, so "
"every proper subset of the positions drops the empty pack on some "
"run. ⚠️ At N=2 `[:2]` IS the roster - the two-pack mirror cannot "
"see it, which is why the suite's separating power on this axis "
"stopped at exactly N=2 for four rounds"))
# 29. ...and the three-wide window, invisible to every behavioural run here.
MUT.append(("`pack_floor_soc` reduced over a THREE-wide window - `vals[:3]` / "
"`vals[-3:]` - invisible to every cycle in this file",
_s_head_kill[3] and _s_tail_kill[3]
and ast.unparse(_pfs_min_iter) == "vals",
"part 12i's LENGTH SWEEP (widths 1-11, on the REAL property over "
"rosters of 1 to 12 real inverters) and its STRUCTURAL PIN. The "
"six real cycles cannot see it - three packs is the longest roster "
"they drive - and the sweep and the pin are what do"))
# 30. the reduction itself, on the wrong axis. Zero byte delta.
MUT.append(("`pack_floor_soc` reduced with `max()` - the FULLEST pack, which is "
"`binding_soc_floor()`'s own reduction pasted onto the SoC axis - "
"or with the mean, the median or the second-emptiest",
all(_soc_red_kills.values()),
"part 12i's NON-SLICE table, on the same six real cycles: the "
"emptiest pack is 11 % where the fullest is 60 %, so every one of "
"the four disagrees on every run. ⚠️ Zero byte delta, and the most "
"likely wrong edit here precisely because the sibling reduction one "
"argument over IS a max"))
# 31. the None guard, which is a reduction over roster length too.
MUT.append(("`pack_floor_soc`'s NONE GUARD narrowed to a slice - "
"`any(v is None for v in vals[:1])`, +4 bytes",
all(g is None for _n, _p, g in _s_none_rows)
and ast.unparse(_pfs_any_iter) == "vals",
"part 12i's NONE-GUARD SWEEP - one silent pack at every position of "
"every length 1..12 - and the same structural pin. ⚠️ 12d's "
"two-pack fixture silences the SECOND pack only, so a guard "
"narrowed to the FIRST answers 'all present' there and hands a None "
"to `min()`"))
# 32. and the roster the list itself is built from.
MUT.append(("`pack_floor_soc` building `vals` from a SLICE of the roster - "
"`[inv.soc for inv in self.inverters[:2]]`, +4 bytes",
ast.unparse(_pfs_vals.generators[0].iter) == "self.inverters"
and _s_head_kill[2] and not _sliced,
"part 12i's sweep reads the REAL property on real rosters so it "
"sees this too, its pin is on the comprehension's own iterable, "
"and 12j's ENUMERATION covers every other comprehension in the "
"class at once"))
# 33. THE ENUMERATION'S OWN FINDING, and the reason this row is worth more than
# the four above: it is not this card's code.
MUT.append(("a reduction over the roster added ANYWHERE ELSE in `Controller` "
"over a slice - which is how rounds 2, 3 and 4 each began",
bool(_roster_comps) and not _sliced,
"part 12j: every roster comprehension in the class, enumerated "
"from main.py's AST and required to iterate the roster whole. ⚠️ "
"Mutating each of them to `[:1]` found FOUR that survived a green "
"19-file suite, none of them this card's: three `sign_wd.tripped` "
"reductions (SAFETY-30's stale-input hold, `publish()`'s status "
"vocabulary and `status_payload()`'s banner - now graded in "
"test_watchdog.py part 8) and the store-migration guard's redundant "
"second disjunct. THIS row is the one that closes the family "
"rather than the member"))
for label, caught, where in MUT:
print(f" {'caught' if caught else 'NOT CAUGHT':>10} {label}")
print(f" {where}")
@@ -4546,7 +5198,7 @@ check("...⚠️ AND SO ARE THE SEVEN DEFEATS THAT SURVIVED A GREEN 19-FILE SUIT
"consecutive cards and, twice running, inside the fix written to close "
"it. The last five were found by a battery rather than by argument, "
"which is the only thing that has ever found any of them",
all(m[1] for m in MUT[21:27]) and len(MUT) == 28)
all(m[1] for m in MUT[21:27]) and len(MUT) == 34)
check("...⚠️ AND ONE IS RECORDED AS NOT CAUGHT, which is the only honest row "
"this round could write for it: a reduction over a window TWELVE packs "
"wide is indistinguishable from `max(floors)` on every roster in this "
@@ -4559,6 +5211,20 @@ check("...⚠️ AND ONE IS RECORDED AS NOT CAUGHT, which is the only honest row
"with, and it exists to be argued with",
not MUT[27][1]
and all(_head_kill[w] and _tail_kill[w] for w in range(1, _SWEEP_N)))
check("...⚠️ AND ROUND 4's SIX, WHICH ARE THE SAME FAMILY AT THE SIBLING "
"ARGUMENT OF THE SAME CALL. `pack_floor_soc` is the MINIMUM over the "
"roster handed to `decide(soc=...)`; `binding_soc_floor()` is the MAXIMUM "
"handed to `decide(soc_floor=...)` beside it, and peakguard.py's own "
"docstring names them as a pair. Rounds 2 and 3 hardened one half; "
"`vals[:2]`, `vals[-2:]` and `vals[:3]` were still green on 52380c0 with "
"the other, and a three-pack roster at 50 / 60 / 11 % answers 11 % clean "
"and 50 % mutated - a discharge PERMISSION graded against a pack 39 "
"points fuller than the emptiest. The last row is the one that matters "
"most: closing the MEMBER four times has cost four review rounds, so the "
"FAMILY is now enumerated from the source, and that enumeration found "
"four more survivors in code this card never touched",
all(m[1] for m in MUT[28:34]) and len(MUT) == 34
and _S_EMPTY_SIM < SOC_FLOOR)
check("...and so is the last one, which is not a defeat at all but the EDGE OF "
"THIS CARD: the grid-following loop can still take the pack past this "
"floor, because the floor bounds the peak guard's own forced discharge "