`app/main.py:1598`'s `site_standing = float(sum(standing.values()))` is the
first argument of SAFETY-30's latching, site-stopping sign detector AND the
`commanded_w` handed to SAFETY-18's `looks_frozen()`. It was pinned by nothing.
Coverage only: not one byte of `app/` moves - the shipped `sum` is correct.
WHY THE SUITE COULD NOT SEE IT. `_sign_site`, the only N>=2 site fixture in the
repository, runs shares of about -2500 W EACH - every one individually over its
320 W gate - so sum, mean and max(key=abs) all agree on it. Its values happen to
be equal, and a check comparing two things that happen to be equal cannot see
the difference it names. MUTATION.md habit 4, ninth instance, inside the site
half of the card that names the rule in its own text. At N=1 it is arithmetic
rather than a fixture defect: sum == mean == max == the one share, identically.
THE FIXTURE (test_watchdog.py part 9). Two SIMULATED units rated 220 W and
280 W, both obeying exactly, inverted gain, house importing 2000 W: the site
commands -500 W against a +2500 W meter and NO SINGLE SHARE clears the shipped
300 W gate. Four candidate reductions, four distinct answers, one over the gate
- sum -500, mean -250, max(key=abs) -280, [0] -220 - asserted as a precondition.
The asymmetry is load-bearing, and the smaller unit is first so [0] stays
distinct from max(key=abs). Gate shipped; only the hold is shortened.
BOTH CONSUMERS. 9b drives the same discrimination at SAFETY-18's threshold: the
site rail is 0.95 x 500 = 475 W, so the sum rails it while 250, 280 and 220 do
not. Shipped: BLIND_FROZEN, floor 0 W. Under both surviving mutations: not blind
at all, no floor filed - the battery may keep importing through a quarter whose
billed average is unobservable. The open quarter is backfilled with the stuck
reading so the mutant cannot reach the same action by the coverage route and
mask the difference. 9c asserts the N=1 identity on a real one-inverter tree.
SAFETY-32 already closes `list(standing.values())[0]` structurally and this card
does not claim it. Its disclosed blind spot - semantics are invisible to a shape
pin, permanently - is why the mean and max(key=abs) needed a behavioural set.
AND THE SEARCH FOR THE NEXT SURVIVOR FOUND ONE. `float(self.target)` - the
arbiter's site target instead of the sum of what stands on the wire - is -11 B
and survives all 20 files on untouched release/1.0 @ f72c48d, because every
fixture that existed was built where the two are the same number. They separate
when a device's reported range is narrower than its max_w: the `device` claim is
filed per actuator and never bounds the site target. Closed here by a
counterfactual that asks for -500 W while -250 W stands.
10 mutations of that one line, each on a fresh full-repo copy mounted :ro,
purged by tools/mutation_run.sh, the edit re-read from disk and the byte delta
printed. Ledger and honest gaps in DECISIONS/SAFETY-38-site-standing-pin.md.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU
12 KiB
SAFETY-38 — pinning site_standing, the one quantity both site guards are asked about
Status: DECIDED 2026-09-05. app/main.py's
site_standing = float(sum(standing.values())) is correct and is not
changed. What this card adds is the thing it never had: a behavioural
fixture whose per-unit shares cannot answer for the site, so that a change
of meaning in that reduction goes red instead of green.
Filed with SAFETY-38, card 6a9ac95f2b78646e5a76d2e9. It is a coverage
card: not one byte of app/ moves.
0. The line, and its two consumers
site_standing = float(sum(standing.values())) # app/main.py:1598
standing is {unit name -> the value THIS controller last put on that unit's setpoint entity} — inv.last_write, i.e. the number after
clamp_to_rating() has applied max_w and the device's own
[dev_min, dev_max]. Two safety inputs are derived from it and nothing else in
cycle() is:
| consumer | where | what it does with it |
|---|---|---|
| SAFETY-30's site sign detector | self.site_sign_wd.evaluate(site_standing, self.grid, …) (main.py:1619) |
latches and stops the whole site when the total command opposes the meter for site_sign_disagree_s |
| SAFETY-18's freeze conjunction | looks_frozen(unchanged_s, site_standing, max_w, …) inside peakguard.decide(frozen=…) (main.py:1926) |
takes the peak guard blind and files floor_w = 0.0 — the battery may not import while the billed quarter is unmeasurable |
Both are P0 paths. Neither was pinned to the reduction.
1. What survived, and what the rescope measured
The card opened with three demonstrated live mutants, all green across the whole
suite. Re-measured on 2026-09-05 against release/1.0 @ f72c48d, after
SAFETY-32 merged:
mutation of site_standing |
before SAFETY-32 | after SAFETY-32 |
|---|---|---|
list(standing.values())[0] — one unit's share |
survives | CAUGHT by SAFETY-32's package-wide structural pin |
sum(...) / max(1, len(standing)) — the mean |
survives | still survives |
max(standing.values(), key=abs) — the largest share |
survives | still survives |
SAFETY-32's pin catches a slice or index standing between a roster-derived
value and its reduction, on the line it is written, at every roster length,
with no fixture — which is exactly [0]. Its own disclosed blind spot says why
the other two are out of reach:
SEMANTICS.
min→max→ the mean are edits invisible to any shape pin, permanently. Only the behavioural sets can see them.
sum → mean and sum → max(key=abs) are semantic: identical shape,
different meaning. No shape pin will ever close them. That disclosure was
accurate, and is now confirmed by measurement rather than taken on trust.
2. Why the suite could not see it — habit 4, inside the site half of the very card that names it
test_watchdog.py's _sign_site is the only N≥2 site fixture in the
repository. It runs at site_sign_disagree_w: 320 with shares of roughly
−2500 W each. Every one of them is individually far over the gate, so on
those numbers
sum = −5000 W over mean = −2500 W over max = −2500 W over
— all three reductions agree, and a check whose two candidate answers happen
to be equal cannot observe a change that swaps them. That is MUTATION.md's
third failure mode, and this is its ninth instance.
At a roster of one it is not a fixture defect but arithmetic:
sum == mean == max == the one share, identically, so all three mutations are
genuinely equivalent on a shipped install. No N=1 fixture can ever separate
them; §3c asserts that rather than leaving it to the reader.
3. The fixture, and the numbers it is built on
Two simulated units rated 220 W and 280 W, both obeying exactly, inverted
gain, house importing 2000 W. Both stand at their own rails, so standing is
{-220, -280} in roster order and the meter reads +2500 W.
| reduction | value | vs the SHIPPED 300 W gate |
|---|---|---|
sum |
−500 W | OVER → trips |
sum / len (the mean) |
−250 W | under → silent |
max(…, key=abs) |
−280 W | under → silent |
list(…)[0] |
−220 W | under → silent |
Four distinct magnitudes — 500, 250, 280, 220 — of which exactly one clears
the gate. The asymmetry is load-bearing twice: equal shares would collapse
mean, max and first onto one number, and putting the smaller unit first is
what keeps [0] distinct from max(key=abs). The gate is left at its shipped
default so the fixture is graded against the magnitude an install runs; only the
hold is shortened (325 s → 2 s), because the hold is a wall clock and the
campaign behind 325 s is measured on the rig, not here.
⚠️ 220 W and 280 W are SIMULATED ratings chosen for that arithmetic and for
nothing else. They are not measurements. In particular they are not
inverter B, which does not physically exist; B is a GW5048D-ES, serial
55048ESU22AW0992, 4600 W from its datasheet (goodwe/NOTES.md §1.1b).
3a. The site sign detector
Shipped tree: the site latches, commands 0 W on every inverter, and the sentence
an operator reads is
SIGN_DISAGREEMENT: commanded -500 W against a meter reading +2500 W …. No
per-unit watchdog trips — every unit is faithfully executing its share, which is
the asymmetry the site detector exists for.
Under the mean and under max(key=abs): nothing trips, ever. The reviewer's
tree was still commanding −500 W of charge into a +2500 W import at t = 595 s
with following=True.
Two counterfactuals sit beside it so the trip is attributable:
- the same inverted-gain site whose shares sum to −250 W — under the gate — does not trip, so the trip above is the sum crossing 300 W and not the mere presence of a second inverter;
- a site whose device range is narrower than
max_wasks for −500 W while only −250 W stands, and does not trip — see §5.
3b. The second consumer, covered rather than scoped out
site_standing is also looks_frozen()'s commanded_w. The same
discrimination applies at a different threshold: the site rail is
RAIL_FRAC * site_max_w = 0.95 × 500 = 475 W.
| reduction | value | vs the 475 W rail |
|---|---|---|
sum |
500 W | RAILED → BLIND_FROZEN, floor_w = 0.0 |
| the mean | 250 W | under → silent |
max(…, key=abs) |
280 W | under → silent |
list(…)[0] |
220 W | under → silent |
The sign is flipped from §3a on purpose: the gain is the shipped positive one and the frozen meter reads import, so the site discharges +500 W against a +2500 W import — the two agree in sign, SAFETY-30 has nothing to say, and this half grades SAFETY-18 alone. Reusing §3a's inverted gain would have the site sign latch park everything at 0 W, and the freeze conjunction could then never be true: a check measuring the other guard's remedy.
⚠️ The open quarter is backfilled with the same stuck reading, and that is
not decoration. Without it the block is mostly uncovered, blindness()
answers BLIND_COVERAGE, and the mutant reaches the same action by a different
route — a check on the action alone could not then see the difference it names.
Seeded, the measurement is honest:
- shipped:
blind = BLIND_FROZEN,floor_w = 0.0— no charging while the billed quarter is unobservable; - both mutations: not blind at all,
floor_w = None— the site may go on importing through a quarter whose billed average cannot be measured. That is the capacity-tariff hazard SAFETY-18 exists for, silently disarmed.
The counterfactual comes from the same run: on the earlier cycles the
command had not yet reached the rail and the guard did not report SAFETY-18,
with the meter equally frozen throughout — so the verdict is produced by the
command magnitude site_standing carries and not by unchanged_s alone. A low
gain (0.05, the schema floor) is what buys those below-the-rail cycles.
3c. At N=1, nothing changes — asserted
A real one-inverter controller is driven through the same frozen-meter
scenario. Its standing has a single entry, so sum, the mean,
max(key=abs) and [0] are printed and asserted to be one number; the
site sign detector is still absent (part 7 counts the calls on the object); and
SAFETY-18 still reaches BLIND_FROZEN on that same value. The claim "the
shipped install is unaffected" now rests on the roster rather than on the
reader.
4. Mutation ledger
Every run: a fresh full-repo copy, __pycache__ and *.pyc excluded,
mounted :ro, graded by tools/mutation_run.sh (which purges, prints the tree
fingerprint, and verifies no cache appears during the run). The mutated line was
re-read from disk after the edit and the byte delta printed. Shipped
app/main.py is 232 520 bytes.
| # | mutation of site_standing |
Δ bytes | verdict |
|---|---|---|---|
| M1 | sum(...) / max(1, len(standing)) — the mean |
+24 | RED |
| M2 | max(standing.values(), key=abs) — largest share |
+9 | RED |
| M3 | list(standing.values())[0] — one share |
+4 | RED |
| M4 | min(standing.values(), key=abs) — smallest share |
+9 | RED |
| M5 | sum(abs(v) for v in ...) — magnitude, sign lost |
+16 | RED |
| M6 | -sum(standing.values()) — sign flipped |
+1 | RED |
| M7 | float(self.target) — the arbiter's site target |
−11 | was a SURVIVOR — now RED, see §5 |
| M8 | sum(standing.values()) / 2.0 |
+6 | RED |
| M9 | sum(standing.values()) * 2.0 |
+6 | RED |
| M10 | sorted(standing.values())[-1] — largest signed |
+7 | RED |
M3 is listed for completeness only. SAFETY-32 already closes it structurally and this card does not claim it; that the behavioural fixture also catches it is a free consequence of the same discriminating numbers, not a second pin.
5. The survivor this card went looking for, and closed
M7 is not one of the card's three. It was found by asking the question the recurring defect demands — what mutation would my new checks NOT see? — and it is the answer: −11 bytes, and it survived every check above and the whole 20-file suite, because in every fixture built so far the arbiter's site target and the sum of what actually stands on the wire are the same number.
They are not the same number when a setpoint device's own reported range is
narrower than its max_w. The device P_SAFETY claim is filed per actuator
(actuator_of("device", inv)), so it bounds each unit's share and never the
site target, and clamp_to_rating() clips again on the way out. On two units
rated 220/280 W whose devices report ±120/±130 W, the site asks for −500 W
and −250 W stands.
Which one the guards mean is already written down, in both of them: SAFETY-02
grades "the command standing on the wire", and looks_frozen()'s docstring
says commanded_w is "the EMITTED setpoint — what the arbiter resolved and
write_setpoint() put on the wire — not the loop's raw wish". SAFETY-20 is
the whole card about the difference. Those sentences are now made to bite: on
the site target the detector fires on a site standing at −250 W against a
+2250 W meter — under its own 300 W gate — which is a latching, site-wide
false trip on an install whose inverters simply report a narrower range than the
operator typed.
6. Honest gaps
- A mutation that changes the SIGN CONVENTION of
standingitself — i.e. inmain.py'sstood = inv.last_write if inv.last_write is not None else inv.target, not on the reduction line — is not in this ledger. It is a different line and a different card; §3a's obedience precondition would see some of it, but that is not a measured claim and is not made here. - The freeze half is graded at ONE roster length (2) and one rail. The
RAIL_FRACconstant,rail_ceiling_w()'s device-bound conjunction andrail_unreachable()aretest_freeze.py's subject and are untouched here. - The site hold is 2 s, not the shipped 325 s. The window's derivation is a
rig campaign (
DECISIONS/SAFETY-30-per-unit-sign.md§3B) and this file does not re-grade it. The gate is the shipped 300 W, which is the coordinate this card is about. - Everything about inverter B here is simulated and labelled so. No number in this document or in the fixture is a measurement of a device that does not yet exist.