`run_tariff_refresh()` promised in its own docstring that "a failure does not clear the plan", and enforced it with `except Exception` - a branch a supplier outage never reaches. `load_prices()` states the reason in its own words: it "RETURNS, NEVER RAISES, for every failure a site can actually have", because a degradation arriving as a traceback into the planning tick would stop the controller regulating over an expired certificate. So a dead feed RETURNS a §11.4 plan, the `try` succeeds, and the assignment below it overwrote the good plan unconditionally. With `static_profile` unset - the shipped default, and the normal state of a dynamic-tariff site - `_degrade()` returns `TariffPlan(no_data, None, "none")`: a complete 48-hour plan replaced by nothing, at the FIRST failed fetch, ~27 minutes after the last good one. `TARIFF_MAX_AGE_S` (5400 s) never governed anything and there was no serve-stale window at all. Fourth card in one family: FCST-04's M4 (`not ok` only), FCST-05's HIGH (`(JSONDecodeError, OSError)` only), FCST-06 (`not self.ok` only), and now `except Exception` only, for a path that never raises. The question is now asked once, after the attempt, from the evidence that exists - FCST-06's answer, one module over. The evidence is `TariffPlan.from_feed` (did this resolution observe the configured feed) and the shipped `fresh_tariff_plan()`, reused rather than transcribed. A degradation replaces a standing observation only once that observation has aged out, so `TARIFF_MAX_AGE_S` is now the serve-stale window it reads like. A §11.4 static fallback does not replace a fresh provider plan either, although it carries real prices: every point `_degrade()` builds is `provisional=True`, so `firm_points()` is empty, `arbitrage()` refuses and `cheap_now()` is False - everything a consumer may act on is already off - while a day-ahead price for a given hour does not change once published. The rule is deliberately not symmetrical: a standing plan that is itself a degradation IS replaced, or an outage would freeze the operator's static profile at the window it was first generated for. `tariff_at` does not move on a kept plan. It is the moment of the fetch that produced the plan standing, `fresh_tariff_plan()` judges the age from it, and an unmoved timestamp is the only thing that tells a kept plan from a freshly fetched one. test_arbitrage.py section 8b drives the whole chain with nothing stubbed but the socket: the transport raises the way aiohttp does, load_prices() returns no_data, _degrade() builds the §11.4 plan, and the shipped run_tariff_refresh() decides. A check that patched resolve_tariff to RAISE would have passed against the defect. Ten mutations, ten reds, no survivors; 29/29 suites green. Out of scope and unchanged: the persisted price store still does not back the plan during an outage (FCST-03), and the status banner and the Arbitrage row's lag are separately carded. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU
This commit is contained in:
@@ -17,6 +17,72 @@ if it has, it needs 0.10.0. Whoever deploys decides.
|
||||
| _this branch_ | FCST-06 - "cannot persist" stops meaning "has no /data" and starts meaning what it says |
|
||||
| _this branch_ | TARIFF-08 - the day-ahead feed is read in the shape it actually publishes, and the fixtures stop agreeing with the parser |
|
||||
| _this branch_ | TARIFF-09 - the day-ahead feed is asked for the horizon it was given, so tomorrow arrives firm instead of carried |
|
||||
| _this branch_ | TARIFF-12 - a supplier outage stops deleting the plan on its first failed fetch, and TARIFF_MAX_AGE_S starts governing |
|
||||
|
||||
### TARIFF-12 - a returned degradation is not a refresh
|
||||
|
||||
**`run_tariff_refresh()` promised in its own docstring that "a failure does not
|
||||
clear the plan", and enforced it with `except Exception` - a branch a supplier
|
||||
outage never reaches.** `load_prices()` states the reason in its own words: it
|
||||
"RETURNS, NEVER RAISES, for every failure a site can actually have", because a
|
||||
degradation arriving as a traceback into the planning tick would stop the
|
||||
controller regulating over an expired certificate. So a dead feed **returns** a
|
||||
§11.4 plan, the `try` succeeds, and the assignment below it overwrote the good
|
||||
plan unconditionally. With `static_profile` unset - the shipped default, and the
|
||||
normal state of a dynamic-tariff site - `_degrade()` returns
|
||||
`TariffPlan(no_data, None, "none")`: **a complete 48-hour plan replaced by
|
||||
nothing.**
|
||||
|
||||
Measured in a 40-hour soak, `static_profile` empty, a 90-minute outage driven:
|
||||
|
||||
| Brussels | refresh | plan |
|
||||
|---|---|---|
|
||||
| 17:26:12 | 55 | `dynamic_dayahead from generic_dayahead, 141 h, provisional` |
|
||||
| **17:56:12** | **56** | **`no tariff - generic_dayahead: ClientConnectorError: Cannot connect to host`** |
|
||||
| 19:26:12 | 59 | restored, one refresh after the feed returned |
|
||||
|
||||
`origin: none`, `n_points: 0` for all 30 outage samples. The plan was gone ~27
|
||||
minutes after the last good fetch, **at the first failure** - so
|
||||
`TARIFF_MAX_AGE_S` (5400 s) never governed anything, and there was no
|
||||
serve-stale window at all: a transient blip cost exactly what a long outage did.
|
||||
|
||||
**The question is now asked once, after the attempt, from the evidence that
|
||||
exists** - FCST-06's answer, one module over. The evidence is
|
||||
`TariffPlan.from_feed` (did this resolution observe the configured feed) and the
|
||||
shipped `fresh_tariff_plan()` (is an observation still current). A degradation
|
||||
replaces a standing observation only once that observation has **aged out**, so
|
||||
`TARIFF_MAX_AGE_S` is now the serve-stale window it reads like.
|
||||
|
||||
**A §11.4 static fallback does not replace a fresh provider plan either**,
|
||||
although it carries real prices. Every point `_degrade()` builds is
|
||||
`provisional=True`, so `firm_points()` is empty, `arbitrage()` refuses and
|
||||
`cheap_now()` is False - everything a consumer may act on is already off - while
|
||||
a day-ahead price for a given hour does not change once published. The fallback
|
||||
is less actionable and no more current; it takes over when the observation
|
||||
retires, which is what §11.4 is for. The rule is **not** symmetrical: a standing
|
||||
plan that is itself a degradation is replaced by the next one, or an outage
|
||||
would freeze the operator's static profile at the window it was first generated
|
||||
for.
|
||||
|
||||
**`tariff_at` does not move on a kept plan.** It is the moment of the fetch that
|
||||
produced the plan standing, `fresh_tariff_plan()` judges the age from it, and an
|
||||
unmoved timestamp is the only thing that tells a kept plan from a freshly
|
||||
fetched one. Re-stamping it would be the refresh asserting the feed answered
|
||||
when it did not - the "freeze at its last value and look current forever" hazard
|
||||
the same docstring names.
|
||||
|
||||
**What did NOT change.** The persisted price history still does not back the
|
||||
plan during an outage: FCST-03 exists to stop stale prices being acted on, and
|
||||
"recent" is not "current". Nothing about the control loop, the arbiter, or the
|
||||
30 s tick - `cycle()` still reads two attributes and awaits no fetch.
|
||||
|
||||
`test_arbitrage.py` section 8b drives the whole chain with **nothing stubbed but
|
||||
the socket**: the transport raises the way aiohttp does, `load_prices()` returns
|
||||
`no_data`, `_degrade()` builds the §11.4 plan, and the shipped
|
||||
`run_tariff_refresh()` decides. A check that patched `resolve_tariff` to *raise*
|
||||
would have passed against the defect. Ten mutations, ten reds, no survivors -
|
||||
see `DECISIONS/TARIFF-12-a-degradation-is-not-a-refresh.md`.
|
||||
|
||||
|
||||
### TARIFF-09 - the feed is asked for the range the caller wanted
|
||||
|
||||
|
||||
Reference in New Issue
Block a user