SAFETY-31 AC 4: the alarm's attribution was graded by nothing, and a mutation proved it
checks / unit (pull_request) Successful in 6m27s
checks / unit (pull_request) Successful in 6m27s
`pos, neg = opposing(...)` -> `([], [])` in main.py's `check_circulating()`
(-102 B) deletes the "Right now X is discharging while Y charges" clause from
the operator notification, and SURVIVED ALL 26 FILES. The commit that shipped
this card claimed "18 mutations, all RED"; the ledger has no entry for the
attribution, and this is that entry.
It is not dead code. Ledger M18 - the whole `await check_circulating()` call
removed - is red on four checks, so the function runs. The three checks on the
notification asserted METER CANNOT SEE, CONTROL HAS BEEN STOPPED and `isolator`,
and never an inverter name. `circulation.py`'s own docstring calls that sentence
the first thing somebody woken by this alarm needs and "impossible to
reconstruct from the number alone".
test_watchdog.py section 6a closes it on a fixture that can tell three failures
apart:
* the roster is ["garage", "shed", "cellar"] - operator names, none of them
`UNIT_A`, for test_maintenance.py section 11's reason: on a fixture whose
names are the module's own constants a check cannot tell the roster from
the constant;
* the site is run TWICE, MIRRORED, so the sentence is not symmetric and a
pos/neg swap is a different red from a deletion;
* a third pack rests at 5 W, so the floor that stops a pack in standby being
named as a participant in a 6 kW loop is graded too.
Mutations, on a tree with no __pycache__ mounted :ro into the checks image:
M20 pos, neg = opposing(...) -> ([], []) -102 B RED (2 checks)
M21 pos and neg exchanged +0 B RED (3 checks)
M22 opposing(..., min_w=cfg.min_w/2.0) -> 0.0 -25 B RED (3 checks)
26/26 files green with the block in place.
AND A DISCLOSURE, WHICH IS THE UNCOMFORTABLE HALF. DECISIONS section 3 records
that the 924-run campaign behind `circulation_w: 700` and `circulation_hold_s:
75` used a harness that is not committed in either repository. Two further
sweeps have now been run and no two of the three agree: peaks of 1700 W, 2750 W
and 1048 W, and worst-continuous figures above 700 W of 5.0 s, 15.0 s and 5.0 s.
That is NOT evidence of fabrication and is not reported as any. The committed
sweep measures why the spread is that wide: on one grid, changing only the
plant model and the write-stagger set, the worst legitimate peak is 1048 W, or
2250 W, or 0 W. The plant and the stagger are TERMS of the number. What the
spread does establish is that the claim cannot be adjudicated while the
instrument is missing.
The window argument survives and is now committed - all three sweeps trip zero
times, on 924 + 48 + 840 non-faulty runs. The threshold CURVE does not survive
as stated: `505.0 s at 100 W`, which is what the "knee at 400 W" argument rests
on, is 10.0 s on the committed grid. The curve still falls with the threshold,
so the knee is real in direction; its depth is established by nothing anybody
can run, and this commit says so rather than repeating it. The overclaiming
comment at test_watchdog.py's 1700 W fixture ("the worst single transient the
rig can produce") is replaced with what the fixture can support.
The committed, re-runnable half is admin/goodwe's `two_master_circulation`,
opened as a separate PR against that repository.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU
This commit is contained in:
co-authored by
Claude Opus 5
parent
6e7b23388c
commit
295a2aedb6
@@ -722,18 +722,41 @@ print(f" e.g. {_sustained.reason!r}")
|
||||
|
||||
print()
|
||||
print(" ⚠️ AND NOT ON THE TRANSIENT, WHICH IS THE FALSE-ALARM CASE")
|
||||
# ⚠️ THE SHAPES BELOW ARE THE RIG'S, NOT INVENTED. Measured on TEST-19 over 924
|
||||
# closed-loop runs in which every unit executed the share it was given: the
|
||||
# worst continuous opposition above 700 W is 5.0 s - ONE control cycle - at a
|
||||
# peak of 1700 W. That is the serialised write path: on a reversing cycle B's
|
||||
# new number lands up to a whole cycle after A's, both slew through zero at
|
||||
# 800 W/s behind a 10 s lag, and between the two crossings they genuinely
|
||||
# oppose. A guard that trips on that is a false-alarm generator, and this
|
||||
# project's doctrine on crying wolf is explicit (SAFETY-15, SAFETY-19).
|
||||
# THE SHAPE BELOW IS THE RIG'S: the serialised write path means that on a
|
||||
# reversing cycle B's new number lands up to a whole control cycle after A's,
|
||||
# both slew through zero behind a real ramp and a real lag, and between the two
|
||||
# crossings they genuinely oppose. A guard that trips on that is a false-alarm
|
||||
# generator, and this project's doctrine on crying wolf is explicit (SAFETY-15,
|
||||
# SAFETY-19).
|
||||
#
|
||||
# ⚠️ THE MAGNITUDE IS A FIXTURE INPUT, NOT A MEASUREMENT, AND THIS COMMENT USED
|
||||
# TO SAY OTHERWISE. It read "the worst single transient the rig can produce -
|
||||
# 1700 W", sourced to a 924-run campaign whose harness IS NOT COMMITTED IN
|
||||
# EITHER REPOSITORY (DECISIONS/SAFETY-31-circulating-power.md §3 says so in its
|
||||
# own words). Three sweeps have now been run against that claim and no two agree:
|
||||
#
|
||||
# 924-run campaign, harness uncommitted peak 1700 W, 505.0 s at 100 W
|
||||
# independent reviewer sweep, 2026-09-07 peak 2750 W, 15.0 s above 700 W
|
||||
# goodwe `two_master_circulation`, 840 runs peak 1048 W, 5.0 s above 700 W
|
||||
#
|
||||
# ⚠️ THAT IS NOT EVIDENCE THAT ANY OF THEM IS WRONG. The three swept different
|
||||
# grids, and the last one measured WHY the spread is so wide: on its own grid the
|
||||
# worst legitimate peak is 1048 W on the modelled plant, 2250 W on `INSTANT`, and
|
||||
# 0 W on `INSTANT` with no write stagger reaching a whole cycle. The plant model
|
||||
# and the stagger set are TERMS of the number, and a worst case quoted without
|
||||
# them is not a measurement. It is evidence that the claim cannot be adjudicated
|
||||
# while the instrument is missing.
|
||||
#
|
||||
# So what this check now says is what it can support: 1700 W is LARGER than
|
||||
# anything any of the three sweeps produced legitimately, and one control cycle
|
||||
# of it does not trip. The committed, re-runnable version of the claim is
|
||||
# `admin/goodwe`'s `sim/inject.py two_master_circulation`, which runs on every
|
||||
# pull request there and requires ZERO of ~840 non-faulty runs to trip.
|
||||
_one_cycle = [0.0] * 5 + [1700.0] + [0.0] * 30
|
||||
_t1, _ = circ_drive(_one_cycle)
|
||||
check("the worst single transient the rig can produce - 1700 W for ONE control "
|
||||
"cycle - does not trip", not _t1.tripped)
|
||||
check("a single transient of 1700 W - larger than the worst peak any committed "
|
||||
"sweep of the legitimate family has produced - lasting ONE control cycle "
|
||||
"does not trip", not _t1.tripped)
|
||||
_repeated = ([0.0] * 3 + [1700.0]) * 20
|
||||
_t2, _ = circ_drive(_repeated)
|
||||
check("...and neither does it REPEATED every fourth cycle for 400 s, which is "
|
||||
@@ -1091,6 +1114,158 @@ check("...and it does not raise the same card on every cycle - a per-PROCESS "
|
||||
"this' a different question from 'is this site tripped'",
|
||||
len(_notes2) == 1)
|
||||
|
||||
|
||||
print()
|
||||
print("6a. AC 4: THE ALARM NAMES WHICH PACKS, AND WHICH WAY ROUND")
|
||||
# ⚠️ THIS BLOCK EXISTS BECAUSE A MUTATION SURVIVED ALL 26 FILES. Replacing
|
||||
#
|
||||
# pos, neg = opposing([inv.batt for inv in self.inverters],
|
||||
# min_w=self.circ_wd.cfg.min_w / 2.0)
|
||||
#
|
||||
# in `check_circulating()` with `pos, neg = ([], [])` (-102 B) silently deletes
|
||||
# the "Right now X is discharging while Y charges" clause from the notification,
|
||||
# and the whole suite stayed GREEN. The block above asserts three tokens -
|
||||
# METER CANNOT SEE, CONTROL HAS BEEN STOPPED, isolator - and NEVER an inverter
|
||||
# name, so the one sentence `circulation.py` calls "the first thing somebody
|
||||
# woken by this alarm needs" and "impossible to reconstruct from the number
|
||||
# alone" was graded by nothing at all.
|
||||
#
|
||||
# ⚠️ IT IS NOT DEAD CODE, WHICH IS THE PART THAT MAKES IT A CHECK GAP RATHER
|
||||
# THAN A REACHABILITY ONE. Ledger M18 - the `await self.check_circulating()`
|
||||
# call removed - goes red on four checks above, so this function runs. What
|
||||
# nothing asserted was the ATTRIBUTION.
|
||||
#
|
||||
# ⚠️ AND THE ROSTER IS ["garage", "shed", "cellar"], WHICH IS THE OTHER HALF OF
|
||||
# THE FIX. Every fixture above runs on `inverter-a`/`inverter-b`;
|
||||
# test_maintenance.py section 11 records what that costs - on a fixture whose
|
||||
# names are the module's own constants, a check cannot tell the roster from the
|
||||
# constant, and `UNIT_A` is not even what a nameless entry falls back to. Here
|
||||
# the names are an operator's, the two participants pull OPPOSITE ways so the
|
||||
# sentence is not symmetric, and a third pack rests at 5 W. That makes three
|
||||
# DIFFERENT mutations - the clause dropped, the two directions swapped, and the
|
||||
# `min_w` floor dropped - into three DIFFERENT red checks. A fixture naming both
|
||||
# units the same, or one in which the direction did not matter, would see the
|
||||
# first and neither of the others.
|
||||
|
||||
_CIRC_ROSTER = [
|
||||
dict(name="garage", setpoint_entity="number.sp_a", soc_entity="sensor.soc_a",
|
||||
batt_entity="sensor.batt_a", max_w=5000),
|
||||
# ⚠️ B_RATING_HYPOTHESIS, and part 4's note above says why it is not 4600:
|
||||
# inverter B does not physically exist and a datasheet is not a measurement.
|
||||
dict(name="shed", setpoint_entity="number.sp_b", soc_entity="sensor.soc_b",
|
||||
batt_entity="sensor.batt_b", max_w=B_RATING_HYPOTHESIS),
|
||||
# THE BYSTANDER. A third pack resting at standby draw, on the roster, read
|
||||
# every cycle, and NOT a participant in anybody's loop.
|
||||
dict(name="cellar", setpoint_entity="number.sp_c", soc_entity="sensor.soc_c",
|
||||
batt_entity="sensor.batt_c", max_w=B_RATING_HYPOTHESIS),
|
||||
]
|
||||
_CIRC_NAMES = [e["name"] for e in _CIRC_ROSTER]
|
||||
_REST_W = 5.0
|
||||
|
||||
|
||||
class _NamedCircHass(_Hass):
|
||||
"""Three named packs; the meter still reads exactly zero.
|
||||
|
||||
Same house as `_CircHass` - grid = load - sum(B) with two units cancelling -
|
||||
with one extra pack idling at standby draw so that "who is named" and "who
|
||||
is merely on the roster" are different questions.
|
||||
"""
|
||||
|
||||
def __init__(self, a_w, b_w, c_w=_REST_W):
|
||||
super().__init__()
|
||||
self.vals = {"sensor.batt_a": a_w, "sensor.batt_b": b_w,
|
||||
"sensor.batt_c": c_w}
|
||||
|
||||
async def number(self, entity, invert=False):
|
||||
return {"sensor.grid": 0.0, "sensor.soc_a": 50.0,
|
||||
"sensor.soc_b": 50.0, "sensor.soc_c": 50.0,
|
||||
**self.vals}.get(entity)
|
||||
|
||||
|
||||
def _named_trio(hass, store, *, hold_s=1, min_w=700):
|
||||
opts = {"meter_entity": "sensor.grid", "meter_source": "off",
|
||||
"max_w": 5000, "gain": 0.6, "auto_start": True,
|
||||
"circulation_w": min_w, "circulation_hold_s": hold_s,
|
||||
"inverters": _CIRC_ROSTER}
|
||||
c = Controller(opts, hass, store, _P(), stop=asyncio.Event())
|
||||
c.auto = True
|
||||
for inv in c.inverters:
|
||||
inv.dev_min, inv.dev_max = -5000.0, 5000.0
|
||||
return c
|
||||
|
||||
|
||||
async def _circ_named(a_w, b_w):
|
||||
hass = _NamedCircHass(a_w, b_w)
|
||||
c = _named_trio(hass, _MemStore())
|
||||
for _ in range(14):
|
||||
await c.read_inputs()
|
||||
await c.cycle()
|
||||
await asyncio.sleep(0.25)
|
||||
notes = [x for x in hass.calls
|
||||
if x[0] == "persistent_notification"
|
||||
and x[2].get("notification_id") == "goodwe_circulating_power"]
|
||||
return c, notes
|
||||
|
||||
|
||||
_cg, _notes_g = asyncio.run(_circ_named(+3000.0, -3000.0))
|
||||
_cs, _notes_s = asyncio.run(_circ_named(-3000.0, +3000.0))
|
||||
_msg_g = _notes_g[0][2]["message"] if _notes_g else ""
|
||||
_msg_s = _notes_s[0][2]["message"] if _notes_s else ""
|
||||
print(f" roster : {_cg.units}")
|
||||
print(f" garage discharging, measured: "
|
||||
f"{dict(zip(_cg.units, [i.batt for i in _cg.inverters]))}")
|
||||
print(f" -> {_msg_g.split(chr(10))[2] if _msg_g.count(chr(10)) > 2 else _msg_g!r}")
|
||||
print(f" shed discharging, measured: "
|
||||
f"{dict(zip(_cs.units, [i.batt for i in _cs.inverters]))}")
|
||||
print(f" -> {_msg_s.split(chr(10))[2] if _msg_s.count(chr(10)) > 2 else _msg_s!r}")
|
||||
|
||||
# --- THE PRECONDITIONS, AS THEIR OWN CHECKS (MUTATION.md rule 1) ------------
|
||||
check("PRECONDITION: the roster carries three DISTINCT operator-chosen names, "
|
||||
f"none of them `UNIT_A` ({UNIT_A!r}) - so every claim below is a claim "
|
||||
"about the roster and not about a constant that happens to equal it",
|
||||
_cg.units == tuple(_CIRC_NAMES) and len(set(_CIRC_NAMES)) == 3
|
||||
and UNIT_A not in _CIRC_NAMES)
|
||||
check("PRECONDITION: the two runs are MIRROR IMAGES - same house, same meter at "
|
||||
"0 W, same magnitudes, and the two participants swapped. Without this the "
|
||||
"direction claim below could be satisfied by a sentence that ignores the "
|
||||
"measurement entirely",
|
||||
[i.batt for i in _cg.inverters] == [+3000.0, -3000.0, _REST_W]
|
||||
and [i.batt for i in _cs.inverters] == [-3000.0, +3000.0, _REST_W]
|
||||
and _cg.grid == 0.0 and _cs.grid == 0.0)
|
||||
check("PRECONDITION: both runs actually tripped and actually raised the card, "
|
||||
"so an absent sentence below is an absent sentence and not an absent alarm",
|
||||
_cg.circ_wd.tripped and _cs.circ_wd.tripped
|
||||
and len(_notes_g) == 1 and len(_notes_s) == 1)
|
||||
|
||||
# --- AC 4: THE ATTRIBUTION ITSELF -------------------------------------------
|
||||
_WHO_G = "Right now garage is discharging while shed charges."
|
||||
_WHO_S = "Right now shed is discharging while garage charges."
|
||||
check("⚠️ THE ALARM NAMES THE TWO PACKS AND SAYS WHICH WAY EACH ONE IS PULLING. "
|
||||
"`circulation.py` calls this the first thing somebody woken at 3 a.m. "
|
||||
"needs and impossible to reconstruct from the watts alone, and until this "
|
||||
"line the sentence could be deleted with all 26 files green",
|
||||
_WHO_G in _msg_g)
|
||||
check("⚠️ AND WITH THE TWO SWAPPED, THE SENTENCE SWAPS. This is the half a "
|
||||
"single fixture cannot have: `pos` and `neg` exchanged, or a hard-coded "
|
||||
"sentence, satisfies the check above and fails this one. The operator is "
|
||||
"being told which pack to walk to",
|
||||
_WHO_S in _msg_s)
|
||||
check("...and each run carries ONLY its own sentence - the mirrored alarm does "
|
||||
"not also contain the other direction, so the clause is derived from this "
|
||||
"cycle's measurement rather than assembled from the roster",
|
||||
_WHO_S not in _msg_g and _WHO_G not in _msg_s)
|
||||
check("...and the RESTING pack is not named. `cellar` is on the roster, is read "
|
||||
"every cycle and is measured at 5 W of standby draw - `opposing()` is "
|
||||
"called with a floor of half the trip threshold precisely so a pack idling "
|
||||
"beside a 6 kW loop is not reported as one of its two participants",
|
||||
"cellar" not in _msg_g and "cellar" not in _msg_s
|
||||
and _cg.inverters[2].batt == _REST_W)
|
||||
check("...and the sentence sits with the figure, not instead of it: the same "
|
||||
"card still carries the watts, the window and the three things only this "
|
||||
"alarm can say",
|
||||
all("CIRCULATING_POWER" in m and "METER CANNOT SEE" in m
|
||||
and "CONTROL HAS BEEN STOPPED" in m and "isolator" in m
|
||||
for m in (_msg_g, _msg_s)))
|
||||
print()
|
||||
print(" the operator's press is the only way out, and it clears BOTH halves")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user