run_tariff_refresh is the only caller of resolve_tariff(), which is the only caller of price_history.carry(). So on a shipped install with a price provider configured nothing was ever fetched: no plan resolved, /data/price_history.json never created, the Price history status row reading empty for the life of the install, and TARIFF-09's short-horizon warning unable to fire because there was no fetch to warn about.
Measured over a 40 h soak of two instances side by side on one feed:
shipped default + provider
same, arbitrage armed
feed requests
0
fetching
/data/price_history.json
present: false, 802/802 samples
written
resolved plan
none
132 points
⚠️git log -S puts the refresh task, its arbitrage.enabled() guard and the DOCS.md sentence saying a configured provider still fetches in the same commit — OPT-04, bdd7f18. config.yaml has carried the claim since TARIFF-02, 852fc7c. The prose and the code disagreed from the moment both existed, and nothing compared them. Both translations and config.yaml additionally carried a claim that had rotted the other way — "nothing consumes the price series yet", untrue since OPT-04 shipped the consumer.
What changed
Option A, as decided. main.tariff_refresh_wanted():
⚠️The or makes it a STRICT WIDENING — every configuration that started the task before still starts it — and the second term is not there for symmetry. price_provider: off + a static_profile + arbitrage_enabled: true is a configuration validate.py explicitly accepts, and it trades. Measured on this branch:
opts : price_provider off, arbitrage_enabled true,
cycle_cost_threshold_eur 0.05, static_profile 0.20 night / 0.284 day
plan : static_profile, 48 h, 48 points
WITH the plan -> ARBITRAGE_CHARGE -5000.0 W
WITHOUT the plan -> None "no tariff plan has been resolved yet"
Gating on the provider alone would leave that site's tariff_plan at None forever and silently disarm a strategy the UI says is running — the exact state validate.py's OPT-04 rules exist to refuse, arrived at from inside the code.
⚠️Nobody acquires a periodic HTTPS call. With price_provider off, resolve_tariff() answers out of the static profile without opening a socket (driven and counted: 0 requests), and the true shipped default — no provider, no trading — starts no task at all. What decides whether a request leaves the machine is price_provider, and only that.
⚠️AC2's letter vs its harm, stated rather than glossed. The criterion asks for no fetch task at all with price_provider: off, whatever arbitrage_enabled says. One configuration (provider off, trading armed) keeps the task it already had, because taking it away costs the measurement above its trading. The harm the criterion names — a periodic HTTPS call — does not occur in any configuration. If the owner wants the literal reading, it is one token (drop the or), and DECISIONS/TARIFF-10-fetching-is-not-trading.md §3 records the trade.
Nothing trades that did not trade before
cycle()'s if arbitrage.enabled(self.o): is untouched. Graded directly, by driving Controller.cycle() on a resolved provider plan with the peak guard asserted clear, moving exactly one option:
arbitrage_enabled
cheapest hour is now
commanded
false
yes
+180 W (loop -> 180 W)
false
no
+180 W (loop -> 180 W)
true
yes
−5000 W (arbitrage -> -5000 W)
true
no
+5000 W (arbitrage -> 5000 W)
The armed rows are the half that makes this a check: without them, "no trade" would be satisfied by a fixture in which nothing could have traded either way.
TariffPlan.cheap_now() — decided, not inherited
Not wired. A provider-backed plan now exists on every configured site, so Cheap window could read it — and Cheap window feeds the maintenance cycle's grid top-up, the one place a price moves energy without trading being armed. On a provider-only site (static_profile empty) static_status() returns False on every cycle, so adopting it would take a paid grid top-up from never to sometimes. self.cheap stays on static_status(); DECISIONS §4 records the three further reasons and the five things adoption would take.
Surfaces
DOCS.md, config.yaml and both translations now say what the add-on does, and section 13 pins DOCS.md against the driven behaviour as a pair so they cannot drift apart again. The Arbitrage row's off (arbitrage_enabled) string was re-read and left: self.arb is None iff the option is off, and on a fetching site it is now the row that says the prices are moving nothing.
The check, and why it is not a check on an if
The if was exactly what somebody meant to write, and its comment argued for it honestly — it was true when OPT-04 wrote it and stopped being true when FCST-04 gave the plan a store and a status row. A pin on the expression would have gone red on the fix and green on the defect.
test_price.pysection 13 grades reachability from a configuration:
the guards on every asyncio.create_task in amain() are evaluated against real option dicts — whatever they are spelled as;
the call graph under app/ is walked to see whether the coroutines those guards start arrive at resolve_tariff() — wherever it has moved to;
and then the fetch is driven, with a counting transport and a real store path, because a graph is an argument and a counted request is an observation.
⚠️Step 3 grades the gate only because it starts the task the way amain() would. With the defect restored and that one line defeated, the four driven rows come back green — recorded as M9. What the instrument cannot see is printed on every run, largest hole first.
Evidence
29/29 suites green. 13 mutations in 14 runs, each on a fresh cp -a of the clean worktree rather than reverted (MUTATION.md habit 18 — no revert to verify, no shared scratch path), __pycache__ purged, tree mounted :ro, run under python3 -B.
id
mutation
Δ bytes
result
M1
the shipped defect: gate back to arbitrage.enabled(opts)
−4 B
RED, 7 named checks
M2
drop the or arbitrage.enabled(opts) term
−27 B
RED, 2
M3
tariff_refresh_wanted → always True
+8 B
RED, the AC2 row
M4
provider_configured: "off" counts as configured
−6 B
RED, the AC2 row
M5
cycle()'s trading gate → if True or …
+8 B
RED, both AC3 rows
M6
instrument: _reaches() always True
+0 B
RED, 4
M7
instrument: _task_sites() returns []
+0 B
RED, 10
M8
fixture: 5 min of telegrams instead of a whole quarter
+0 B
RED, the peak-guard precondition + both armed rows
M9
M1 plus the harness's guard-driven start defeated
−4 B
the four driven rows go GREEN — see above
M10
provider_id() drops .strip()
−8 B
survivor, then closed (M10b, RED on 2)
M11
provider_configured() → return True
−32 B
RED, 4
M12
DOCS.md reverted to the old, vaguer claim
+0 B
RED, the AC4 pairing row
M13
Dutch translation left behind while English moved
+0 B
RED, the both-languages row
⚠️Two findings are recorded rather than rounded off.
M10 was a real survivor. Dropping .strip() from provider_id() left test_price.py, test_price_history.py, test_static_tariff.py and test_validate.py all green — nothing in the repository fed a price_provider with surrounding whitespace, and the .strip() predates this card (it was carried in from the two call sites provider_id() replaced). The Supervisor's list(off|…) schema cannot produce " off ", but validate_startup_config() is also sim/inject.py's entry point and takes any dict. Closed in the same commit.
M8 is the fixture defect this card walked into and then fenced. The first draft of the AC3 fixture fed five minutes of P1 telegrams. Whether that covers the current billing quarter depends on where the wall clock sits inside it, so the peak guard was blind and engaged on some runs — and decide() refuses outright while the guard is holding (§9.1 over §10.1). On those runs the armed baseline did not trade, and "trading off never trades" would have been graded on a fixture where nothing could trade either way. It is 900 s now, and the guard's clear state is a check rather than an assumption.
Not done, deliberately
run_tariff_refresh()'s body is untouched — TARIFF-12 is in there. The expected CHANGELOG.md conflict is trivial.
No version number picked (PKG-06/07: whoever deploys decides).
Nothing deployed, no /addons/ write, no contact with 192.168.2.158 or the inverter.
The provider plan still does not feed the Tariff status row; FCST-04 pinned that as a pair with DOCS.md and it is left alone.
## The defect
`amain()` started the background tariff refresh only when `arbitrage_enabled` was on, and that option ships `false`:
```python
if arbitrage.enabled(opts):
tasks.append(asyncio.create_task(controller.run_tariff_refresh()))
```
`run_tariff_refresh` is the **only** caller of `resolve_tariff()`, which is the **only** caller of `price_history.carry()`. So on a shipped install with a price provider configured **nothing was ever fetched**: no plan resolved, `/data/price_history.json` never created, the *Price history* status row reading `empty` for the life of the install, and TARIFF-09's short-horizon warning unable to fire because there was no fetch to warn about.
Measured over a 40 h soak of two instances side by side on one feed:
| | shipped default + provider | same, arbitrage armed |
|---|---|---|
| feed requests | **0** | fetching |
| `/data/price_history.json` | `present: false`, **802/802 samples** | written |
| resolved plan | none | 132 points |
⚠️ **`git log -S` puts the refresh task, its `arbitrage.enabled()` guard and the `DOCS.md` sentence saying a configured provider still fetches in the *same commit*** — OPT-04, `bdd7f18`. `config.yaml` has carried the claim since TARIFF-02, `852fc7c`. The prose and the code disagreed from the moment both existed, and nothing compared them. Both translations and `config.yaml` additionally carried a claim that had rotted the other way — *"nothing consumes the price series yet"*, untrue since OPT-04 shipped the consumer.
## What changed
**Option A**, as decided. `main.tariff_refresh_wanted()`:
```python
return provider_configured(opts) or arbitrage.enabled(opts)
```
⚠️ **The `or` makes it a STRICT WIDENING** — every configuration that started the task before still starts it — and the second term is not there for symmetry. `price_provider: off` + a `static_profile` + `arbitrage_enabled: true` is a configuration `validate.py` explicitly **accepts**, and it trades. Measured on this branch:
```
opts : price_provider off, arbitrage_enabled true,
cycle_cost_threshold_eur 0.05, static_profile 0.20 night / 0.284 day
plan : static_profile, 48 h, 48 points
WITH the plan -> ARBITRAGE_CHARGE -5000.0 W
WITHOUT the plan -> None "no tariff plan has been resolved yet"
```
Gating on the provider alone would leave that site's `tariff_plan` at `None` forever and silently disarm a strategy the UI says is running — the exact state `validate.py`'s OPT-04 rules exist to refuse, arrived at from inside the code.
⚠️ **Nobody acquires a periodic HTTPS call.** With `price_provider` off, `resolve_tariff()` answers out of the static profile without opening a socket (driven and counted: **0 requests**), and the true shipped default — no provider, no trading — starts **no task at all**. What decides whether a request leaves the machine is `price_provider`, and only that.
⚠️ **AC2's letter vs its harm, stated rather than glossed.** The criterion asks for *no fetch task at all* with `price_provider: off`, *whatever `arbitrage_enabled` says*. One configuration (provider off, trading armed) keeps the task it already had, because taking it away costs the measurement above its trading. The harm the criterion names — a periodic HTTPS call — does not occur in any configuration. **If the owner wants the literal reading, it is one token** (drop the `or`), and `DECISIONS/TARIFF-10-fetching-is-not-trading.md` §3 records the trade.
### Nothing trades that did not trade before
`cycle()`'s `if arbitrage.enabled(self.o):` is untouched. Graded **directly**, by driving `Controller.cycle()` on a resolved provider plan with the peak guard asserted clear, moving exactly one option:
| `arbitrage_enabled` | cheapest hour is now | commanded |
|---|---|---|
| `false` | yes | **+180 W** (`loop -> 180 W`) |
| `false` | no | **+180 W** (`loop -> 180 W`) |
| `true` | yes | −5000 W (`arbitrage -> -5000 W`) |
| `true` | no | +5000 W (`arbitrage -> 5000 W`) |
The armed rows are the half that makes this a check: without them, "no trade" would be satisfied by a fixture in which nothing could have traded either way.
### `TariffPlan.cheap_now()` — decided, not inherited
**Not wired.** A provider-backed plan now exists on every configured site, so *Cheap window* could read it — and *Cheap window* feeds the maintenance cycle's grid top-up, the one place a price moves energy without trading being armed. On a provider-only site (`static_profile` empty) `static_status()` returns `False` on every cycle, so adopting it would take a paid grid top-up from **never** to **sometimes**. `self.cheap` stays on `static_status()`; `DECISIONS` §4 records the three further reasons and the five things adoption would take.
### Surfaces
`DOCS.md`, `config.yaml` and both translations now say what the add-on does, and section 13 pins `DOCS.md` against the **driven** behaviour as a pair so they cannot drift apart again. The *Arbitrage* row's `off (arbitrage_enabled)` string was re-read and left: `self.arb is None` iff the option is off, and on a fetching site it is now the row that says the prices are moving nothing.
## The check, and why it is not a check on an `if`
The `if` was exactly what somebody meant to write, and its comment argued for it honestly — it was true when OPT-04 wrote it and stopped being true when FCST-04 gave the plan a store and a status row. A pin on the expression would have gone red on the fix and green on the defect.
`test_price.py` **section 13** grades **reachability from a configuration**:
1. the guards on every `asyncio.create_task` in `amain()` are **evaluated** against real option dicts — whatever they are spelled as;
2. the call graph under `app/` is walked to see whether the coroutines those guards start arrive at `resolve_tariff()` — wherever it has moved to;
3. and then the fetch is **driven**, with a counting transport and a real store path, because a graph is an argument and a counted request is an observation.
⚠️ **Step 3 grades the gate only because it starts the task the way `amain()` would.** With the defect restored *and* that one line defeated, the four driven rows come back **green** — recorded as M9. What the instrument cannot see is printed on every run, largest hole first.
## Evidence
**29/29 suites green.** 13 mutations in 14 runs, each on a **fresh `cp -a`** of the clean worktree rather than reverted (MUTATION.md habit 18 — no revert to verify, no shared scratch path), `__pycache__` purged, tree mounted `:ro`, run under `python3 -B`.
| id | mutation | Δ bytes | result |
|---|---|---|---|
| M1 | the shipped defect: gate back to `arbitrage.enabled(opts)` | **−4 B** | **RED**, 7 named checks |
| M2 | drop the `or arbitrage.enabled(opts)` term | −27 B | **RED**, 2 |
| M3 | `tariff_refresh_wanted` → always `True` | +8 B | **RED**, the AC2 row |
| M4 | `provider_configured`: `"off"` counts as configured | −6 B | **RED**, the AC2 row |
| M5 | `cycle()`'s trading gate → `if True or …` | +8 B | **RED**, both AC3 rows |
| M6 | instrument: `_reaches()` always `True` | **+0 B** | **RED**, 4 |
| M7 | instrument: `_task_sites()` returns `[]` | **+0 B** | **RED**, 10 |
| M8 | fixture: 5 min of telegrams instead of a whole quarter | **+0 B** | **RED**, the peak-guard precondition + both armed rows |
| M9 | M1 **plus** the harness's guard-driven start defeated | −4 B | the four driven rows go **GREEN** — see above |
| M10 | `provider_id()` drops `.strip()` | −8 B | **survivor**, then closed (M10b, RED on 2) |
| M11 | `provider_configured()` → `return True` | −32 B | **RED**, 4 |
| M12 | `DOCS.md` reverted to the old, vaguer claim | +0 B | **RED**, the AC4 pairing row |
| M13 | Dutch translation left behind while English moved | +0 B | **RED**, the both-languages row |
⚠️ **Two findings are recorded rather than rounded off.**
**M10 was a real survivor.** Dropping `.strip()` from `provider_id()` left `test_price.py`, `test_price_history.py`, `test_static_tariff.py` and `test_validate.py` all green — nothing in the repository fed a `price_provider` with surrounding whitespace, and the `.strip()` predates this card (it was carried in from the two call sites `provider_id()` replaced). The Supervisor's `list(off|…)` schema cannot produce `" off "`, but `validate_startup_config()` is also `sim/inject.py`'s entry point and takes any dict. Closed in the same commit.
**M8 is the fixture defect this card walked into and then fenced.** The first draft of the AC3 fixture fed five minutes of P1 telegrams. Whether that *covers* the current billing quarter depends on where the wall clock sits inside it, so the peak guard was blind and engaged on some runs — and `decide()` refuses outright while the guard is holding (§9.1 over §10.1). On those runs the armed baseline did not trade, and "trading off never trades" would have been graded on a fixture where nothing could trade either way. It is 900 s now, and the guard's clear state is a **check** rather than an assumption.
## Not done, deliberately
* `run_tariff_refresh()`'s body is untouched — **TARIFF-12** is in there. The expected `CHANGELOG.md` conflict is trivial.
* No version number picked (PKG-06/07: whoever deploys decides).
* Nothing deployed, no `/addons/` write, no contact with 192.168.2.158 or the inverter.
* The provider plan still does not feed the *Tariff* status row; FCST-04 pinned that as a pair with `DOCS.md` and it is left alone.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU
amain() started the background tariff refresh only when arbitrage_enabled was
on, and that option ships false. run_tariff_refresh is the only caller of
resolve_tariff(), which is the only caller of price_history.carry() - so on a
shipped install with a price provider configured nothing was ever fetched: no
plan resolved, /data/price_history.json never created, the Price history status
row reading `empty` for the life of the install, and TARIFF-09's short-horizon
warning unable to fire because there was no fetch to warn about. Measured over
a 40 h two-instance soak on one feed: 0 requests and `present: false` across all
802 samples, against a resolved 132-point plan on the instance with arbitrage
armed.
git log -S puts the refresh task, its arbitrage.enabled() guard AND the DOCS.md
sentence saying a configured provider still fetches in the same commit - OPT-04,
bdd7f18; config.yaml has carried the claim since TARIFF-02, 852fc7c. The prose
and the code disagreed from the moment both existed and nothing compared them.
config.yaml and both translations additionally carried a claim that had rotted
the other way - "nothing consumes the price series yet", untrue since OPT-04
shipped the consumer. All four surfaces now say what the add-on does, and a
check pairs DOCS against the DRIVEN behaviour so they cannot drift again.
The gate is tariff_refresh_wanted() = provider_configured(opts) or
arbitrage.enabled(opts), and the `or` is a STRICT WIDENING: every configuration
that started the task before still starts it. The second term is not there for
symmetry. price_provider: off + a static_profile + arbitrage_enabled: true is a
configuration validate.py explicitly ACCEPTS and it trades - measured, at
cycle_cost_threshold_eur 0.05 on a 0.20/0.284 day/night profile,
ARBITRAGE_CHARGE -5000 W. Gating on the provider alone would leave that site's
tariff_plan at None forever and answer "no tariff plan has been resolved yet" on
every cycle, silently disarming a strategy the UI says is running.
Nothing acquires a periodic HTTPS call. With price_provider off, resolve_tariff()
answers out of the static profile without opening a socket (driven and counted:
0 requests), and the true shipped default - no provider, no trading - starts no
task at all. What decides whether a request leaves the machine is price_provider
and only that. The literal reading of the ticket's AC2 - drop the `or` - is one
token and is recorded in DECISIONS §3 with what it costs, for the owner.
AND NOTHING TRADES THAT DID NOT TRADE BEFORE. cycle()'s
`if arbitrage.enabled(self.o):` is untouched, and that is graded directly rather
than inferred from the gate having moved: Controller.cycle() is driven on a
resolved provider plan with the peak guard asserted clear, and arbitrage_enabled
is the only option that moves between the rows - false/false give +180 W
("loop -> 180 W") on both the cheap-now and dear-now plans, true/true give
-5000 W and +5000 W ("arbitrage -> ...").
TariffPlan.cheap_now() is deliberately NOT wired, decided rather than inherited
(DECISIONS §4). A provider-backed plan now exists on every configured site, so
Cheap window could read it - and Cheap window feeds the maintenance cycle's grid
top-up, the one place a price moves energy without trading being armed. On a
provider-only site static_status() returns False on every cycle, so adopting it
would take a paid grid top-up from never to sometimes. self.cheap stays on
static_status(); what adoption would take is listed rather than left implied.
THE CHECK THAT WOULD HAVE CAUGHT THIS is test_price.py section 13, and it grades
REACHABILITY FROM A CONFIGURATION rather than the shape of an `if` - the `if` was
exactly what somebody meant to write. amain()'s guards are EVALUATED against real
option dicts, whatever they are spelled as; the call graph under app/ is walked
to see whether the tasks those guards start arrive at resolve_tariff(), wherever
it has moved to; and then the fetch is DRIVEN with a counting transport, because
a graph is an argument and a counted request is an observation. What the
instrument cannot see is printed on every run, largest hole first.
Evidence: 29/29 green. 13 mutations in 14 runs - M10 re-run as M10b once it was
closed - each applied to a fresh cp -a of the clean worktree rather than
reverted (MUTATION.md habit 18), purged, :ro, -B. The
shipped defect - the gate back at arbitrage.enabled(opts), -4 B - is red on
seven named checks. Dropping the `or` (-27 B) is red on the strict-widening row;
removing cycle()'s trading gate (+8 B) is red on both AC3 rows. The instrument
is mutated too (habit 16): _reaches() always True and _task_sites() returning []
are both +0 B and both red.
Two findings are recorded rather than rounded off. provider_id() dropping
.strip() (-8 B) was a real SURVIVOR across four price suites - nothing fed a
padded provider id - and is closed in this commit by the padded-id pair; and the
first draft of the AC3 fixture fed five minutes of P1 telegrams, so whether the
peak guard was blind depended on where the wall clock sat inside the billing
quarter and the armed baseline failed to trade on some runs. It is 900 s now,
and the guard's clear state is a check rather than an assumption.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU
admin
merged commit 559f0eb022 into release/1.02026-09-12 21:07:55 +02:00
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.
The defect
amain()started the background tariff refresh only whenarbitrage_enabledwas on, and that option shipsfalse:run_tariff_refreshis the only caller ofresolve_tariff(), which is the only caller ofprice_history.carry(). So on a shipped install with a price provider configured nothing was ever fetched: no plan resolved,/data/price_history.jsonnever created, the Price history status row readingemptyfor the life of the install, and TARIFF-09's short-horizon warning unable to fire because there was no fetch to warn about.Measured over a 40 h soak of two instances side by side on one feed:
/data/price_history.jsonpresent: false, 802/802 samples⚠️
git log -Sputs the refresh task, itsarbitrage.enabled()guard and theDOCS.mdsentence saying a configured provider still fetches in the same commit — OPT-04,bdd7f18.config.yamlhas carried the claim since TARIFF-02,852fc7c. The prose and the code disagreed from the moment both existed, and nothing compared them. Both translations andconfig.yamladditionally carried a claim that had rotted the other way — "nothing consumes the price series yet", untrue since OPT-04 shipped the consumer.What changed
Option A, as decided.
main.tariff_refresh_wanted():⚠️ The
ormakes it a STRICT WIDENING — every configuration that started the task before still starts it — and the second term is not there for symmetry.price_provider: off+ astatic_profile+arbitrage_enabled: trueis a configurationvalidate.pyexplicitly accepts, and it trades. Measured on this branch:Gating on the provider alone would leave that site's
tariff_planatNoneforever and silently disarm a strategy the UI says is running — the exact statevalidate.py's OPT-04 rules exist to refuse, arrived at from inside the code.⚠️ Nobody acquires a periodic HTTPS call. With
price_provideroff,resolve_tariff()answers out of the static profile without opening a socket (driven and counted: 0 requests), and the true shipped default — no provider, no trading — starts no task at all. What decides whether a request leaves the machine isprice_provider, and only that.⚠️ AC2's letter vs its harm, stated rather than glossed. The criterion asks for no fetch task at all with
price_provider: off, whateverarbitrage_enabledsays. One configuration (provider off, trading armed) keeps the task it already had, because taking it away costs the measurement above its trading. The harm the criterion names — a periodic HTTPS call — does not occur in any configuration. If the owner wants the literal reading, it is one token (drop theor), andDECISIONS/TARIFF-10-fetching-is-not-trading.md§3 records the trade.Nothing trades that did not trade before
cycle()'sif arbitrage.enabled(self.o):is untouched. Graded directly, by drivingController.cycle()on a resolved provider plan with the peak guard asserted clear, moving exactly one option:arbitrage_enabledfalseloop -> 180 W)falseloop -> 180 W)truearbitrage -> -5000 W)truearbitrage -> 5000 W)The armed rows are the half that makes this a check: without them, "no trade" would be satisfied by a fixture in which nothing could have traded either way.
TariffPlan.cheap_now()— decided, not inheritedNot wired. A provider-backed plan now exists on every configured site, so Cheap window could read it — and Cheap window feeds the maintenance cycle's grid top-up, the one place a price moves energy without trading being armed. On a provider-only site (
static_profileempty)static_status()returnsFalseon every cycle, so adopting it would take a paid grid top-up from never to sometimes.self.cheapstays onstatic_status();DECISIONS§4 records the three further reasons and the five things adoption would take.Surfaces
DOCS.md,config.yamland both translations now say what the add-on does, and section 13 pinsDOCS.mdagainst the driven behaviour as a pair so they cannot drift apart again. The Arbitrage row'soff (arbitrage_enabled)string was re-read and left:self.arb is Noneiff the option is off, and on a fetching site it is now the row that says the prices are moving nothing.The check, and why it is not a check on an
ifThe
ifwas exactly what somebody meant to write, and its comment argued for it honestly — it was true when OPT-04 wrote it and stopped being true when FCST-04 gave the plan a store and a status row. A pin on the expression would have gone red on the fix and green on the defect.test_price.pysection 13 grades reachability from a configuration:asyncio.create_taskinamain()are evaluated against real option dicts — whatever they are spelled as;app/is walked to see whether the coroutines those guards start arrive atresolve_tariff()— wherever it has moved to;⚠️ Step 3 grades the gate only because it starts the task the way
amain()would. With the defect restored and that one line defeated, the four driven rows come back green — recorded as M9. What the instrument cannot see is printed on every run, largest hole first.Evidence
29/29 suites green. 13 mutations in 14 runs, each on a fresh
cp -aof the clean worktree rather than reverted (MUTATION.md habit 18 — no revert to verify, no shared scratch path),__pycache__purged, tree mounted:ro, run underpython3 -B.arbitrage.enabled(opts)or arbitrage.enabled(opts)termtariff_refresh_wanted→ alwaysTrueprovider_configured:"off"counts as configuredcycle()'s trading gate →if True or …_reaches()alwaysTrue_task_sites()returns[]provider_id()drops.strip()provider_configured()→return TrueDOCS.mdreverted to the old, vaguer claim⚠️ Two findings are recorded rather than rounded off.
M10 was a real survivor. Dropping
.strip()fromprovider_id()lefttest_price.py,test_price_history.py,test_static_tariff.pyandtest_validate.pyall green — nothing in the repository fed aprice_providerwith surrounding whitespace, and the.strip()predates this card (it was carried in from the two call sitesprovider_id()replaced). The Supervisor'slist(off|…)schema cannot produce" off ", butvalidate_startup_config()is alsosim/inject.py's entry point and takes any dict. Closed in the same commit.M8 is the fixture defect this card walked into and then fenced. The first draft of the AC3 fixture fed five minutes of P1 telegrams. Whether that covers the current billing quarter depends on where the wall clock sits inside it, so the peak guard was blind and engaged on some runs — and
decide()refuses outright while the guard is holding (§9.1 over §10.1). On those runs the armed baseline did not trade, and "trading off never trades" would have been graded on a fixture where nothing could trade either way. It is 900 s now, and the guard's clear state is a check rather than an assumption.Not done, deliberately
run_tariff_refresh()'s body is untouched — TARIFF-12 is in there. The expectedCHANGELOG.mdconflict is trivial./addons/write, no contact with 192.168.2.158 or the inverter.DOCS.mdand it is left alone.🤖 Generated with Claude Code
https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU