SAFETY-42: close two mutation gaps, and correct what makes N=1 inert
checks / unit (pull_request) Successful in 6m3s
checks / unit (pull_request) Successful in 6m3s
The battery is 12 mutations, one freshly copied tree each, purged, `:ro`,
`python3 -B`, byte deltas measured. First round: 9 red, 3 survivors. All three
are now accounted for and the ledger is in DECISIONS §8.
M5 - `share <= 0` relaxed to `share == 0`, +0 B - WAS A REAL GAP AND THE TWO
OBVIOUS CHECKS DID NOT CLOSE IT. `short_by(-2000, 0) is None` and
`short_by(-2000, -100) is None` both stayed GREEN under the mutant: for a charge
share that is UNDER-delivered `share - measured` is negative and the `gap > 0`
test answers None anyway, so neither input can separate the two spellings. The
one that can is a unit moving MORE than its charge share - -2000 W told,
-3000 W measured, `share - measured` = +1000 W - which the mutant reports as a
shortfall. It is not one; it is something else driving that pack, which is
SAFETY-30's and SAFETY-31's question. That input is now the check, and M5 is RED.
MUTATION.md's third failure mode in miniature: the assertion ran, the sentence
was true, and the data could not reach the failure it named.
M11 - `inv.is_live(now)[0]` replaced by a literal `True`, -15 B - was a WIRING
gap the pure checks structurally could not see. `account(live=False, ...)` was
graded as a FUNCTION; nothing graded that `cycle()` asks the question, because no
fixture had a unit whose writes were being rejected. Such a unit still has its
last ACCEPTED command standing on the wire (`write_setpoint` leaves `last_write`
alone on a rejected write) and still measures nothing, so it looks exactly like a
wedged one to every number this check reads - and reporting it would be the
second, louder alarm about a state the allocator has already handled and logged.
`test_delivery.py` part 6 now builds that unit, with `rejected_since` back-dated
past LINK_STALE_S because the fixture runs in two seconds and the timer is
test_inverter.py's to grade. M11 is RED.
M3 - `len(asked) < 2` relaxed to `< 1`, +0 B - IS STILL A SURVIVOR, and it is an
EQUIVALENT MUTANT. With one asked unit `unaccounted` is a subset of a one-element
set, so it is either empty (`not unaccounted` returns {}) or equal to `asked`
(`len(unaccounted) == len(asked)` returns {}); with zero asked units both
spellings return {} as well. No input separates them, so no check can.
⚠️ AND FINDING IT CORRECTED THE MODULE'S OWN PROSE, WHICH IS THE MORE USEFUL
HALF. `delinquent()`'s docstring claimed `len(asked) < 2` was what makes N=1
inert. It is not - the PROPER-SUBSET rule is, and that line is a fast path and a
signpost. A false attribution of the mechanism in the file's own prose is exactly
what this repository keeps finding, and a mutation that survives is how it
surfaced. The line stays, with the equivalence written down beside it the way
circulation.circulating_w() records its own max(0.0, ...).
Also recorded, rather than left implicit: `account()`'s `parked` branch is graded
as a function but its WIRING is not mutated on its own; part 6's reversed-unit
fixture exercises it end to end, which is weaker, and the ledger says so.
Baseline 23/23 green before and after. app/main.py is byte-identical to the
previous commit - both closures are a test and a comment.
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
ccc4bf100c
commit
eae9e23e95
@@ -425,3 +425,81 @@ it is working.
|
||||
|
||||
## 8. Mutation ledger
|
||||
|
||||
Every run on a **freshly copied tree of its own, `__pycache__` purged and the
|
||||
count printed, mounted `:ro`, `python3 -B`** — MUTATION.md's rule, followed
|
||||
because the first SAFETY-30 battery reused one directory, a container mounted it
|
||||
mid-copy, read a missing `config.yaml` as a failing check, and the whole run was
|
||||
discarded. **The baseline is green** (23/23) or nothing below is a result. Byte
|
||||
deltas measured, not estimated; the `+0 B` ones are marked, since a
|
||||
size-preserving edit is what TEST-15 turned into a false survivor once already.
|
||||
|
||||
| # | file | mutation | Δ | result |
|
||||
|---|---|---|---|---|
|
||||
| M1 | `delivery.py` | the SoC account removed — a cold pack at its floor alarms | −31 B | **RED** — `test_delivery.py`. This is the card |
|
||||
| M2 | `delivery.py` | the proper-subset rule removed — a whole-site cause names every unit | −34 B | **RED** |
|
||||
| **M3** | `delivery.py` | **the `len(asked) < 2` fast path relaxed to `< 1`** | **+0 B** | **SURVIVOR — and an EQUIVALENT MUTANT.** See below |
|
||||
| M4 | `delivery.py` | the strict bound relaxed to `>=` — a gap the control law calls tracking is named | +1 B | **RED** |
|
||||
| **M5** | `delivery.py` | `share <= 0` relaxed to `share == 0` — charge shares graded | **+0 B** | **SURVIVOR on the first battery — GAP CLOSED**, then **RED**. See below |
|
||||
| M6 | `delivery.py` | `delivery_hold_s` 185 → a round 300 | **+0 B** | **RED** — the CFG-03 pin |
|
||||
| M7 | `main.py` | the watts threshold no longer `Tuning.saturation_w` (×4) | +6 B | **RED** |
|
||||
| M8 | `main.py` | the latch never persisted — nothing survives a restart | −35 B | **RED** |
|
||||
| M9 | `main.py` | `_delivery_announced` never set — one notification per cycle | −34 B | **RED** |
|
||||
| M10 | `main.py` | `peakguard.pack_floor_pct()`'s conjunction with `peak_soc_floor` used instead of the pack's own cut-out | +151 B | **RED** — the between-the-floors fixture |
|
||||
| **M11** | `main.py` | **`inv.is_live(now)[0]` replaced by a literal `True`** | −15 B | **SURVIVOR at review — GAP CLOSED**, then **RED**. See below |
|
||||
| M12 | `inverter.py` | the latch not restored from the store on start-up | −46 B | **RED** |
|
||||
|
||||
**11 of 12 red. One survivor, argued rather than left.**
|
||||
|
||||
### M5 was a real gap, and the two obvious checks did not close it
|
||||
|
||||
`test_delivery.py` already asserted `short_by(-2000, 0) is None` and
|
||||
`short_by(-2000, -100) is None` — and **both stayed green under the mutant**.
|
||||
For a charge share that is *under*-delivered, `share − measured` is negative and
|
||||
the `gap > 0` test answers `None` anyway, so those two inputs cannot separate
|
||||
`share <= 0` from `share == 0`. The one input that can is a unit moving **more**
|
||||
than its charge share — `-2000 W` told, `-3000 W` measured, `share − measured` =
|
||||
`+1000 W` — which the mutant reports as a shortfall. It is not one; it is
|
||||
something else driving that pack, which is SAFETY-30's and SAFETY-31's question.
|
||||
The check now uses that input and the mutant is red.
|
||||
|
||||
This is MUTATION.md's third failure mode in miniature: **the assertion ran, the
|
||||
sentence was true, and the data could not reach the failure it named.**
|
||||
|
||||
### M11 was a wiring gap the pure checks structurally could not see
|
||||
|
||||
`account(live=False, …)` was already graded — as a **function**. What was not
|
||||
graded was that `cycle()` actually *asks* `inv.is_live(now)`, and replacing that
|
||||
call with a literal `True` left every check green, because **no fixture had a
|
||||
unit whose writes were being rejected**. A dropped unit still has its last
|
||||
*accepted* command standing on the wire (`write_setpoint` leaves `last_write`
|
||||
alone on a rejected write) and still measures nothing, so it looks exactly like a
|
||||
wedged one to every number this check reads — and reporting it would be the
|
||||
second, louder alarm about a state already being handled that this module takes
|
||||
its 30 s floor from. `test_delivery.py` part 6 now builds that unit (with
|
||||
`rejected_since` back-dated past `LINK_STALE_S`, because the fixture runs in two
|
||||
seconds and the timer is `test_inverter.py`'s to grade) and the mutant is red.
|
||||
|
||||
### M3 is recorded as a survivor because it is one — and it is EQUIVALENT
|
||||
|
||||
`len(asked) < 2` → `len(asked) < 1`. With **one** asked unit, `unaccounted` is a
|
||||
subset of a one-element set, so it is either empty — `not unaccounted` returns
|
||||
`{}` — or equal to `asked` — `len(unaccounted) == len(asked)` returns `{}`.
|
||||
There is no third case, and with **zero** asked units both spellings return `{}`
|
||||
as well. **No input separates them**, so no check can, and one written to try
|
||||
would be a check about nothing.
|
||||
|
||||
⚠️ **And finding it corrected the module's own docstring, which is the more
|
||||
useful half.** That paragraph claimed `len(asked) < 2` was *what makes N=1
|
||||
inert*. It is not: the **proper-subset rule** is, and the line is a fast path and
|
||||
a signpost. A false attribution of the mechanism in the file's own prose is
|
||||
exactly what this repository keeps finding, and a mutation that survives is how
|
||||
it surfaced. The line stays — it tells a reader at a glance that a one-inverter
|
||||
install never reaches any of this — with the equivalence written down beside it,
|
||||
the way `circulation.circulating_w()` records its own `max(0.0, …)`.
|
||||
|
||||
### Not separately mutated, and said rather than left implicit
|
||||
|
||||
`account()`'s `parked` branch is graded as a function (part 1) but its **wiring**
|
||||
— `parked=inv.sign_wd.tripped` — is not mutated on its own. Part 6's reversed-unit
|
||||
fixture exercises the path end to end and asserts the account it produces, which
|
||||
is weaker than a mutation and is stated as such.
|
||||
|
||||
Reference in New Issue
Block a user