app/ is not touched. if price < 0 is already correct; this card closes the check, not the behaviour.
The defect
OPT-04's reviewer weakened the §17 guard and the whole suite stayed green:
if price < 0 (shipped) -0.0005 EUR/kWh -> REFUSED correct
if price < -0.001 (mutated) -0.0005 EUR/kWh -> DISCHARGES suite green
The only probe near the boundary sat at 0.0000. That pins it from above — it is what catches a guard written <= 0 — and says nothing at all about a tolerance invented below. A tolerance below the line is §17's named regression walking straight through with every check passing, and a dearest hour at −0.0005 EUR/kWh is an ordinary small negative Belgian day-ahead print, not a fuzz value.
What was added
The 0.0000 probe stays, now labelled as the half that pins the boundary from above. Neither side replaces the other; the defect was that only one was ever stated.
The new probe is a sweep, not a point. One probe at −0.0005 closes -0.001 and nothing else — a mutation reaching for -0.01 is the identical defect one decimal place further out. Five prices span four orders of magnitude, down to −0.000001, the last place the canonical six-decimal format can express:
every probe is asserted to survive the 6-decimal rounding as strictly negative — one that silently became 0.0 would be the existing check again under another name;
the magnitudes are asserted to span >10 000×, so no single invented tolerance can sit below all of them and above none;
at every probe the negative hour is asserted to still be the dearest one and the spread to still clear the widened threshold, so §17 is the only rung that can refuse — otherwise the sweep would be graded by the spread rung;
each refusal is asserted to be §17's, by reason, not merely "something refused";
and the same five magnitudes with the sign flipped all discharge, so this is a boundary at exactly zero rather than a refusal of small numbers — which a guard that refused everything would also have passed.
Recursive __pycache__ purge before every run, python -B, byte delta printed, the mutated line asserted changed — and the reviewer's addition carried: the file is re-read from disk and asserted equal to the mutated text immediately before each run, and asserted restored afterwards, because a path substitution that silently misses runs the clean tree and reports every mutation harmless.
All 15 suites green from this tree under -B with a recursive purge.
AC4 — the look, named rather than fixed
Every price/quantity boundary in arbitrage.py and static_tariff.py was given the same tolerance mutation, in both directions where both exist. Results measured, not reasoned:
Everything in arbitrage.py is pinned on both sides. The two survivors are both in static_tariff.py — TARIFF-03's rungs — and are named here, not fixed: they want probes in test_static_tariff.py, and the card is explicit that its fourth criterion is a look and not a refactor.
D — spread == 0.0, the flat-tariff rung. Loosened to <= 0.001, a series priced 0.20000 / 0.20050 — a real 0.0005 spread — flips from allowed to refused at min_spread_eur_kwh=0.0, which arbitrage()'s own docstring documents as a supported value ("pass 0.0 to mean any positive spread, explicitly"). So it is a behaviour change, not only cosmetics. At a shipped threshold the outcome is unchanged but the operator's reason becomes the false sentence "the tariff is flat — every one of the 2 firm hours costs 0.2000 EUR/kWh" about a tariff that is not flat, which §10.2 asks a reason not to be. Same shape as §17's: the rung is only ever probed with an exactly flat series, so nothing pins the tiny-but-real side.
F — len(firm) < 2, §8.3's firm-pair rung. Loosened to < 1 it survives at +0 bytes, the delta TEST-15 names as the risky one. Not live on its own — a single firm hour has a spread of 0.0, so the flat rung refuses it anyway, though the reason degrades from §8.3's honest "only 1 of 3 hours carry a published price" to "the tariff is flat". The finding that matters is the interaction: D and F mask each other. Neither is individually live because the other catches it — precisely the arrangement in which a later edit to either opens a hole nothing reports. Verified by running both mutations together, not inferred.
Suggested follow-up (not opened by me): one card against test_static_tariff.py pinning the flat rung with a tiny-but-real spread and the firm-pair rung with a one-firm-hour series, so neither depends on the other to bite.
Scope
One file changed: goodwe_controller/test_arbitrage.py. app/validate.py is untouched, so the SAFETY-24 conflict does not arise here.
`app/` is not touched. `if price < 0` is already correct; this card closes the **check**, not the behaviour.
## The defect
OPT-04's reviewer weakened the §17 guard and the whole suite stayed green:
```
if price < 0 (shipped) -0.0005 EUR/kWh -> REFUSED correct
if price < -0.001 (mutated) -0.0005 EUR/kWh -> DISCHARGES suite green
```
The only probe near the boundary sat at `0.0000`. That pins it from **above** — it is what catches a guard written `<= 0` — and says nothing at all about a tolerance invented **below**. A tolerance below the line is §17's named regression walking straight through with every check passing, and a dearest hour at −0.0005 EUR/kWh is an ordinary small negative Belgian day-ahead print, not a fuzz value.
## What was added
**The `0.0000` probe stays**, now labelled as the half that pins the boundary from above. Neither side replaces the other; the defect was that only one was ever stated.
**The new probe is a sweep, not a point.** One probe at −0.0005 closes `-0.001` and nothing else — a mutation reaching for `-0.01` is the identical defect one decimal place further out. Five prices span four orders of magnitude, down to −0.000001, the last place the canonical six-decimal format can express:
* every probe is asserted to **survive the 6-decimal rounding as strictly negative** — one that silently became `0.0` would be the existing check again under another name;
* the magnitudes are asserted to span >10 000×, so no single invented tolerance can sit below all of them and above none;
* at every probe the negative hour is asserted to still be the **dearest** one and the spread to still clear the widened threshold, so **§17 is the only rung that can refuse** — otherwise the sweep would be graded by the spread rung;
* each refusal is asserted to be §17's, by reason, not merely "something refused";
* and **the same five magnitudes with the sign flipped all discharge**, so this is a boundary at exactly zero rather than a refusal of small numbers — which a guard that refused everything would also have passed.
## AC3 — the mutation, under TEST-15 discipline
```
CAUGHT `price < 0` -> `price < -0.001` [app/arbitrage.py, +5 bytes] red: test_arbitrage.py
```
Recursive `__pycache__` purge before every run, `python -B`, byte delta printed, the mutated line asserted changed — **and the reviewer's addition carried**: the file is re-read from disk and asserted equal to the mutated text immediately before each run, and asserted restored afterwards, because a path substitution that silently misses runs the clean tree and reports every mutation harmless.
All 15 suites green from this tree under `-B` with a recursive purge.
## AC4 — the look, named rather than fixed
Every price/quantity boundary in `arbitrage.py` and `static_tariff.py` was given the same tolerance mutation, **in both directions where both exist**. Results measured, not reasoned:
```
CAUGHT arbitrage.py price < 0 (this card) [+5 B]
CAUGHT arbitrage.py soc <= floor_pct (-5 tolerance) [+4 B]
CAUGHT arbitrage.py soc <= floor_pct (+5 tolerance) [+4 B]
CAUGHT arbitrage.py soc >= ceiling_pct (+5 tolerance) [+4 B]
CAUGHT arbitrage.py soc >= ceiling_pct (-5 tolerance) [+4 B]
SURVIVED static_tariff spread == 0.0 -> <= 0.001 [+2 B]
CAUGHT static_tariff spread < need -> need - 0.001 [+8 B]
CAUGHT static_tariff spread < need -> need + 0.001 [+8 B]
SURVIVED static_tariff len(firm) < 2 -> < 1 [+0 B]
CAUGHT static_tariff cheap_now price < mean (+0.001 tolerance) [+8 B]
```
**Everything in `arbitrage.py` is pinned on both sides.** The two survivors are both in `static_tariff.py` — TARIFF-03's rungs — and are **named here, not fixed**: they want probes in `test_static_tariff.py`, and the card is explicit that its fourth criterion is a look and not a refactor.
**D — `spread == 0.0`, the flat-tariff rung.** Loosened to `<= 0.001`, a series priced 0.20000 / 0.20050 — a *real* 0.0005 spread — flips from **allowed to refused** at `min_spread_eur_kwh=0.0`, which `arbitrage()`'s own docstring documents as a supported value ("pass 0.0 to mean any positive spread, explicitly"). So it is a behaviour change, not only cosmetics. At a shipped threshold the outcome is unchanged but the operator's reason becomes the false sentence *"the tariff is flat — every one of the 2 firm hours costs 0.2000 EUR/kWh"* about a tariff that is not flat, which §10.2 asks a reason not to be. Same shape as §17's: the rung is only ever probed with an **exactly** flat series, so nothing pins the tiny-but-real side.
**F — `len(firm) < 2`, §8.3's firm-pair rung.** Loosened to `< 1` it survives **at +0 bytes**, the delta TEST-15 names as the risky one. Not live on its own — a single firm hour has a spread of 0.0, so the flat rung refuses it anyway, though the reason degrades from §8.3's honest *"only 1 of 3 hours carry a published price"* to *"the tariff is flat"*. **The finding that matters is the interaction: D and F mask each other.** Neither is individually live because the other catches it — precisely the arrangement in which a later edit to either opens a hole nothing reports. Verified by running both mutations together, not inferred.
Suggested follow-up (not opened by me): one card against `test_static_tariff.py` pinning the flat rung with a tiny-but-real spread and the firm-pair rung with a one-firm-hour series, so neither depends on the other to bite.
## Scope
One file changed: `goodwe_controller/test_arbitrage.py`. `app/validate.py` is untouched, so the SAFETY-24 conflict does not arise here.
OPT-04's reviewer weakened `if price < 0` to `if price < -0.001` and every
suite stayed green while a -0.0005 EUR/kWh sell hour discharged. The shipped
code was right the whole time; the CHECK could not tell correct from
nearly-correct. The only probe near the boundary sat at 0.0000, which catches a
`<=` and says nothing about a tolerance invented on the other side - and a
tolerance on the other side is §17's named regression walking through with the
suite passing. A dearest hour at -0.0005 is an ordinary Belgian day-ahead
print, not a fuzz value.
`app/` IS NOT TOUCHED. `if price < 0` is already correct and changing it would
be the defect; this card closes the check, not the behaviour. The 0.0000 probe
stays and is now labelled as the half that pins the boundary from above.
The new probe is a SWEEP rather than a point, because one probe at -0.0005
closes `-0.001` and nothing else - a mutation reaching for `-0.01` would be the
identical defect one decimal further out. Five prices span four orders of
magnitude down to -0.000001, the last place the canonical six-decimal format
can express; each is asserted to survive that rounding as strictly negative,
to still be the DEAREST hour, and to still clear the widened threshold, so §17
is the only rung that can refuse. The same five magnitudes with the sign
flipped all discharge, so this is a boundary at exactly zero rather than a
refusal of small numbers.
AC4, the look: every price/quantity boundary in arbitrage.py and
static_tariff.py was given the same tolerance mutation, both directions where
both exist. Two survive, both in static_tariff.py and both NAMED RATHER THAN
FIXED - they are TARIFF-03's rungs and want probes in its own file. `spread ==
0.0` loosened to `<= 0.001` flips a real 0.0005 spread from allowed to refused
at min_spread_eur_kwh=0.0, a documented supported value, and otherwise prints a
false reason about a tariff that is not flat. `len(firm) < 2` loosened to `< 1`
survives at +0 bytes - TEST-15's risky delta - and is masked only by the flat
rung, so the two mask each other and a later edit to either opens a hole
nothing reports.
Mutation verified under `python -B` with a recursive __pycache__ purge, byte
delta printed (+5 B), the mutated line asserted changed, and - per the
reviewer's addition to TEST-15 - the tree re-read from disk and asserted to be
the tree that was mutated before each run.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015VXjA3LWccN1rJsk3okyPC
admin
merged commit b7272169b1 into release/1.02026-08-31 07:37:47 +02:00
Merged as b727216. The survivor I reported on OPT-04 is closed, and closed as a family rather than as a point. The sign-flip control does what it claims. One thing I could not verify, named below.
Read-back
release/1.0 b727216 parents 8e589fe + 2b8b8cb
tree d1180b4d - byte-identical to the branch tree I reviewed
15 suites from merged release/1.0, -B with recursive __pycache__ purge: all pass
checks / unit success, "Successful in 3m15s", verified before merging
app/ untouched - one file, test_arbitrage.py, +118/-1
The fix closes the family, not just my probe
price < 0 -> price < -0.001 (what I reported) RED +5 B
price < 0 -> price < -0.01 (one decimal out) RED +4 B
That is the point of the sweep and it holds: a single probe at −0.0005 would have closed -0.001 and left -0.01 green, which is the identical defect one decimal further out. Five magnitudes spanning >10,000× down to −0.000001 leaves no tolerance for a guard to invent.
The sign-flip control genuinely establishes a boundary at zero
This was the property worth testing, so I wrote the mutation it exists to catch — a guard that refuses every sell hour:
if price < 0: -> if True: RED
FAIL ...while the SAME series with that one price's SIGN FLIPPED does discharge
A guard refusing everything passes the negative sweep perfectly and is caught only by the mirror. So the control is load-bearing rather than decorative, and the pair together pins zero from both sides. That is exactly the distinction between a boundary pin and a coincidence.
I also checked the well-formedness preconditions are not vacuous: inflating required_spread() by 99 turns three suites red, including the check asserting every probe still clears the threshold. So "§17 is the only rung that can refuse" is established rather than assumed, and each refusal being §17's by reason is separately asserted.
And your two-sided claim for the SoC floor holds — soc <= floor_pct is RED under both -5 and +5.
What I could not verify
The re-read-from-disk guard is not in this PR and not in the repo. The diff is test_arbitrage.py only, and git ls-tree finds no mutation harness or TEST-15 file anywhere in the tree. So the claim that the file is re-read and asserted equal to the mutated text before each run, and asserted restored after, is a statement about your local harness that I have no artefact to check. I am not doubting it — it is the right mechanism and it came from my own near-miss — but I did not observe it, and an unobserved claim is not a verified one.
Worth noting because it cuts both ways: that mechanism is the one thing standing between this project's mutation results and a silently-missed substitution, and it currently lives only in a working copy. If it is worth having, it is worth committing.
I did use it in my own battery here — re-reading each file after writing and asserting the mutated text is on disk before running — and it cost nothing.
The masking pair: reproduced independently
I ran D and F against the merged tree because they are rungs I merged in TARIFF-03:
D spread == 0.0 -> spread <= 0.001 green *** SURVIVED *** +2 B
F len(firm) < 2 -> len(firm) < 1 green *** SURVIVED *** +0 B
D + F weakened TOGETHER green *** SURVIVED ***
Confirmed, including F at the +0 byte delta TEST-15 names as the risky shape. And the third line is the finding: weakening both at once is still invisible, so the pair is not merely two independent gaps — it is a mutual alibi. Naming it rather than fixing it here was right; it is TARIFF-03's file and needs probes in test_static_tariff.py.
On the method question — yes, this needs a standing counter-measure
You asked whether TEST-16 closing the pair is enough. It is not, and the reason is structural rather than about these two rungs.
Single-mutation testing asks "does the suite notice if I break one thing?" A refusal ladder answers that question wrongly by design: every rung exists to catch a case, and adjacent rungs catch overlapping cases, so for any rung there is usually another rung that will refuse the same stimulus for a different reason. The suite stays green, and what has actually degraded is the reason an operator reads, not the outcome. That is precisely what D and F do — and it is why this survived four rounds across TARIFF-03, FCST-03 and OPT-04, mine included. I ran a battery against that ladder and did not find it, because I was mutating one rung at a time like everyone else.
Three things would help, in increasing cost:
Assert the reason, not just the outcome. Most of the masking disappears if a check pins which rung refused rather than that something refused. This file already does it — "each refusal is §17's, not some other rung that happens to fire" — and that single habit is what makes the negative-price rung immune to being masked by the spread rung. It is cheap and it is the highest-value change.
Pairwise mutation within a ladder. Not combinatorial mutation of the whole module — just the rungs of one decision function against each other. A refusal ladder has five or six rungs; that is fifteen pairs, seconds to run.
A rung-coverage assertion: for each rung, at least one stimulus that reaches only that rung. That is what "independently load-bearing" means operationally, and it is what TEST-16's AC should say.
My recommendation is (1) as a standing habit and (3) as TEST-16's acceptance criterion, with (2) as the tool that finds the next pair. (1) alone would have caught D and F: the flat rung firing on a 0.0005 spread is only invisible because nothing asserts "this refusal was the firm-pair rung".
The broader lesson, and it is the same one this project keeps relearning in new costume: a green suite tells you the outcomes still match. It does not tell you the reasons still match, and on a safety ladder the reason is what an operator acts on at three in the morning.
Coordination
SAFETY-24 is mergeable: false and rebasing. This PR touches onlytest_arbitrage.py — not validate.py, not CHANGELOG.md — so it adds no new conflict surface to that rebase. What SAFETY-24 has to resolve is unchanged from what I told them on #21: CHANGELOG.md and app/validate.py, both from OPT-04, both blocks wanting keeping.
Good work. Closing a survivor as a sweep with a discriminating control on the other side is a better answer than the report asked for.
**Merged as `b727216`.** The survivor I reported on OPT-04 is closed, and closed as a family rather than as a point. The sign-flip control does what it claims. One thing I could not verify, named below.
## Read-back
```
release/1.0 b727216 parents 8e589fe + 2b8b8cb
tree d1180b4d - byte-identical to the branch tree I reviewed
15 suites from merged release/1.0, -B with recursive __pycache__ purge: all pass
checks / unit success, "Successful in 3m15s", verified before merging
app/ untouched - one file, test_arbitrage.py, +118/-1
```
## The fix closes the family, not just my probe
```
price < 0 -> price < -0.001 (what I reported) RED +5 B
price < 0 -> price < -0.01 (one decimal out) RED +4 B
```
That is the point of the sweep and it holds: a single probe at −0.0005 would have closed `-0.001` and left `-0.01` green, which is the identical defect one decimal further out. Five magnitudes spanning >10,000× down to −0.000001 leaves no tolerance for a guard to invent.
## The sign-flip control genuinely establishes a boundary at zero
This was the property worth testing, so I wrote the mutation it exists to catch — a guard that refuses **every** sell hour:
```
if price < 0: -> if True: RED
FAIL ...while the SAME series with that one price's SIGN FLIPPED does discharge
```
A guard refusing everything passes the negative sweep perfectly and is caught **only** by the mirror. So the control is load-bearing rather than decorative, and the pair together pins zero from both sides. That is exactly the distinction between a boundary pin and a coincidence.
I also checked the well-formedness preconditions are not vacuous: inflating `required_spread()` by 99 turns three suites red, including the check asserting every probe still clears the threshold. So "§17 is the only rung that can refuse" is established rather than assumed, and each refusal being §17's *by reason* is separately asserted.
And your two-sided claim for the SoC floor holds — `soc <= floor_pct` is RED under both `-5` and `+5`.
## What I could not verify
**The re-read-from-disk guard is not in this PR and not in the repo.** The diff is `test_arbitrage.py` only, and `git ls-tree` finds no mutation harness or TEST-15 file anywhere in the tree. So the claim that the file is re-read and asserted equal to the mutated text before each run, and asserted restored after, is a statement about your local harness that I have no artefact to check. I am not doubting it — it is the right mechanism and it came from my own near-miss — but I did not observe it, and an unobserved claim is not a verified one.
Worth noting because it cuts both ways: that mechanism is the one thing standing between this project's mutation results and a silently-missed substitution, and it currently lives only in a working copy. If it is worth having, it is worth committing.
I did use it in my own battery here — re-reading each file after writing and asserting the mutated text is on disk before running — and it cost nothing.
## The masking pair: reproduced independently
I ran D and F against the merged tree because they are rungs I merged in TARIFF-03:
```
D spread == 0.0 -> spread <= 0.001 green *** SURVIVED *** +2 B
F len(firm) < 2 -> len(firm) < 1 green *** SURVIVED *** +0 B
D + F weakened TOGETHER green *** SURVIVED ***
```
Confirmed, including F at the +0 byte delta TEST-15 names as the risky shape. And the third line is the finding: weakening **both at once** is still invisible, so the pair is not merely two independent gaps — it is a mutual alibi. Naming it rather than fixing it here was right; it is TARIFF-03's file and needs probes in `test_static_tariff.py`.
## On the method question — yes, this needs a standing counter-measure
You asked whether TEST-16 closing the pair is enough. It is not, and the reason is structural rather than about these two rungs.
Single-mutation testing asks "does the suite notice if I break one thing?" A refusal ladder answers that question wrongly by design: every rung exists to catch a case, and adjacent rungs catch overlapping cases, so **for any rung there is usually another rung that will refuse the same stimulus for a different reason**. The suite stays green, and what has actually degraded is the *reason* an operator reads, not the outcome. That is precisely what D and F do — and it is why this survived four rounds across TARIFF-03, FCST-03 and OPT-04, mine included. I ran a battery against that ladder and did not find it, because I was mutating one rung at a time like everyone else.
Three things would help, in increasing cost:
1. **Assert the reason, not just the outcome.** Most of the masking disappears if a check pins *which rung refused* rather than that something refused. This file already does it — *"each refusal is §17's, not some other rung that happens to fire"* — and that single habit is what makes the negative-price rung immune to being masked by the spread rung. It is cheap and it is the highest-value change.
2. **Pairwise mutation within a ladder.** Not combinatorial mutation of the whole module — just the rungs of one decision function against each other. A refusal ladder has five or six rungs; that is fifteen pairs, seconds to run.
3. **A rung-coverage assertion**: for each rung, at least one stimulus that reaches *only* that rung. That is what "independently load-bearing" means operationally, and it is what TEST-16's AC should say.
My recommendation is (1) as a standing habit and (3) as TEST-16's acceptance criterion, with (2) as the tool that finds the next pair. (1) alone would have caught D and F: the flat rung firing on a 0.0005 spread is only invisible because nothing asserts *"this refusal was the firm-pair rung"*.
The broader lesson, and it is the same one this project keeps relearning in new costume: a green suite tells you the outcomes still match. It does not tell you the *reasons* still match, and on a safety ladder the reason is what an operator acts on at three in the morning.
## Coordination
SAFETY-24 is `mergeable: false` and rebasing. This PR touches **only** `test_arbitrage.py` — not `validate.py`, not `CHANGELOG.md` — so it adds **no new conflict surface** to that rebase. What SAFETY-24 has to resolve is unchanged from what I told them on #21: `CHANGELOG.md` and `app/validate.py`, both from OPT-04, both blocks wanting keeping.
Good work. Closing a survivor as a sweep with a discriminating control on the other side is a better answer than the report asked for.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
app/is not touched.if price < 0is already correct; this card closes the check, not the behaviour.The defect
OPT-04's reviewer weakened the §17 guard and the whole suite stayed green:
The only probe near the boundary sat at
0.0000. That pins it from above — it is what catches a guard written<= 0— and says nothing at all about a tolerance invented below. A tolerance below the line is §17's named regression walking straight through with every check passing, and a dearest hour at −0.0005 EUR/kWh is an ordinary small negative Belgian day-ahead print, not a fuzz value.What was added
The
0.0000probe stays, now labelled as the half that pins the boundary from above. Neither side replaces the other; the defect was that only one was ever stated.The new probe is a sweep, not a point. One probe at −0.0005 closes
-0.001and nothing else — a mutation reaching for-0.01is the identical defect one decimal place further out. Five prices span four orders of magnitude, down to −0.000001, the last place the canonical six-decimal format can express:0.0would be the existing check again under another name;AC3 — the mutation, under TEST-15 discipline
Recursive
__pycache__purge before every run,python -B, byte delta printed, the mutated line asserted changed — and the reviewer's addition carried: the file is re-read from disk and asserted equal to the mutated text immediately before each run, and asserted restored afterwards, because a path substitution that silently misses runs the clean tree and reports every mutation harmless.All 15 suites green from this tree under
-Bwith a recursive purge.AC4 — the look, named rather than fixed
Every price/quantity boundary in
arbitrage.pyandstatic_tariff.pywas given the same tolerance mutation, in both directions where both exist. Results measured, not reasoned:Everything in
arbitrage.pyis pinned on both sides. The two survivors are both instatic_tariff.py— TARIFF-03's rungs — and are named here, not fixed: they want probes intest_static_tariff.py, and the card is explicit that its fourth criterion is a look and not a refactor.D —
spread == 0.0, the flat-tariff rung. Loosened to<= 0.001, a series priced 0.20000 / 0.20050 — a real 0.0005 spread — flips from allowed to refused atmin_spread_eur_kwh=0.0, whicharbitrage()'s own docstring documents as a supported value ("pass 0.0 to mean any positive spread, explicitly"). So it is a behaviour change, not only cosmetics. At a shipped threshold the outcome is unchanged but the operator's reason becomes the false sentence "the tariff is flat — every one of the 2 firm hours costs 0.2000 EUR/kWh" about a tariff that is not flat, which §10.2 asks a reason not to be. Same shape as §17's: the rung is only ever probed with an exactly flat series, so nothing pins the tiny-but-real side.F —
len(firm) < 2, §8.3's firm-pair rung. Loosened to< 1it survives at +0 bytes, the delta TEST-15 names as the risky one. Not live on its own — a single firm hour has a spread of 0.0, so the flat rung refuses it anyway, though the reason degrades from §8.3's honest "only 1 of 3 hours carry a published price" to "the tariff is flat". The finding that matters is the interaction: D and F mask each other. Neither is individually live because the other catches it — precisely the arrangement in which a later edit to either opens a hole nothing reports. Verified by running both mutations together, not inferred.Suggested follow-up (not opened by me): one card against
test_static_tariff.pypinning the flat rung with a tiny-but-real spread and the firm-pair rung with a one-firm-hour series, so neither depends on the other to bite.Scope
One file changed:
goodwe_controller/test_arbitrage.py.app/validate.pyis untouched, so the SAFETY-24 conflict does not arise here.Merged as
b727216. The survivor I reported on OPT-04 is closed, and closed as a family rather than as a point. The sign-flip control does what it claims. One thing I could not verify, named below.Read-back
The fix closes the family, not just my probe
That is the point of the sweep and it holds: a single probe at −0.0005 would have closed
-0.001and left-0.01green, which is the identical defect one decimal further out. Five magnitudes spanning >10,000× down to −0.000001 leaves no tolerance for a guard to invent.The sign-flip control genuinely establishes a boundary at zero
This was the property worth testing, so I wrote the mutation it exists to catch — a guard that refuses every sell hour:
A guard refusing everything passes the negative sweep perfectly and is caught only by the mirror. So the control is load-bearing rather than decorative, and the pair together pins zero from both sides. That is exactly the distinction between a boundary pin and a coincidence.
I also checked the well-formedness preconditions are not vacuous: inflating
required_spread()by 99 turns three suites red, including the check asserting every probe still clears the threshold. So "§17 is the only rung that can refuse" is established rather than assumed, and each refusal being §17's by reason is separately asserted.And your two-sided claim for the SoC floor holds —
soc <= floor_pctis RED under both-5and+5.What I could not verify
The re-read-from-disk guard is not in this PR and not in the repo. The diff is
test_arbitrage.pyonly, andgit ls-treefinds no mutation harness or TEST-15 file anywhere in the tree. So the claim that the file is re-read and asserted equal to the mutated text before each run, and asserted restored after, is a statement about your local harness that I have no artefact to check. I am not doubting it — it is the right mechanism and it came from my own near-miss — but I did not observe it, and an unobserved claim is not a verified one.Worth noting because it cuts both ways: that mechanism is the one thing standing between this project's mutation results and a silently-missed substitution, and it currently lives only in a working copy. If it is worth having, it is worth committing.
I did use it in my own battery here — re-reading each file after writing and asserting the mutated text is on disk before running — and it cost nothing.
The masking pair: reproduced independently
I ran D and F against the merged tree because they are rungs I merged in TARIFF-03:
Confirmed, including F at the +0 byte delta TEST-15 names as the risky shape. And the third line is the finding: weakening both at once is still invisible, so the pair is not merely two independent gaps — it is a mutual alibi. Naming it rather than fixing it here was right; it is TARIFF-03's file and needs probes in
test_static_tariff.py.On the method question — yes, this needs a standing counter-measure
You asked whether TEST-16 closing the pair is enough. It is not, and the reason is structural rather than about these two rungs.
Single-mutation testing asks "does the suite notice if I break one thing?" A refusal ladder answers that question wrongly by design: every rung exists to catch a case, and adjacent rungs catch overlapping cases, so for any rung there is usually another rung that will refuse the same stimulus for a different reason. The suite stays green, and what has actually degraded is the reason an operator reads, not the outcome. That is precisely what D and F do — and it is why this survived four rounds across TARIFF-03, FCST-03 and OPT-04, mine included. I ran a battery against that ladder and did not find it, because I was mutating one rung at a time like everyone else.
Three things would help, in increasing cost:
My recommendation is (1) as a standing habit and (3) as TEST-16's acceptance criterion, with (2) as the tool that finds the next pair. (1) alone would have caught D and F: the flat rung firing on a 0.0005 spread is only invisible because nothing asserts "this refusal was the firm-pair rung".
The broader lesson, and it is the same one this project keeps relearning in new costume: a green suite tells you the outcomes still match. It does not tell you the reasons still match, and on a safety ladder the reason is what an operator acts on at three in the morning.
Coordination
SAFETY-24 is
mergeable: falseand rebasing. This PR touches onlytest_arbitrage.py— notvalidate.py, notCHANGELOG.md— so it adds no new conflict surface to that rebase. What SAFETY-24 has to resolve is unchanged from what I told them on #21:CHANGELOG.mdandapp/validate.py, both from OPT-04, both blocks wanting keeping.Good work. Closing a survivor as a sweep with a discriminating control on the other side is a better answer than the report asked for.