SAFETY-38: pin site_standing with shares that cannot answer for the site
`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
This commit is contained in:
@@ -2,6 +2,69 @@
|
||||
|
||||
## Unreleased
|
||||
|
||||
**`site_standing` — the one quantity BOTH site guards are asked about — is now
|
||||
pinned by a fixture whose per-unit shares cannot answer for it (SAFETY-38).**
|
||||
`app/main.py`'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**, and three mutations of it disarmed both guards with the whole suite
|
||||
green. **This card changes coverage only: not one byte of `app/` moves — the
|
||||
shipped `sum` is correct and must not be "fixed".**
|
||||
|
||||
* **⭐ THE FIXTURE THE SUITE COULD NOT BUILD: per-unit shares EACH BELOW the
|
||||
site gate whose SUM clears it.** 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
|
||||
300 W gate. That is the case the site detector exists for, and it is the only
|
||||
arrangement that can separate `sum` from the mean and from the largest share.
|
||||
`test_watchdog.py` part 9a.
|
||||
* **WHY THE EXISTING FIXTURE COULD NOT.** `_sign_site`, the suite's only N≥2
|
||||
site fixture, runs shares of about −2500 W **each** — every one individually
|
||||
over the gate — so sum, mean and largest-share 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 very card that names it.
|
||||
* **FOUR CANDIDATE REDUCTIONS, FOUR DISTINCT ANSWERS, ONE OVER THE GATE** —
|
||||
`sum` −500, mean −250, `max(key=abs)` −280, `[0]` −220 — and that is asserted
|
||||
as a precondition, not described. The asymmetric ratings are load-bearing:
|
||||
equal shares collapse three of the four onto one number, and the **smaller**
|
||||
unit is first so `[0]` stays distinct from `max(key=abs)`. The **gate is the
|
||||
shipped 300 W**; only the hold is shortened, because the hold is a wall clock.
|
||||
* **⭐ THE SECOND CONSUMER IS COVERED, NOT SCOPED OUT.** The same value is
|
||||
`looks_frozen()`'s `commanded_w` at `main.py:1926`. At the same roster the
|
||||
site rail is 0.95 × 500 = **475 W**, so `sum` = 500 W rails it while 250, 280
|
||||
and 220 do not. Shipped: the peak guard goes blind on SAFETY-18's conjunction
|
||||
and files `floor_w = 0.0`. Under both surviving mutations it is **not blind at
|
||||
all and files no floor**, so the battery may keep importing through a quarter
|
||||
whose billed average is unobservable. `test_watchdog.py` part 9b. The open
|
||||
quarter is deliberately backfilled with the stuck reading so the mutant cannot
|
||||
reach the same action by the *coverage* route and mask the difference.
|
||||
* **AT N=1 NOTHING CHANGES, AND IT IS ASSERTED.** On a real one-inverter
|
||||
controller `standing` has one entry, so `sum`, the mean, `max(key=abs)` and
|
||||
`[0]` are **the same number** — all three mutations are genuinely *equivalent*
|
||||
on a shipped install and no N=1 fixture could ever separate them. Printed and
|
||||
checked, beside the site detector still being absent there. Part 9c.
|
||||
* **ONE OF THE THREE WAS ALREADY CLOSED, AND THIS CARD DOES NOT RE-COVER IT.**
|
||||
[[SAFETY-32]]'s package-wide structural pin catches
|
||||
`list(standing.values())[0]` on the line it is written, at every roster
|
||||
length, with no fixture. Its own disclosed blind spot — *"SEMANTICS. `min` →
|
||||
`max` → the mean are edits invisible to any shape pin, permanently"* — is why
|
||||
the other two needed this card, and it is now confirmed by measurement rather
|
||||
than taken on trust.
|
||||
* **⭐ A SURVIVOR THIS CARD WENT LOOKING FOR AND CLOSED.**
|
||||
`site_standing = float(self.target)` — the arbiter's site *target* instead of
|
||||
the sum of what actually **stands on the wire** — is **−11 bytes** and
|
||||
survived the full suite, because in every fixture that existed the two are the
|
||||
same number. They are not when a device's reported range is narrower than its
|
||||
`max_w`: the `device` claim is filed **per actuator**, so it bounds each
|
||||
unit's share and never the site target. A site asking for −500 W while −250 W
|
||||
stands would then take a **latching site-wide false trip** under its own
|
||||
300 W gate. Closed by part 9a's device-clamped counterfactual.
|
||||
* **10 mutations of that one line, each on a fresh full-repo copy mounted `:ro`,
|
||||
bytecode purged by `tools/mutation_run.sh`, the edit re-read from disk and the
|
||||
byte delta printed. All 10 red.** Ledger and the honest gaps in
|
||||
`DECISIONS/SAFETY-38-site-standing-pin.md`.
|
||||
|
||||
**The add-on configuration screen now speaks English and Dutch instead of raw
|
||||
option keys (UX-01).** There was no `translations/` directory at all, so Home
|
||||
Assistant fell back to rendering the option KEYS — an operator opening
|
||||
|
||||
Reference in New Issue
Block a user