SAFETY-42 round 2: pin the two thresholds at a config that is not the shipped one
checks / unit (pull_request) Successful in 6m7s
checks / unit (pull_request) Successful in 6m7s
THE BLOCKER. At the `delinquent()` call site, `self.tuning.saturation_w` -> `250.0` (-19 B) was a survivor of all 23 suites. It is not an equivalent mutant: two copies of inverter A, B wedged, `saturation_w: 1800` (inside its own schema int(100,2000)) and a load of 1600 W put B's shortfall strictly between the literal and the configured value - head names nobody, the mutant names B. At the shipped 500 both name it, which is exactly why every fixture in part 4 was blind: their gaps are all above both thresholds. It matters beyond a mutation score. `delivery.py`'s MIN_W_IS_SATURATION_W note and the `delivery_hold_s` block in config.yaml both promise an operator, in shipped prose, that there is no fourth copy of a threshold to keep in step and no way for the two to drift - raise `saturation_w` and this check moves with you. A literal at the call site removes exactly that promise, silently, and the promise is why the design was chosen over a `delivery_w` key. ASKING THE SAME QUESTION OF EVERY OTHER NUMBER THIS CHECK READS FOUND A SECOND SURVIVOR OF ALL 23 SUITES: `bms_floor=inv.bms_soc_floor` -> `10.0` (-13 B), invisible because config.yaml's own example states 10 % and every fixture in this file used it. Two arms bracket it: a pack at 15 % against its own 25 % cut-out is cold and must stay unnamed (the literal names an empty pack), and a pack at 8 % against its own 5 % cut-out is wedged and must be named (the literal explains the fault away). One arm on each side leaves no constant at all. `delivery_hold_s` and `is_live()`'s window were ALREADY pinned - the fixtures had to move them to run - and `LINK_STALE_S` has no option behind it, so "pinned across a changed config" is not available for it and is not claimed. DECISIONS section 9 enumerates all of it with the deltas and which check kills which. Verified in the CI image, tree mounted :ro, `python3 -B`, `__pycache__` purged and counted before every run (TEST-15): R5 saturation_w -> 250.0 -19 B killed by part 4b(a) R6 bms_soc_floor -> 10.0 -13 B killed by part 4b(b), both arms R7 arm(self.delivery_cfg) -> DeliveryConfig() -1 B killed (already) R8 is_live(now) -> is_live(now, 3600.0) +8 B killed (already) R9 pack cut-out -> conjoined site floor +105 B killed twice head: 23/23 test files PASS, no bytecode written during the run. No shipped behaviour changes: the only edits to app/ are comments. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU
This commit is contained in:
@@ -503,3 +503,90 @@ the way `circulation.circulating_w()` records its own `max(0.0, …)`.
|
||||
— `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.
|
||||
|
||||
## 9. Round 2 — the thresholds this check reads, and which of them are pinned at a **changed** config
|
||||
|
||||
Everything in §8 mutated the *logic*. This section asks a narrower question that
|
||||
§8 never asked, and it found two live survivors of the whole 23-file suite:
|
||||
|
||||
> for each number `main.py` hands this reduction, is the check still red when
|
||||
> that number is replaced by a **literal equal to the value the fixtures use**?
|
||||
|
||||
That is not pedantry about style. Two of these numbers are **options**, and
|
||||
`delivery.MIN_W_IS_SATURATION_W` and the `delivery_hold_s` block in `config.yaml`
|
||||
both make a promise about them *in shipped, operator-readable prose*: there is no
|
||||
fourth copy of a default to keep in step, and no way for the two to drift,
|
||||
because an operator who raises `saturation_w` has told the loop that a bigger gap
|
||||
is ordinary tracking. A literal at the call site keeps every fixture green and
|
||||
removes exactly that promise — and the promise is why the design was chosen over
|
||||
a fourth option key.
|
||||
|
||||
### The enumeration
|
||||
|
||||
| # | what the call site reads | mutated to | Δ | before round 2 | now |
|
||||
|---|---|---|---|---|---|
|
||||
| R5 | `self.tuning.saturation_w` | `250.0` | −19 B | **SURVIVED all 23 suites** | red — part 4b(a) |
|
||||
| R6 | `bms_floor=inv.bms_soc_floor` | `10.0` | −13 B | **SURVIVED all 23 suites** | red — part 4b(b) |
|
||||
| R7 | `arm(self.delivery_cfg)` | `arm(DeliveryConfig())` | −1 B | red | red — part 4 already ran at a hold of 1 s, not the shipped 185 s |
|
||||
| R8 | `inv.is_live(now)[0]` | `inv.is_live(now, 3600.0)[0]` | +8 B | red | red — part 6's link-dead fixture |
|
||||
| R9 | `bms_floor=inv.bms_soc_floor` | the **conjoined** floor, `max(pack cut-out, site peak_soc_floor)` | +105 B | red | red — and now red **twice**: the pre-existing between-floors check at shipped floors, and part 4b(b)'s shallow arm at a cut-out that is not shipped |
|
||||
|
||||
**R7 and R8 were already pinned across a changed setting, and R5 and R6 were
|
||||
not.** The difference is entirely in the fixtures, not in the code:
|
||||
`delivery_hold_s` had to be moved off 185 s for any of this file to run inside a
|
||||
two-second test, and part 6 had to back-date `rejected_since` past
|
||||
`LINK_STALE_S` to build a dropped unit at all. Nothing ever forced
|
||||
`saturation_w` off 500 or a `bms_soc_floor` off 10 — so those two sat at the
|
||||
shipped value in every fixture, and a literal equal to the shipped value is
|
||||
invisible to a fixture that only ever uses the shipped value. **That is this
|
||||
repository's recurring defect stated in one line: a fixture whose values cannot
|
||||
reach the failure it names.**
|
||||
|
||||
`LINK_STALE_S` (R8) is the one number here with **no option behind it** — it is a
|
||||
module constant in `inverter.py` and no test reassigns it — so "pinned across a
|
||||
changed config" is not available for it, and is not claimed. What part 6 pins is
|
||||
that *widening* the window changes the answer. Spelling `is_live(now, 30.0)` at
|
||||
the call site would be an equivalent mutant today and would only drift if the
|
||||
constant moved, which is a source change and not a config change.
|
||||
|
||||
### Why R5 and R6 are not equivalent mutants — the inputs that separate them
|
||||
|
||||
Both are separating inputs found by asking where the shipped answer and the
|
||||
literal's answer can differ, and both are configs the shipped schema accepts.
|
||||
|
||||
**R5, watts.** Two copies of inverter A, B wedged, `saturation_w: 1800` (inside
|
||||
its own schema `int(100,2000)`), load 1600 W. B's standing share and its
|
||||
shortfall then land strictly **between** `250.0` and the configured 1800 W:
|
||||
|
||||
* head → `named = {}` — the operator raised the threshold and this check moved
|
||||
with them, which is the whole promise;
|
||||
* R5 → `named = {inverter-b}` — an alarm at a gap the control law has been told
|
||||
is ordinary tracking.
|
||||
|
||||
At the shipped `saturation_w: 500` **both** name it, which is why every fixture
|
||||
in part 4 is blind: their gaps are all above both thresholds.
|
||||
|
||||
**R6, percent.** The literal is invisible because `bms_soc_floor: 10` is what
|
||||
`config.yaml`'s own `inverters:` example states and what every fixture in
|
||||
`test_delivery.py` used. Two arms, on either side of it, both inside the schema
|
||||
`int(0,50)` and both `validate.py` rule-8-valid:
|
||||
|
||||
* a pack at **15 %** against its own **25 %** cut-out is genuinely cold. Head
|
||||
stays silent; R6 grades it against 10 %, finds it "permitted", and **names an
|
||||
empty pack** — the crying-wolf failure this card exists to remove.
|
||||
* a pack at **8 %** against its own **5 %** cut-out is genuinely wedged. Head
|
||||
names it; R6 grades it against 10 %, calls it a cold pack, and **loses the
|
||||
fault**.
|
||||
|
||||
One arm on each side is what leaves no constant at all: a single deeper arm
|
||||
kills `10.0` but not `50.0`, a single shallower arm kills `10.0` but not `0.0`.
|
||||
The two arms are byte-identical in the power domain on every cycle — the
|
||||
configured cut-out is the only thing that differs, which is §3.1's claim made
|
||||
again at cut-outs that are not the shipped one.
|
||||
|
||||
### What is still not pinned this way, said rather than left implicit
|
||||
|
||||
`parked=inv.sign_wd.tripped` and `standing[inv.name]`/`inv.batt` are wiring, not
|
||||
thresholds: there is no "value the fixture happens to use" for a boolean or for a
|
||||
measurement, so this question does not apply to them, and §8's last note already
|
||||
records that the `parked` wiring is graded end-to-end rather than by mutation.
|
||||
|
||||
Reference in New Issue
Block a user