Files
goodwe-addon/goodwe_controller/test_price.py
T
glenn schrooyenandClaude Opus 5.5 1d33265aac
checks / unit (pull_request) Successful in 9m20s
infra-pin / fresh (pull_request) Successful in 1s
test_price: grade the Why row of the same render, not a second read of why_words
Since UX-10 the waiting sentence counts its seconds when it is read, so the
second read could cross a second boundary on a loaded runner ("1 second" in
the row, "2 seconds" in the check). CI run 953 on this PR failed that way;
test_price.py passes locally on the same tree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 17:04:55 +02:00

5976 lines
309 KiB
Python
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Runnable check for the price provider framework. `python3 test_price.py`
WHAT THIS FILE IS DEFENDING, in the order the ticket names it:
1. Every shipped plugin emits the EXACT canonical schema (§9.3), all-in.
2. Adding a plugin needs no optimiser change - so the framework must contain
no plugin name, and a plugin invented HERE, in this file, must reach the
right volatility regime without price.py being touched.
3. `manual_csv` works with no network at all.
4. The injection price is a separate scalar (§9.3).
5. "No price data" is a DIFFERENT OUTCOME from "prices, all of them zero".
TWO RULES THIS FILE HOLDS ITSELF TO, because the reviewer will:
* EVERY VERDICT IS DERIVED. There is no `check("the schema is right", True)`
anywhere below. Where a check needs an expected number, the number is
computed here from the same inputs by different arithmetic than price.py
uses, and where it needs a set, the set is built from the objects rather
than typed out.
* NO STIMULUS IS EVALUATED WHERE THE QUANTITY IT GRADES IS DEGENERATE. That
is not decoration here, it is the main hazard in a markup test:
- markup and taxes must DIFFER and both be NON-ZERO, or "added markup
twice" and "added markup and taxes once each" produce the same number
and the double-counting check grades nothing. This file uses 0.031 /
0.057 for the arithmetic and asserts the non-degeneracy conditions as
checks in their own right rather than assuming them. ⚠️ Until TARIFF-18
the shipped pair was 0.03 and 0.06 - taxes EXACTLY twice the markup,
the one degeneracy that collapses "added the markup twice" into "added
both" - and that coincidence was the stated reason for the separate
constants. TARIFF-18 moved taxes to 0.11736 and ENDED it; the constants
stay, because a fixture that IS the shipped default cannot show a
reader which of the two a check really consumed.
- the raw price must be non-zero, or an all-in figure equals markup+taxes
and a plugin that dropped the energy component entirely still passes.
- a fixture must carry BOTH a provisional and a non-provisional point, or
`plan_is_provisional` returns the same answer under any bug.
- the DST checks run on the 23-hour and 25-hour days, not on a normal
24-hour day where a fixed-24-slot implementation and a correct one
agree exactly.
⚠️ MUTATING THE MODULE THIS FILE GRADES? PURGE `__pycache__` FIRST - or run
`tools/mutation_run.sh`, which does. `python3 -B` alone does NOT save you: it
stops bytecode being WRITTEN, not a stale one being READ, and a size-preserving
edit reverted inside one second is invisible to CPython's staleness test. That
is TEST-15, and it has already turned one live mutation into a false survivor.
Read MUTATION.md before you report a survivor.
"""
import ast
import asyncio
import hashlib
import glob
import inspect
import io
import json
import logging
import os
import re
import shutil
import sys
from named_absence import first, nth
import tempfile
from datetime import date, datetime, timedelta, timezone
from textwrap import dedent
from app.price import (
ALL_IN, Absence, BANNED_IN_PLUGIN_CODE, Component, FrameworkOption,
RETIRED_PROVIDERS,
MAX_PLAUSIBLE_EUR_KWH, code_leaks,
FLEMISH_TAXES_COMPONENTS_EUR_KWH, PRICE_OPTION_DEFAULTS,
PROVIDERS, PluginOptions, PricePoint, PriceProduct,
SUPERSEDED_TAXES_DEFAULTS_EUR_KWH,
PriceProvider, PriceRead, ProviderCapabilities, RawPoint, SourceUnavailable,
arbitrage_allowed, assemble, brussels, brussels_date_range,
brussels_offset, check_egress, dated_feed_url,
price_option,
export_formula, export_price_at,
export_price_eur_kwh, load_prices, plan_is_provisional, register,
taxes_at, taxes_profile,
volatility_regime, uncertainty_of, widen_factor, Uncertainty,
)
from app.custom_formula import (
CARD_TOLERANCE, EUR_KWH_PER_EUR_MWH, EXPORT_FORMULA_OPTION, FormulaInvalid,
INDEXES, INJECTION_OFFSET_MAX_EUR_KWH, PASSTHROUGH_HI, PASSTHROUGH_LO,
PLAUSIBLE_HI_EUR_KWH,
PLAUSIBLE_LO_EUR_KWH, RESOLUTIONS, UNITS, card_discrepancy,
implausible_all_in, implausible_injection, in_card_units,
parse_export_formula, parse_price_formula, preview_lines,
price_eur_kwh, stale_card, to_resolution)
from app import custom_formula as _cf
from app import price as _price
from app import arbitrage as _arb
from app import main as _main
from app.p1 import make_sample, split_signed
from app.static_tariff import (
TariffPlan, parse_taxes_profile, static_status)
from app.validate import validate_startup_config
fails = []
def check(name, cond):
print(f" {'ok ' if cond else 'FAIL'} {name}")
if not cond:
fails.append(name)
def run(coro):
return asyncio.run(coro)
def _reason(fn):
"""The message fn() raised, so a check can assert WHICH rule refused.
`raises(...)` only says that something refused. M7 survived because two
different rules could refuse the same URL and the check could not tell them
apart - so where a check is named for a specific rule, it asserts on the
reason instead.
"""
try:
fn()
except Exception as e: # noqa: BLE001
return str(e)
return ""
def raises(fn, exc):
"""True if fn() raises exactly `exc`. A DIFFERENT exception is False, not
True - otherwise "it was refused" and "it broke on the way to being
refused" would grade the same, which is how M7 hid."""
try:
fn()
except exc:
return True
except Exception:
return False
return False
class _Raised:
"""A value a check wanted, whose computation raised.
Compares unequal to everything, is falsy, and prints what went wrong.
"""
def __init__(self, e):
self.e = e
def __eq__(self, other):
return False
def __hash__(self):
return id(self)
def __bool__(self):
return False
def __repr__(self):
return f"<raised {type(self.e).__name__}: {self.e}>"
def derived(fn):
"""Compute a value a check grades. An exception becomes `_Raised`.
⚠️ THIS IS NOT A DEFAULT, and the distinction is the one this whole ticket
is about. `_Raised` is not `[]`, not `None`, not 0; it equals nothing, it is
falsy, and it renders as the exception that produced it. Its only job is
that a mutation which makes a derivation IMPOSSIBLE - the reviewer removing
`_NoData`'s missing `.points`, say - reports as a failed check naming the
exception, instead of aborting this file before the sections below it run.
A silent `except: return []` here would be the defect under test, in the
harness that tests it.
"""
try:
return fn()
except Exception as e: # noqa: BLE001
return _Raised(e)
UTC = timezone.utc
HERE = os.path.dirname(os.path.abspath(__file__))
# ⚠️ NOT the shipped pair, and deliberately so. See this module's docstring.
# Until TARIFF-18 the shipped pair was 0.03 / 0.06, where taxes are exactly
# 2 x the markup and several confusions collapse; TARIFF-18 moved taxes to
# 0.11736 and ended that particular degeneracy. These constants are kept
# because the reason outlives it: a fixture EQUAL to the shipped default cannot
# distinguish a check that read the fixture from one that read the default.
# Section 0's fourth check is what pins that, per figure, and the three
# degeneracy conditions are asserted below rather than assumed.
MARKUP = 0.031
TAXES = 0.057
def opts(**over):
"""A price configuration. Everything below is this, spoilt."""
o = {"price_provider": "off",
"price_markup_eur_kwh": MARKUP,
"price_taxes_eur_kwh": TAXES,
"price_timeout_s": 5}
o.update(over)
return o
def hours(n, start, step_h=1):
return [start + timedelta(hours=i * step_h) for i in range(n)]
# --------------------------------------------------------------------------- #
print("0. the arithmetic this file grades with must not be degenerate")
# ⚠️ These four ARE the guard against a check that passes for the wrong reason.
# If any of them goes red, every markup assertion below is measuring nothing.
check("markup and taxes are both non-zero", MARKUP != 0 and TAXES != 0)
check("...and differ, so 'added the wrong one' is visible",
MARKUP != TAXES)
check("...and markup twice != markup plus taxes, so double-counting the markup "
"is distinguishable from correctly adding both",
abs(2 * MARKUP - (MARKUP + TAXES)) > 1e-9)
# ⚠️ TARIFF-18 CHANGED THE ARGUMENT THIS CHECK MAKES, AND THE CHANGE IS
# RECORDED RATHER THAN QUIETLY DROPPED. Until TARIFF-18 the shipped pair was
# (0.03, 0.06) - taxes EXACTLY twice the markup - which is the one degeneracy
# that makes "added the markup twice" indistinguishable from "added both", and
# this check named that coincidence as the reason the file grades with its own
# constants. Raising the taxes default to 0.11736 ENDS the coincidence, so that
# sentence is no longer true and is not kept. What the file needs from the
# shipped pair is weaker and is now asserted directly: each shipped figure must
# DIFFER from the constant this file grades with, or a check that read the
# shipped default instead of the fixture would pass by coincidence. Asserted
# per-figure and not as a tuple - a tuple comparison is satisfied by one of the
# two matching.
check(f"...and NEITHER shipped default is what this file grades with: shipped "
f"({PRICE_OPTION_DEFAULTS['price_markup_eur_kwh']}, "
f"{PRICE_OPTION_DEFAULTS['price_taxes_eur_kwh']}) against this file's "
f"({MARKUP}, {TAXES})",
MARKUP != PRICE_OPTION_DEFAULTS["price_markup_eur_kwh"]
and TAXES != PRICE_OPTION_DEFAULTS["price_taxes_eur_kwh"])
# --------------------------------------------------------------------------- #
print()
print("1. the shipped defaults live in config.yaml, and here only as a mirror")
# ⚠️ CFG-03. The add-on already carries option defaults in three places. This
# ticket adds ONE in-code mirror (PRICE_OPTION_DEFAULTS) and pins it to
# config.yaml by reading the file rather than by transcribing it - the same
# move test_validate.py makes for max_w and meter_freeze_s. If they drift, this
# goes red and somebody has to decide which is right.
_CFG = os.path.join(os.path.dirname(os.path.abspath(__file__)), "config.yaml")
with open(_CFG, "r", encoding="utf-8") as fh:
_cfg_lines = fh.read().splitlines()
def shipped(key):
"""The value config.yaml ships, from the `options:` block, parsed by hand.
No yaml module in the shipped image, and this is two lines of parsing
against a file whose shape is known - the alternative is a transcription,
which is the thing being guarded against.
"""
in_schema = False
for line in _cfg_lines:
if line.startswith("schema:"):
in_schema = True
if in_schema or not line.startswith(f" {key}:"):
continue
raw = line.split(":", 1)[1].split("#", 1)[0].strip().strip('"').strip("'")
# OPT-04 added the first BOOLEAN price option, and without this the
# mirror check would compare the string "false" against `False` and go
# red for a file that is perfectly in step. Parsing a bool as a bool is
# what the yaml module this image does not carry would have done; it
# widens nothing, because a key whose shipped value disagrees with the
# mirror still drifts.
if raw in ("true", "false"):
return raw == "true"
try:
return int(raw)
except ValueError:
pass
try:
return float(raw)
except ValueError:
return raw
return KeyError
_found = {k: shipped(k) for k in PRICE_OPTION_DEFAULTS}
_missing = sorted(k for k, v in _found.items() if v is KeyError)
check(f"config.yaml ships every price option ({len(PRICE_OPTION_DEFAULTS)} keys)",
_missing == [])
_drift = sorted(k for k, v in _found.items()
if v is not KeyError and v != PRICE_OPTION_DEFAULTS[k])
check(f"...and each shipped default equals the in-code mirror {_found}",
_drift == [])
# The schema block must list them too, or the Supervisor drops the option and
# the add-on silently runs on the mirror's value instead of the operator's.
_schema_block = _cfg_lines[_cfg_lines.index("schema:"):]
_unschemad = sorted(k for k in PRICE_OPTION_DEFAULTS
if not any(ln.startswith(f" {k}:") for ln in _schema_block))
check("...and every one of them is in the schema block too", _unschemad == [])
# ⚠️ Derived from the registry, not typed: the schema's provider list must offer
# exactly the plugins that exist PLUS the retired ids, or the UI and the code
# disagree about what is installable.
#
# ⚠️ THE RETIRED IDS ARE IN THE SCHEMA ON PURPOSE AND THIS IS THE CHECK THAT
# KEEPS THEM THERE (TARIFF-07). The Supervisor validates the options file
# against this line BEFORE the add-on runs. Drop a retired id from it and an
# EXISTING config naming that id becomes schema-invalid: the operator gets an
# opaque "expected one of ..." and validate.py's retired-id refusal - the whole
# point of the withdrawal - is never reached, on exactly the sites that need
# it. So the schema is deliberately WIDER than the registry, by exactly
# RETIRED_PROVIDERS and by nothing else. Both halves are graded: an id that is
# in neither set, and a retired id that has gone missing.
_line = first((ln for ln in _schema_block if ln.startswith(" price_provider:")),
"price_provider in the schema")
_schema_ids = (set(_line.split("list(", 1)[1].rstrip(")").split("|"))
if "list(" in _line else set())
_wanted = set(PROVIDERS) | set(RETIRED_PROVIDERS) | {"off"}
check(f"the schema offers exactly the registered plugins, the retired ids and "
f"'off' {sorted(_schema_ids)}",
_schema_ids == _wanted)
check(f"...and every retired id is STILL offered, so a config already naming "
f"one reaches validate.py rather than the Supervisor's schema error "
f"{sorted(set(RETIRED_PROVIDERS) - _schema_ids) or 'none missing'}",
set(RETIRED_PROVIDERS) <= _schema_ids and set(RETIRED_PROVIDERS) != set())
check("...and no retired id is ALSO a live plugin, which would silently take "
"that refusal away",
set(RETIRED_PROVIDERS) & set(PROVIDERS) == set())
# --------------------------------------------------------------------------- #
print()
print("1b. TARIFF-18 - the shipped taxes default is a SUM OF NAMED CARD ROWS")
# ⚠️ THE DEFECT THIS SECTION PREVENTS IS THE ONE THE SHIPPED 0.06 WAS: A NUMBER
# WITH NO PROVENANCE. 0.06 EUR/kWh is roughly HALF the real Flemish figure, and
# nobody could argue with it because there was nothing written down to argue
# against - no rows, no card, no date. Section 1 above pins the default against
# config.yaml, which catches DRIFT between the two copies and says nothing
# whatever about whether the number is right. This section grades the number.
#
# ⚠️ AND IT GRADES IT FROM TWO INDEPENDENT SIDES, because the module's rows and
# the module's default are both in app/price.py and an edit that moved BOTH
# would satisfy a check that only re-added the rows (MUTATION.md: "where did
# each side of this comparison come from"). So: the default is re-added from
# the shipped rows, AND the total is compared against the card's own figures
# TYPED HERE, in the units the card prints them in.
print(" app/price.py's rows, c EUR/kWh incl. 6 % VAT:")
for _name, _v in FLEMISH_TAXES_COMPONENTS_EUR_KWH:
print(f" {_v * 100:>7.3f} {_name}")
_T18_SUM = sum(v for _n, v in FLEMISH_TAXES_COMPONENTS_EUR_KWH)
_T18_DEFAULT = PRICE_OPTION_DEFAULTS["price_taxes_eur_kwh"]
print(f" {_T18_SUM * 100:>7.3f} = the shipped default {_T18_DEFAULT}")
check(f"the {len(FLEMISH_TAXES_COMPONENTS_EUR_KWH)} rows re-add to the shipped "
f"default: {round(_T18_SUM, 6)} against {_T18_DEFAULT}",
abs(_T18_SUM - _T18_DEFAULT) < 1e-12)
# Side two: the Eneco residential card for Flanders, September 2026, in the
# c EUR/kWh it prints. Typed here, so an edit inside app/price.py cannot move
# both sides of a comparison at once.
_T18_CARD_C = (5.35 # Fluvius distribution, Antwerpen
+ 4.876 # bijzondere accijns, "Alle verbruik"
+ 1.51 # bijdrage groene stroom en WKK Vlaanderen
+ 0.0) # energiebijdrage, 0 EUR/MWh since 2026-08-01
check(f"...and the card itself, typed in this file in c EUR/kWh, totals "
f"{_T18_CARD_C:.3f} c = {_T18_CARD_C / 100:.5f} EUR/kWh, which IS the "
f"shipped default to the card's own last digit",
abs(_T18_CARD_C / 100.0 - _T18_DEFAULT) < 1e-9)
# ⚠️ THE ZERO ROW IS A TRAP MARKER AND IS ASSERTED AS ONE. "Vanaf 1 augustus
# 2026 bedraagt de energiebijdrage 0 EUR/MWh" - it was merged into the
# bijzondere accijns. Anybody re-deriving this total from a guide written
# before that date finds a separate energiebijdrage line and adds it to an
# accijns that already contains it. Deleting the row is the edit that lets that
# happen silently, so the row's PRESENCE is graded and not only the sum.
_T18_NAMES = [n.lower() for n, _v in FLEMISH_TAXES_COMPONENTS_EUR_KWH]
check("the energiebijdrage is carried as a row worth ZERO rather than left "
"out, so the next person to re-derive this total finds it already "
"accounted for instead of adding it a second time",
any("energiebijdrage" in n for n in _T18_NAMES)
and all(v == 0.0 for n, v in FLEMISH_TAXES_COMPONENTS_EUR_KWH
if "energiebijdrage" in n.lower()))
check("...and the three rows that are NOT zero are distinct non-zero figures, "
"so the sum above is not being carried by one row with the others inert",
len({v for _n, v in FLEMISH_TAXES_COMPONENTS_EUR_KWH if v}) == 3)
# ⚠️ THE PER-DSO BAND, AND THE DIRECTION THE DEFAULT ERRS IN. Only the
# distribution row moves between Fluvius DSOs - 5,28 c EUR/kWh in
# Midden-Vlaanderen to 6,70 in West - so the all-in Flemish total runs about
# 0.117 to 0.131 EUR/kWh. The default must be INSIDE that band and in its lower
# half: this scalar is inert on which hour is cheapest (section 16b) but §17's
# negative-price guard reads the ABSOLUTE all-in price, and a figure set too
# high carries a genuinely negative hour above zero. test_arbitrage.py 2c
# drives that boundary through the shipped decision.
_T18_LO, _T18_HI = 0.117, 0.131
check(f"the default {_T18_DEFAULT} is inside the Flemish per-DSO band "
f"{_T18_LO}-{_T18_HI} EUR/kWh, and in its LOWER HALF, which is the "
f"direction \u00a717's guard makes safe",
_T18_LO <= _T18_DEFAULT <= (_T18_LO + _T18_HI) / 2)
# ⚠️ READ DEFENSIVELY ON PURPOSE. Emptying the superseded tuple must produce a
# NAMED red, not an IndexError inside this f-string - a crash takes the file
# red with nothing on the record saying which claim broke, which is MUTATION.md
# reporting rule 3 in the negative.
_T18_OLD = (SUPERSEDED_TAXES_DEFAULTS_EUR_KWH[0]
if SUPERSEDED_TAXES_DEFAULTS_EUR_KWH else None)
check(f"...and it is no longer the superseded {_T18_OLD}, which sat at "
f"{(_T18_OLD / _T18_DEFAULT) if _T18_OLD else 0:.0%} of the Flemish "
f"figure - about half of what the operator actually pays",
SUPERSEDED_TAXES_DEFAULTS_EUR_KWH == (0.06,)
and all(abs(_T18_DEFAULT - u) > 1e-9
for u in SUPERSEDED_TAXES_DEFAULTS_EUR_KWH))
# --------------------------------------------------------------------------- #
print()
print("2. every shipped plugin emits the exact canonical schema (AC1)")
# One recorded fixture per plugin, none of them a real fetch. Prices are
# deliberately distinct, non-zero, and one of them NEGATIVE - §17 keeps a
# negative-price regression case, and a windy Sunday is the best charging hour
# of the year, so a framework that clamped it away would cost real money.
T0 = datetime(2026, 1, 15, 0, 0, tzinfo=UTC)
# ⚠️ THE NEGATIVE HOUR IS -95 EUR/MWh, NOT -12.5, AND THE MAGNITUDE IS THE
# POINT. All-in adds 0.088 EUR/kWh here, so a -12.5 EUR/MWh hour comes out at
# +0.0755 and the "a negative price survives" check below would be grading a
# positive number - passing whether or not the framework clamped anything.
# -95 EUR/MWh is both realistic for a windy Belgian Sunday and still negative
# after markup and taxes, which is the only version of the stimulus that grades
# what it claims to.
EPEX_EUR_MWH = [61.4, -95.0, 183.0] # EUR/MWh, one deeply negative
RETAIL_EUR_KWH = [0.1341, 0.0872, 0.2610] # EUR/kWh, markup already inside
# ⚠️ THE INVENTED ROW-PER-POINT FIXTURE IS GONE, AND SO IS ITS PARSER
# (TARIFF-07). Until this card every price fixture here was a
# `{"data": [{"start": ..., "price_eur_mwh": ...}]}` document that
# `_points_from_json()` was written to read and that NO FEED ANYBODY REACHED
# HAS EVER PUBLISHED. The three plugins fed from it - `engie_dynamic`,
# `totalenergies_dynamic`, `eneco_eddy` - had no captured body because their
# endpoints do not exist, so the loop was closed twice over: an invented parser
# graded against an invented body, on behalf of plugins that could never fetch.
# Both plugins that remain are replayed from something real - `generic_dayahead`
# from captured bytes (section 2b), `manual_csv` from the CSV a human types.
#
# The shape itself survives in ONE place and in one direction only: as the body
# section 9 feeds `generic_dayahead` to prove that a row-per-point document is a
# FAILURE for it rather than a second dialect it might sniff. It is named for
# what it is.
def row_shape_body(key, values):
"""A `{"data": [...]}` document. ⚠️ EXISTS ONLY TO BE REFUSED.
No feed this repository has ever reached publishes this, and the parser
that read it was withdrawn with the three plugins it served (TARIFF-07).
It is kept as a NEGATIVE stimulus: if `_points_from_columnar_json()` ever
grows a sniff for `data`, section 9's "the body is the OTHER shape" row
turns from `no_data` into a series and a check goes red, rather than
TARIFF-08's scope rule being widened by a review nobody remembers.
"""
return json.dumps({"data": [
{"start": (T0 + timedelta(hours=i)).isoformat(), key: v,
"provisional": False}
for i, v in enumerate(values)]}).encode()
def columnar_feed(values, provisional_at=None, unit="EUR / MWh",
step_min=60, start=None, prices=None):
"""A body in the shape `api.energy-charts.info` really publishes.
Parallel arrays and no per-point object; `unix_seconds` are UTC epoch
seconds. `prices` overrides the price column outright, which is how the
unequal-length case below is built without hand-writing JSON.
⚠️ THERE IS NO PER-POINT `provisional` IN THIS SHAPE, so `provisional_at`
exists only to be REFUSED - the columnar document cannot carry the flag and
the plugin supplies it. It is a keyword the callers do not pass; the
parameter is here so that a future caller who reaches for it gets a
TypeError rather than a silently ignored argument.
"""
if provisional_at is not None:
raise TypeError(
"the columnar document has no per-point provisional flag; "
"`generic_dayahead` declares it in fetch(). See "
"_points_from_columnar_json().")
t0 = start if start is not None else T0
secs = [int((t0 + timedelta(minutes=step_min * i)).timestamp())
for i in range(len(values))]
doc = {"unix_seconds": secs,
"price": list(values) if prices is None else list(prices),
"unit": unit,
"license_info": "CC BY 4.0 (creativecommons.org/licenses/by/4.0) "
"from Bundesnetzagentur | SMARD.de",
"deprecated": False}
return json.dumps(doc).encode()
# TARIFF-15. THE TWO REAL TARIFF CARDS, as the operator would type them.
# ⚠️ THESE ARE NOT INVENTED. Both suppliers print the formula, the reference
# index they assumed AND the resulting price on the same card, so section 13
# can grade the SHIPPED code against a number somebody else published - which
# is the only kind of expectation this file is allowed to have (2b's rule,
# applied to arithmetic rather than to bytes).
ENECO_CARD = {"index": "epex_be", "resolution": "quarter_hourly",
"factor": 0.102, "offset": 1.00, "units": "c_eur_per_kwh",
"vat_pct": 6,
"card": {"month": "2026-09", "index_eur_per_mwh": 133.9905,
"expect_incl_vat": 15.55}}
ENGIE_CARD = {"index": "epex_be", "resolution": "quarter_hourly",
"factor": 0.100, "offset": 0.90, "units": "c_eur_per_kwh",
"vat_pct": 6,
"card": {"month": "2026-09", "index_eur_per_mwh": 61.96,
"expect_incl_vat": 7.522}}
# The energy+markup component of the shared EPEX fixture under Eneco's card,
# spelled out here as the card spells it: c EUR/kWh, then /100 to EUR/kWh.
FORMULA_EUR_KWH = [(0.102 * v + 1.00) * 1.06 / 100.0 for v in EPEX_EUR_MWH]
CSV_BODY = ("start,eur_per_kwh,provisional\n"
"2026-01-15T00:00:00+01:00,0.1834,false\n"
"2026-01-15T01:00:00+01:00,-0.0210,true\n"
"2026-01-15T02:00:00+01:00,0.3115,false\n")
# The fixture, the transport that serves it, and - crucially - the ENERGY
# component of each point, computed here so the all-in expectation below is
# arithmetic this file did itself.
# ⚠️ `generic_dayahead` IS FED THE COLUMNAR SHAPE AND THE OTHER THREE ARE NOT,
# and the asymmetry is the honest one rather than an oversight: the columnar
# document is what the one endpoint anybody has ever reached actually returns.
FIXTURES = {
"generic_dayahead": (columnar_feed(EPEX_EUR_MWH),
[v / 1000 for v in EPEX_EUR_MWH]),
"custom_formula": (columnar_feed(EPEX_EUR_MWH), FORMULA_EUR_KWH),
"manual_csv": (None, [0.1834, -0.0210, 0.3115]),
}
check(f"the plugins that ship after TARIFF-07 and TARIFF-15 are registered "
f"{sorted(PROVIDERS)}",
set(PROVIDERS) == {"generic_dayahead", "custom_formula", "manual_csv"})
check(f"...and the three withdrawn ids resolve to NOTHING "
f"{sorted(RETIRED_PROVIDERS)}",
set(RETIRED_PROVIDERS) == {"engie_dynamic", "totalenergies_dynamic",
"eneco_eddy"}
and not (set(RETIRED_PROVIDERS) & set(PROVIDERS)))
def _readd_retired():
return register(type("_ReAdd", (PriceProvider,), {
"id": "eneco_eddy",
"capabilities": classmethod(lambda c: ProviderCapabilities(
product=PriceProduct.DAY_AHEAD,
sources=frozenset({Component.ENERGY}))),
"fetch": lambda s, o, a, b, t: None}))
# ⚠️ GRADED ON THE REASON, NOT THE VERDICT. `register()` already refuses a
# DUPLICATE id, so "it raised ValueError" would pass on a tree where the
# retired guard is deleted and the id happens to be live again - the masking
# shape MUTATION.md's TEST-16 section is about.
_readd_why = _reason(_readd_retired)
check(f"re-registering a retired id is refused, and refused for the RETIRED "
f"reason rather than the duplicate one ({_readd_why[:60]!r})",
"RETIRED_PROVIDERS" in _readd_why and "eneco_eddy" in _readd_why)
check("...and the refusal did not take effect anyway",
"eneco_eddy" not in PROVIDERS)
check("...and the fixture set covers every one of them",
set(FIXTURES) == set(PROVIDERS))
def canned(body):
"""A transport that serves recorded bytes and records that it was used."""
calls = []
async def t(url, timeout_s):
calls.append((url, timeout_s))
return body
t.calls = calls
return t
def load(pid, extra=None, transport=None):
o = opts(price_provider=pid, **(extra or {}))
return run(load_prices(o, T0, T0 + timedelta(hours=48),
transport=transport))
import tempfile # noqa: E402
_csv_dir = tempfile.mkdtemp(prefix="tariff02-")
CSV_PATH = os.path.join(_csv_dir, "prices.csv")
with open(CSV_PATH, "w", encoding="utf-8") as fh:
fh.write(CSV_BODY)
def load_any(pid):
body, _ = FIXTURES[pid]
if pid == "manual_csv":
return load(pid, {"price_csv_path": CSV_PATH}, transport=refuse_transport)
if pid == "custom_formula":
# ⚠️ THE SAME BODY AS generic_dayahead, ON PURPOSE. Both read the same
# feed, so feeding them different bytes would hide the fact that what
# differs between them is the ARITHMETIC and the declaration - which
# is exactly what section 3 grades.
return load(pid, {"price_formula": json.dumps(ENECO_CARD)},
transport=canned(body))
return load(pid, transport=canned(body))
async def _refuse(url, timeout_s):
raise AssertionError(f"the offline plugin reached the network: {url}")
refuse_transport = _refuse
READS = {pid: load_any(pid) for pid in PROVIDERS}
_wrong_kind = sorted(p for p, r in READS.items() if r.kind != "series")
check(f"every plugin returned a series from its fixture", _wrong_kind == [])
# THE SCHEMA. Exactly three keys, in every point of every plugin, and the check
# derives the key set from the objects rather than asserting a typed literal.
_keys = derived(lambda: {frozenset(p.keys())
for r in READS.values()
for p in r.canonical()["prices"]})
check(f"every canonical point has exactly the three §9.3 keys, and only those "
f"-> {_keys}",
_keys == {frozenset({"start", "eur_per_kwh", "provisional"})})
check("the envelope is exactly {'prices': [...]}",
derived(lambda: all(set(r.canonical()) == {"prices"}
for r in READS.values())) is True)
_badtype = derived(lambda: sorted(
p for p, r in READS.items()
if not all(isinstance(pt["eur_per_kwh"], float)
and isinstance(pt["provisional"], bool)
and isinstance(pt["start"], str)
for pt in r.canonical()["prices"])))
check(f"...with a float price, a bool flag and an ISO string start {_badtype}",
_badtype == [])
_naive = derived(lambda: sorted(p for p, r in READS.items()
if any(pt.start.utcoffset() is None
for pt in r.points)))
check(f"every start is timezone-aware (§9.3 shows +01:00) {_naive}",
_naive == [])
_offsets = derived(lambda: {pt.start.utcoffset()
for r in READS.values() for pt in r.points})
check(f"...and at a Belgian winter offset for a January fixture {_offsets}",
_offsets == {timedelta(hours=1)})
check("the whole envelope round-trips through json.dumps",
derived(lambda: all(json.loads(json.dumps(r.canonical())) == r.canonical()
for r in READS.values())) is True)
# --------------------------------------------------------------------------- #
print()
print("2b. TARIFF-08 - the SHIPPED parser, replayed against REAL captured "
"bodies")
#
# ⚠️ WHY THIS SECTION EXISTS, WHICH IS THE MORE IMPORTANT HALF OF THE TICKET.
# Until 2026-09-10 every price fixture in this repository - here, in
# test_price_history.py and in test_static_tariff.py - was a
# `{"data": [{"start": ..., "price_eur_mwh": ...}]}` document invented by
# whoever wrote `_points_from_json()`. `api.energy-charts.info` has never
# published that shape. So the suite validated the parser against the parser's
# own assumption: a closed loop that cannot notice the assumption is wrong, and
# it stayed green while `generic_dayahead` raised `SourceUnavailable` on every
# live fetch it ever made. §11.4 then made that silent - a parse error becomes
# `no_data` becomes the static profile with arbitrage off, which is the CORRECT
# degradation and says nothing at all about the cause.
#
# The only thing that breaks the loop is bytes nobody here wrote. Both fixtures
# below are unmodified response bodies, captured by hand with curl on
# 2026-09-10; `scenarios/energy-charts-be-price-captures.json` records the URL,
# the retrieval time, the licence as returned and a sha256 of each, and the
# checksums are RE-DERIVED here so a fixture edited to make a check pass stops
# being a capture and starts being a failure.
#
# ⚠️ THERE IS NOW EXACTLY ONE NETWORKED PLUGIN, WHICH IS TARIFF-07's DOING.
# `engie_dynamic`, `totalenergies_dynamic` and `eneco_eddy` had no capture
# because their endpoints do not exist - verified by hand on 2026-09-13: a 404
# from a gateway with no route, a TLS hostname mismatch, and a 404 with an empty
# body. They were withdrawn rather than left fed by an invented fixture. The
# check below is derived from the shipped `PROVIDERS` registry, so a plugin
# added with no capture forces the question rather than inheriting silence.
_CAPTURES = json.load(open(
os.path.join(HERE, "scenarios", "energy-charts-be-price-captures.json"),
encoding="utf-8"))
def _capture(name):
with open(os.path.join(HERE, "scenarios", name), "rb") as fh:
return fh.read()
_REAL = {n: _capture(n) for n in _CAPTURES["files"]}
_bad_sha = sorted(n for n, b in _REAL.items()
if hashlib.sha256(b).hexdigest()
!= _CAPTURES["files"][n]["sha256"])
check(f"all three captured bodies are the bytes the provenance file "
f"recorded {sorted(_REAL)}",
len(_REAL) == 3 and _bad_sha == [])
# The document really is columnar, derived from the bytes rather than asserted:
# if it ever grows a `data` key this check says so before any parser does.
_shapes = {n: sorted(json.loads(b.decode("utf-8"))) for n, b in _REAL.items()}
check(f"...and every one is COLUMNAR - parallel arrays, no 'data' key "
f"{sorted({tuple(v) for v in _shapes.values()})}",
all(v == ["deprecated", "license_info", "price", "unit", "unix_seconds"]
for v in _shapes.values()))
check("...which is precisely the shape the shipped parser used to demand a "
"'data' list from",
all("data" not in v for v in _shapes.values()))
# ⚠️ CC BY 4.0 IS WHY THESE BYTES MAY BE IN THE REPOSITORY AT ALL. DATA-02
# established that the BE zone is redistributable with attribution; the feed
# restates the licence per response, so a capture whose own licence line has
# changed is a capture that must not stay committed.
_lic = {n: json.loads(b.decode("utf-8"))["license_info"] for n, b in _REAL.items()}
check(f"...and each carries the CC BY 4.0 line the provenance block cites",
all("CC BY 4.0" in v for v in _lic.values())
and all(_lic[n] == _CAPTURES["files"][n]["licence_as_returned"]
for n in _REAL))
_QH = "energy-charts-be-price-2026-09-10-quarterhourly.json"
_H1 = "energy-charts-be-price-2025-09-10-hourly.json"
# ⚠️ THE TWO GRANULARITIES ARE NOT DECORATION. Belgium moved to a 15-minute MTU
# on 2025-10-01 and the same endpoint answers 96 points at 900 s for a day after
# it and 24 at 3600 s for a day before it. A fixture at one spacing cannot grade
# whether anything assumes the other, so both are replayed and the spacings are
# DERIVED from the bodies rather than typed.
_spacing = {}
for _n, _b in _REAL.items():
_u = json.loads(_b.decode("utf-8"))["unix_seconds"]
_spacing[_n] = sorted({_u[i + 1] - _u[i] for i in range(len(_u) - 1)})
check(f"the two captures really differ in granularity - {_spacing[_QH]} s over "
f"{len(json.loads(_REAL[_QH])['price'])} points and {_spacing[_H1]} s "
f"over {len(json.loads(_REAL[_H1])['price'])} points - or replaying both "
f"grades nothing more than replaying one",
_spacing[_QH] == [900] and _spacing[_H1] == [3600])
_REPLAY = {n: load("generic_dayahead", transport=canned(b))
for n, b in _REAL.items()}
_not_series = sorted(n for n, r in _REPLAY.items() if r.kind != "series")
check(f"the SHIPPED generic_dayahead parses both real bodies "
f"{[r.words for r in _REPLAY.values()]}",
_not_series == [])
# The point count is the source's own interval count, taken from the document.
_counts = derived(lambda: {n: len(r.points) for n, r in _REPLAY.items()})
_want_counts = {n: len(json.loads(b.decode("utf-8"))["price"])
for n, b in _REAL.items()}
check(f"...emitting one point per published interval, no decimation "
f"{_counts}", _counts == _want_counts)
check(f"...which for the quarter-hourly body is 96 points and not 24 - the "
f"15-minute MTU is the shape of the real feed, not a hypothetical",
_want_counts[_QH] == 96 and _want_counts[_H1] == 24)
# §9.3: unix_seconds are UTC instants, RENDERED at the Brussels offset. Both
# captured days are inside CEST, so the correct first start is local midnight at
# +02:00. ⚠️ A NAIVE READING - `datetime.fromtimestamp(t)` with no tzinfo - is
# what this grades: it would produce the host's local wall clock, and on a UTC
# host that is 22:00 of the previous day. The expectation is computed here from
# the raw epoch second by different arithmetic than price.py uses.
_first_raw = {n: json.loads(b.decode("utf-8"))["unix_seconds"][0]
for n, b in _REAL.items()}
_first_utc = {n: datetime(1970, 1, 1, tzinfo=UTC) + timedelta(seconds=t)
for n, t in _first_raw.items()}
_first_got = derived(lambda: {n: r.points[0].start
for n, r in _REPLAY.items()})
check(f"every start is the UTC instant the feed published "
f"{ {n: v.isoformat() for n, v in _first_utc.items()} }",
isinstance(_first_got, dict)
and all(_first_got[n] == _first_utc[n] for n in _REAL))
check(f"...rendered at the Brussels offset, so a September day starts at "
f"00:00+02:00 and NOT at the 22:00 a naive local reading gives "
f"{ {n: v.isoformat() for n, v in (_first_got if isinstance(_first_got, dict) else {}).items()} }",
isinstance(_first_got, dict)
and all(v.utcoffset() == timedelta(hours=2) for v in _first_got.values())
and all((v.hour, v.minute) == (0, 0) for v in _first_got.values()))
# EUR/MWh -> all-in EUR/kWh, recomputed here from the raw column.
_allin_want = {n: [round(v / 1000 + MARKUP + TAXES, 6)
for v in json.loads(b.decode("utf-8"))["price"]]
for n, b in _REAL.items()}
_allin_got = derived(lambda: {n: [p.eur_per_kwh for p in r.points]
for n, r in _REPLAY.items()})
check(f"...and every price is the published EUR/MWh figure over 1000 plus "
f"markup and taxes (first: {[v[0] for v in _allin_want.values()]})",
_allin_got == _allin_want)
# ⚠️ NON-DEGENERACY. If the captured prices happened to be all equal, the
# arithmetic check above would pass under a parser that emitted the same number
# for every interval.
_raw_spread = {n: round(max(v) - min(v), 6)
for n, v in ((k, json.loads(b.decode("utf-8"))["price"])
for k, b in _REAL.items())}
check(f"...on bodies whose prices actually vary, or that check grades a "
f"constant {_raw_spread}",
all(v > 1.0 for v in _raw_spread.values()))
# --- the refusals, driven from the real body by breaking one thing ---------- #
def _edited(name, **fields):
"""The captured body with one field replaced. Everything else is real."""
doc = json.loads(_REAL[name].decode("utf-8"))
doc.update(fields)
return json.dumps(doc).encode()
_full = json.loads(_REAL[_QH].decode("utf-8"))
_short = load("generic_dayahead",
transport=canned(_edited(_QH, price=_full["price"][:-4])))
check(f"a `price` array 4 entries short of `unix_seconds` is no_data, NOT a "
f"series truncated to {len(_full['price']) - 4} points - the missing "
f"tail of a Belgian day is the evening peak, and a short series is "
f"traded on where no data at all switches arbitrage off (§11.4)",
_short.kind == "no_data")
check("...and the reason names the two lengths, so an operator can see it was "
"a length mismatch and not an outage",
_short.kind == "no_data"
and str(len(_full["unix_seconds"])) in _short.reason
and str(len(_full["price"]) - 4) in _short.reason)
# The other direction, so the check is not a one-sided `len(price) < len(time)`.
_long = load("generic_dayahead",
transport=canned(_edited(_QH, price=_full["price"] + [1.0])))
check("...and a `price` array one entry LONGER is refused too",
_long.kind == "no_data")
# ⚠️ THE CONTROL. Without it, "refuses everything" passes both checks above.
check("...while the untouched body is still a series, so the refusal is the "
"length and not the editing",
load("generic_dayahead",
transport=canned(_edited(_QH))).kind == "series")
_unit = load("generic_dayahead", transport=canned(_edited(_QH,
unit="EUR / kWh")))
check("a feed that changes its unit to EUR/kWh is no_data, not every price "
"silently divided by 1000 - MAX_PLAUSIBLE_EUR_KWH only bounds prices "
"that are too LARGE, so nothing downstream could catch it",
_unit.kind == "no_data")
_nulls = _full["price"][:]
for _i in (3, 17):
_nulls[_i] = None
_nulled = load("generic_dayahead", transport=canned(_edited(_QH,
price=_nulls)))
check(f"a published null is DROPPED, leaving a hole rather than a zero - a "
f"missing quarter is not a free quarter",
_nulled.kind == "series"
and derived(lambda: len(_nulled.points)) == len(_full["price"]) - 2)
check("...and the dropped instants are the ones the feed left null, not the "
"first two",
derived(lambda: sorted(
int(p.start.timestamp()) for p in _nulled.points))
== sorted(t for i, t in enumerate(_full["unix_seconds"])
if i not in (3, 17)))
_allnull = load("generic_dayahead",
transport=canned(_edited(_QH,
price=[None] * len(_full["price"]))))
check("...but a body with a price for nothing at all is no_data, not an empty "
"series", _allnull.kind == "no_data")
# ⚠️ WHICH RULE REFUSED IT, not merely that something did. `_validate_raw()`
# would also refuse an empty list, one layer further out and with a sentence
# that says nothing about the feed - so asserting only `no_data` here would
# grade the same under a parser that had lost this refusal entirely.
check(f"...refused by the COLUMNAR parser, naming what arrived, rather than "
f"reaching the boundary validator as a generic empty list - "
f"{_allnull.reason!r}",
_allnull.kind == "no_data"
and f"{len(_full['price'])} instants" in _allnull.reason
and "a price for none of them" in _allnull.reason)
# ⚠️ THE TWO WARNINGS ARE GRADED, NOT ASSUMED. §11.4 is exactly what makes a
# price defect quiet - a parse failure becomes no_data becomes the static
# profile, correctly and silently - so an unverified diagnostic is the first
# thing here to rot. Both are captured off the shipped logger.
_wbuf = io.StringIO()
_wh = logging.StreamHandler(_wbuf)
logging.getLogger("goodwe.price").addHandler(_wh)
load("generic_dayahead", transport=canned(_edited(_QH, price=_nulls)))
_dep = load("generic_dayahead", transport=canned(_edited(_QH,
deprecated=True)))
logging.getLogger("goodwe.price").removeHandler(_wh)
_warn = _wbuf.getvalue()
_n_null = len([v for v in _nulls if v is None])
check(f"the dropped nulls are reported with a count ({_n_null} of "
f"{len(_nulls)}), so a series with holes in it is visible to an operator "
f"rather than merely short",
f"{_n_null} of {len(_nulls)}" in _warn)
check("...and a feed announcing deprecated=true says so WHILE IT STILL WORKS - "
"the day it stops, §11.4 makes its death look exactly like a quiet one",
"deprecated" in _warn and _dep.kind == "series")
# Which plugins are replayed from real bytes, derived from the registry so a
# sixth supplier cannot be added without this line noticing.
_networked = sorted(pid for pid, cls in PROVIDERS.items()
if cls.capabilities().egress_hosts)
# ⚠️ TARIFF-15 PUT A SECOND PLUGIN ON THIS LIST AND IT IS REPLAYED FROM THE
# SAME CAPTURED BYTES (section 13c), because it reads the same endpoint with
# the same parser. A networked plugin with no capture is what this line exists
# to forbid.
_replayed = ["custom_formula", "generic_dayahead"]
check(f"the networked plugins are {_networked}, and EVERY one of them is "
f"replayed from a captured real body ({_replayed}) - no shipped plugin "
f"reaches a network this file has never seen answer (TARIFF-07)",
_networked == _replayed)
# --------------------------------------------------------------------------- #
print()
print("2c. TARIFF-09 - the RANGE the caller asked for, on the Brussels "
"calendar")
#
# ⚠️ WHAT WAS WRONG, AND WHY NO CHECK IN THIS FILE COULD SEE IT.
# `generic_dayahead.fetch()` accepted `since` and `until` and requested the
# bare `https://api.energy-charts.info/price?bzn=BE`, which answers ONE
# Brussels day. Everything past today was then filled from yesterday's profile
# by FCST-04, flagged provisional, and §8.3 dropped all of it from the trade
# set - so the optimiser never saw tomorrow as firm, at any hour of any day.
# Measured on the merged tree at 2026-09-10T09:00+02:00 over a 48 h horizon:
# 15 firm whole hours and 33 carried, both ends of the only permitted cycle
# inside ONE calendar day; and at 23:00, 1 firm hour and 47 carried, where the
# firm spread no longer clears the round-trip cost at all.
#
# The reason section 2b could not catch it is that section 2b fed the parser
# BYTES and never looked at the REQUEST. `canned()` has always recorded the URL
# it was asked for and nothing asserted on it - so the transport knew the
# plugin was ignoring its arguments and no check ever asked. The checks below
# assert on the URL, and the two-day body they replay is a capture of what that
# URL really answers.
_TD = "energy-charts-be-price-2026-09-09_2026-09-10-twoday.json"
# ⚠️ NON-DEGENERACY FIRST (habit 1): the two-day capture must really cover TWO
# Brussels days and the one-day capture ONE, or "the range reached the feed"
# is graded against two fixtures that are the same shape.
_bdays = {n: sorted({brussels(datetime.fromtimestamp(t, UTC)).date().isoformat()
for t in json.loads(b.decode("utf-8"))["unix_seconds"]})
for n, b in _REAL.items()}
check(f"the captures span the local-day counts this section needs - "
f"{ {n: len(v) for n, v in _bdays.items()} } - or the range check has "
f"nothing to distinguish {_bdays[_TD]} from {_bdays[_QH]}",
len(_bdays[_TD]) == 2 and len(_bdays[_QH]) == 1
and len(_bdays[_H1]) == 1)
_npts = {n: len(json.loads(b.decode("utf-8"))["price"])
for n, b in _REAL.items()}
check(f"...and the two-day body carries 192 quarter-hours where the one-day "
f"body carries 96, which is the counterfactual this card is built on "
f"{_npts}",
len(json.loads(_REAL[_TD].decode("utf-8"))["price"]) == 192
and len(json.loads(_REAL[_QH].decode("utf-8"))["price"]) == 96)
# --- the URL the SHIPPED plugin asks for ----------------------------------- #
def asked_url(since, until, extra=None):
"""The URL `generic_dayahead` really requests for a window. Behaviour,
not source text: it comes back off the injected transport."""
t = canned(_REAL[_TD])
o = opts(price_provider="generic_dayahead", **(extra or {}))
run(load_prices(o, since, until, transport=t))
return nth(t.calls, 0)[0]
# The expectation is built HERE, from the window, by different arithmetic than
# price.py uses: a literal string with the two dates spelled out.
_w0 = datetime.fromisoformat("2026-09-09T09:17:43+02:00")
check(f"a 48 h horizon from 09:17 local asks the feed for the three Brussels "
f"days it touches -> {asked_url(_w0, _w0 + timedelta(hours=48))!r}",
asked_url(_w0, _w0 + timedelta(hours=48))
== "https://api.energy-charts.info/price?bzn=BE"
"&start=2026-09-09&end=2026-09-11")
check("...and the shipped default no longer asks the bare URL that answers a "
"single day",
asked_url(_w0, _w0 + timedelta(hours=48))
!= "https://api.energy-charts.info/price?bzn=BE")
# ⚠️ THE BRUSSELS/UTC BOUNDARY, WHICH IS THE TRAP. `since`/`until` are
# INSTANTS and the API takes DATES. At 22:30 UTC in September Belgium is
# already on the NEXT calendar day, so a range derived from `since.date()` -
# or from `since.astimezone(utc).date()`, which is the same thing - asks for
# the day the controller has just left and drops the day it is standing in.
# This is `_bdate()`'s lesson one layer up: price_history.py MISFILES an hour
# when it gets this wrong; here the hours are never fetched at all.
_edge = datetime.fromisoformat("2026-09-10T22:30:00+00:00")
check(f"the precondition: {_edge.isoformat()} really is a different day in "
f"Brussels ({brussels(_edge).date().isoformat()}) than in UTC "
f"({_edge.date().isoformat()}), or the boundary check grades nothing",
brussels(_edge).date().isoformat() != _edge.date().isoformat())
check(f"...so the range starts on the BRUSSELS day, 2026-09-11, and not on "
f"the UTC day 2026-09-10 -> "
f"{asked_url(_edge, _edge + timedelta(hours=12))!r}",
asked_url(_edge, _edge + timedelta(hours=12))
== "https://api.energy-charts.info/price?bzn=BE"
"&start=2026-09-11&end=2026-09-11")
# ...and the SAME trap on the other end. `until` at 22:30 UTC is already
# 00:30 of the next Brussels day, so a UTC-derived END stops one day short and
# the last hours of the horizon are never fetched. Both ends of the range
# derivation are graded, or a mutation only has to get one of them right.
_edge2_a = datetime.fromisoformat("2026-09-09T12:00:00+00:00")
_edge2_b = datetime.fromisoformat("2026-09-10T22:30:00+00:00")
check(f"the precondition: {_edge2_b.isoformat()} is "
f"{brussels(_edge2_b).date().isoformat()} in Brussels and "
f"{_edge2_b.date().isoformat()} in UTC, so the two answers differ",
brussels(_edge2_b).date().isoformat() != _edge2_b.date().isoformat())
check(f"...so the range ENDS on the Brussels day 2026-09-11, not on the UTC "
f"day 2026-09-10 -> {asked_url(_edge2_a, _edge2_b)!r}",
asked_url(_edge2_a, _edge2_b)
== "https://api.energy-charts.info/price?bzn=BE"
"&start=2026-09-09&end=2026-09-11")
# ⚠️ `until` IS EXCLUSIVE, so local midnight belongs to the day BEFORE it. A
# 24 h horizon from local midnight is ONE day, not two.
_mid = datetime.fromisoformat("2026-09-09T00:00:00+02:00")
check(f"a horizon ending exactly at local midnight asks for one day, because "
f"`until` is exclusive -> {asked_url(_mid, _mid + timedelta(hours=24))!r}",
asked_url(_mid, _mid + timedelta(hours=24))
== "https://api.energy-charts.info/price?bzn=BE"
"&start=2026-09-09&end=2026-09-09")
check(f"...while one second past midnight is two days, so the rule is the "
f"exclusive END and not a fixed subtraction -> "
f"{asked_url(_mid, _mid + timedelta(hours=24, seconds=1))!r}",
asked_url(_mid, _mid + timedelta(hours=24, seconds=1))
== "https://api.energy-charts.info/price?bzn=BE"
"&start=2026-09-09&end=2026-09-10")
check(f"an empty or inverted window collapses to a single day rather than "
f"asking for a backwards range no feed has a defined answer for -> "
f"{asked_url(_mid, _mid - timedelta(hours=5))!r}",
asked_url(_mid, _mid - timedelta(hours=5))
== "https://api.energy-charts.info/price?bzn=BE"
"&start=2026-09-09&end=2026-09-09")
# The helper on its own, over the DST transitions, where a fixed-offset
# assumption and a correct one disagree.
_mar = datetime.fromisoformat("2026-03-29T00:30:00+00:00") # 02:30 CEST
_bdr = derived(lambda: brussels_date_range(_mar, _mar + timedelta(hours=1)))
check(f"brussels_date_range() is right across the spring transition, where "
f"00:30 UTC is already 02:30 local -> {_bdr}",
_bdr == (date(2026, 3, 29), date(2026, 3, 29)))
# --- the two-day body, end to end through the SHIPPED parser --------------- #
_two = load("generic_dayahead", transport=canned(_REAL[_TD]))
check(f"the shipped generic_dayahead parses the real two-day body "
f"{_two.words}", _two.kind == "series")
def _series_days(read):
return derived(lambda: sorted({brussels(p.start).date().isoformat()
for p in read.points}))
_two_days = _series_days(_two)
check(f"...and the series it produces spans TWO Brussels local days "
f"{_two_days}, where the one-day body's spans one "
f"{_series_days(_REPLAY[_QH])}",
_two_days == ["2026-09-09", "2026-09-10"]
and len(_two_days) == 2)
check(f"...one point per published interval, 192 of them, no decimation and "
f"no day dropped ({derived(lambda: len(_two.points))})",
derived(lambda: len(_two.points)) == 192)
# --- the SHORT ANSWER, which is normal and must stay a series -------------- #
#
# ⚠️ ASKING FOR TOMORROW BEFORE THE ~13:00-14:00 CET AUCTION RETURNS FEWER DAYS
# THAN REQUESTED, AND THAT IS NOT AN ERROR. Measured: at 2026-09-10T07:46:55Z
# the request `?bzn=BE&start=2026-09-10&end=2026-09-11` came back BYTE-IDENTICAL
# to the bare-URL capture - one day, 96 quarter-hours. A `no_data` there would
# switch a working site's arbitrage off every morning, so the requirement is
# that the short body stays a `series` of what EXISTS and FCST-04's fill covers
# the rest with flagged, provisional hours. The identity is re-derived from the
# bytes rather than restated.
_prepub = _CAPTURES["files"][_QH]["also_returned_verbatim_by"]
check(f"the pre-publication range request {_prepub['url']!r} returned the "
f"SAME BYTES as the bare default URL - one day where two were asked for",
hashlib.sha256(_REAL[_QH]).hexdigest()
== _CAPTURES["files"][_QH]["sha256"]
and "start=2026-09-10&end=2026-09-11" in _prepub["url"])
check(f"...and the shipped parser answers that short body with a SERIES of "
f"what exists, never no_data - {_REPLAY[_QH].words}",
_REPLAY[_QH].kind == "series"
and derived(lambda: len(_REPLAY[_QH].points)) == 96)
# ⚠️ THE COUNTS ARE TAKEN THROUGH `derived()` AND COMPARED ONLY WHEN BOTH ARE
# NUMBERS. A mutation that turns the short answer into `no_data` makes
# `.points` raise `Absence` by design, and `_Raised < _Raised` is a TypeError -
# which takes the file down with a traceback instead of reporting a FAIL. A
# check whose failure mode is a crash has no name attached (habit 5's shape,
# with an exception in place of a hang).
_n_short = derived(lambda: len(_REPLAY[_QH].points))
_n_two = derived(lambda: len(_two.points))
check(f"...which is strictly fewer points than the two-day body "
f"({_n_short} < {_n_two}), so 'short' is a real property of this "
f"stimulus and not a name for it",
isinstance(_n_short, int) and isinstance(_n_two, int)
and _n_short < _n_two)
# --- price_feed_url: the exact URL, and the cost said out loud ------------- #
#
# ⚠️ THE DECISION. An override is used VERBATIM and this plugin appends
# nothing to it. Merging our `start`/`end` into an operator's query string
# would mean re-serialising a URL they wrote, and `start`/`end` are
# energy-charts' spelling - and this provider also DECLARES a SECOND host,
# `web-api.tp.entsoe.eu`, whose date parameters are not those and are not
# verified here, so appending our vocabulary there yields two ignored
# parameters and the short horizon anyway. The cost is
# that an overriding site silently gets the horizon this card exists to fix,
# so it is NOT silent: a warning names the consequence at fetch time.
_ovr = "https://api.energy-charts.info/price?bzn=BE&somethingelse=1"
check(f"an override is requested EXACTLY as given, with nothing appended -> "
f"{asked_url(_w0, _w0 + timedelta(hours=48), {'price_feed_url': _ovr})!r}",
asked_url(_w0, _w0 + timedelta(hours=48),
{"price_feed_url": _ovr}) == _ovr)
_obuf = io.StringIO()
_oh = logging.StreamHandler(_obuf)
logging.getLogger("goodwe.price").addHandler(_oh)
asked_url(_w0, _w0 + timedelta(hours=48), {"price_feed_url": _ovr})
_ovr_log = _obuf.getvalue()
_obuf2 = io.StringIO()
_oh2 = logging.StreamHandler(_obuf2)
logging.getLogger("goodwe.price").removeHandler(_oh)
logging.getLogger("goodwe.price").addHandler(_oh2)
asked_url(_w0, _w0 + timedelta(hours=48))
_dflt_log = _obuf2.getvalue()
logging.getLogger("goodwe.price").removeHandler(_oh2)
check(f"...and the operator is TOLD what it costs them, at fetch time, naming "
f"the horizon consequence rather than only the substitution",
"price_feed_url" in _ovr_log and _ovr in _ovr_log
and "provisional" in _ovr_log and "2026-09-11" in _ovr_log)
# ⚠️ THE CONTROL. Without it, "warns about everything" passes the line above.
check("...while the DEFAULT path says nothing, so the warning is about the "
"override and not about fetching",
"price_feed_url is set" not in _dflt_log)
# The builder refuses to append to a base that already has a query string,
# which is the "URL nobody intended" this decision is meant to make
# unreachable. Driven, not read off the source.
check(f"dated_feed_url() refuses a base that already carries a query string, "
f"rather than producing a second '?' - "
f"{_reason(lambda: dated_feed_url('https://h/p?a=1', b='2'))!r}",
raises(lambda: dated_feed_url("https://h/p?a=1", b="2"),
SourceUnavailable))
check("...and a clean base still builds, so the refusal is the existing query "
"string and not the builder",
dated_feed_url("https://h/p", b="2") == "https://h/p?b=2")
# --- the plugin contract: who consumes the range, pinned ------------------- #
#
# ⚠️ THIS IS A SOURCE-SHAPE PIN AND IS NAMED AS ONE (habit 4). The claim is
# about the CODE - which plugins reference the range arguments they are handed
# - which is the same kind of claim `code_leaks()` makes, and it is not
# evidence that any feed parses. `generic_dayahead`'s BEHAVIOUR is graded above
# by the URL it really requests.
#
# It exists because `fetch(self, opts, since, until, transport)` promises every
# plugin the range and the promise is NOT enforced. Enforcing it would oblige
# the three plugins whose endpoints nobody has verified (TARIFF-07) to invent a
# date-parameter vocabulary - TARIFF-08's defect moved from the response to the
# request. So the split is pinned instead, and a sixth supplier has to change
# this line rather than inherit the silence.
def _uses_range(cls):
src = dedent(inspect.getsource(cls.fetch))
tree = ast.parse(src)
names = {n.id for n in ast.walk(tree) if isinstance(n, ast.Name)}
return bool(names & {"since", "until"})
_uses = sorted(pid for pid, cls in PROVIDERS.items() if _uses_range(cls))
check(f"the plugins that CONSUME their `since`/`until` are {_uses}; the rest "
f"take the range and ignore it, which is what TARIFF-09 fixed for "
f"generic_dayahead - and after TARIFF-07 and TARIFF-15 the only one left "
f"that does not is the offline plugin, for the honest reason below",
_uses == ["custom_formula", "generic_dayahead"])
check("...and `manual_csv` is honestly among the ones that do not: it reads a "
"whole file and the fill windows it downstream, so a range parameter "
"would have nowhere to go",
"manual_csv" in sorted(set(PROVIDERS) - set(_uses)))
# --------------------------------------------------------------------------- #
print()
print("3. all-in: energy + markup + taxes, added exactly once (AC1, trap 1)")
# The expected figure is computed HERE, from the fixture's energy component and
# the plugin's own declaration, by different arithmetic than assemble() uses.
for pid, read in sorted(READS.items()):
caps = PROVIDERS[pid].capabilities()
_, energy = FIXTURES[pid]
add = (MARKUP if Component.MARKUP in caps.adds else 0.0) + \
(TAXES if Component.TAXES in caps.adds else 0.0)
want = [round(e + add, 6) for e in energy]
got = derived(lambda r=read: [p.eur_per_kwh for p in r.points])
check(f"{pid}: all-in == published {sorted(c.value for c in caps.sources)} "
f"+ framework {sorted(c.value for c in caps.adds) or 'nothing'} "
f"-> {got}",
got == want)
# ⚠️ THE ONE THAT MATTERS. generic_dayahead is the only shipped plugin whose
# feed is wholesale, so it is the only one that gets both components - and a
# feed declaring more must get strictly less. Derived by comparing output on the
# SAME energy figure rather than by restating numbers.
#
# ⚠️ THE MIDDLE RUNG - a feed sourcing {ENERGY, MARKUP}, which gets TAXES only -
# HAS NO SHIPPED DECLARER SINCE TARIFF-07, and is graded through `assemble()`
# with a hand-built declaration instead of being dropped. It is the only rung
# that distinguishes "adds the complement" from "adds everything or nothing",
# so deleting it with the three retail plugins would have left the partial case
# ungraded on the exact function that must not get it wrong.
def added(pid):
return derived(lambda: round(READS[pid].points[0].eur_per_kwh
- FIXTURES[pid][1][0], 6))
def added_for(sources):
"""What assemble() adds to ONE point declared as sourcing `sources`."""
caps = ProviderCapabilities(product=PriceProduct.DAY_AHEAD,
sources=frozenset(sources))
e = RETAIL_EUR_KWH[0]
return derived(lambda: round(
assemble(caps, [RawPoint(T0, e, False)], MARKUP, TAXES)[0].eur_per_kwh
- e, 6))
_generic_add = added("generic_dayahead")
_csv_add = added("manual_csv")
_retail_add = added_for({Component.ENERGY, Component.MARKUP})
check(f"the wholesale feed gets markup+taxes ({_generic_add})",
isinstance(_generic_add, float)
and abs(_generic_add - (MARKUP + TAXES)) < 1e-9)
check(f"a feed that already carries the markup gets taxes ONLY "
f"({_retail_add}), exactly one markup less than the wholesale one",
isinstance(_retail_add, float) and isinstance(_generic_add, float)
and abs(_retail_add - TAXES) < 1e-9
and abs((_generic_add - _retail_add) - MARKUP) < 1e-9)
check(f"an all-in feed gets nothing added ({_csv_add})",
isinstance(_csv_add, float) and abs(_csv_add) < 1e-9)
# Negative prices survive. A framework that clamped them would turn the best
# charging hour of the year into an average one, silently.
_neg = derived(lambda: [p.eur_per_kwh for p in READS["generic_dayahead"].points
if p.eur_per_kwh < 0])
check(f"a negative day-ahead price passes through, all-in {_neg}",
_neg == [round(EPEX_EUR_MWH[1] / 1000 + MARKUP + TAXES, 6)])
# provisional is per point and comes from the feed, not from the series.
#
# ⚠️ `generic_dayahead` IS EXCLUDED FROM THE "MIXED" REQUIREMENT, AND THE
# EXCLUSION IS THE POINT RATHER THAN A GAP. The columnar document the real feed
# publishes has no per-point provisional field - it is a settled auction result
# - so the plugin declares `False` for all of them. Demanding a mixed series
# from it would mean feeding it an invented body carrying an invented flag,
# which is exactly the closed loop TARIFF-08 exists to remove. It is graded on
# what its real body can grade instead, immediately below.
# ⚠️ `custom_formula` IS EXCLUDED FOR THE SAME REASON AND NOT FOR A NEW ONE
# (TARIFF-15): it reads the SAME columnar document, which has no per-point
# flag, and applies arithmetic to it - arithmetic cannot invent a provenance
# the document never carried.
_SETTLED_FEED = {"generic_dayahead", "custom_formula"}
for pid in sorted(set(READS) - _SETTLED_FEED):
read = READS[pid]
flags = derived(lambda r=read: [p.provisional for p in r.points])
check(f"{pid}: provisional is per point and mixed {flags}",
isinstance(flags, list) and any(flags) and not all(flags))
check(f"...so plan_is_provisional is derived from those points",
derived(lambda r=read: plan_is_provisional(r)) is True)
for pid in sorted(_SETTLED_FEED):
_gen_flags = derived(lambda p=pid: [pt.provisional
for pt in READS[p].points])
check(f"{pid}'s settled auction comes out settled at every point "
f"{_gen_flags}",
isinstance(_gen_flags, list)
and len(_gen_flags) == len(EPEX_EUR_MWH)
and not any(_gen_flags))
check(f"...so plan_is_provisional is False for {pid}, derived from the "
f"same points",
derived(lambda p=pid: plan_is_provisional(READS[p])) is False)
# ...and the same function says False when nothing is provisional. Without this
# arm, "always True" would pass every check above.
_settled = PriceRead.series(
[PricePoint(brussels(T0), 0.2, False)], PriceProduct.DAY_AHEAD, "x", T0)
check("a fully settled series is NOT provisional",
plan_is_provisional(_settled) is False)
# --------------------------------------------------------------------------- #
print()
print("4. double-counting the markup is detectable, and unreachable (trap 1)")
class _Rogue(PriceProvider):
"""A plugin that adds the markup itself while declaring it did not.
NOT registered - it exists only here, as the mutation the design is meant to
survive. It is what a plugin author writes when they copy the wholesale
plugin, hardcode their supplier's margin, and forget to update `sources`.
"""
id = "_rogue_not_shipped"
@classmethod
def capabilities(cls):
return ProviderCapabilities(product=PriceProduct.DAY_AHEAD,
sources=frozenset({Component.ENERGY}))
async def fetch(self, o, since, until, transport):
# The margin the author hardcoded, because they could not read the
# configured one - see the FrameworkOption check below.
return [RawPoint(T0, RETAIL_EUR_KWH[0], False)]
# The honest counterpart to _Rogue: the SAME declaration _Rogue makes, on a
# feed that really does publish only energy. No shipped plugin is needed for it
# and none is used - the comparison is between two declarations, not two
# suppliers.
_honest_caps = ProviderCapabilities(
product=PriceProduct.DAY_AHEAD,
sources=frozenset({Component.ENERGY, Component.MARKUP}))
_honest = assemble(_honest_caps, [RawPoint(T0, RETAIL_EUR_KWH[0], False)],
MARKUP, TAXES)[0].eur_per_kwh
_rogue_raw = run(_Rogue().fetch(PluginOptions({}), T0, T0, None))
_rogue = assemble(_Rogue.capabilities(), _rogue_raw, MARKUP, TAXES)[0].eur_per_kwh
_delta = derived(lambda: round(_rogue - _honest, 6))
check(f"a plugin that pre-adds the markup and declares it did not comes out "
f"{_delta} high - exactly one markup, so the defect is DETECTABLE and "
f"attributable rather than merely wrong",
isinstance(_delta, float) and abs(_delta - MARKUP) < 1e-9)
check("...and it is not confusable with 'the taxes were added twice'",
isinstance(_delta, float) and abs(_delta - TAXES) > 1e-9)
# ...and now the structural half: the number is not reachable from a plugin, so
# the honest author cannot write that line even by accident.
_po = PluginOptions({"price_markup_eur_kwh": MARKUP,
"price_taxes_eur_kwh": TAXES,
"export_price_eur_kwh": 0.04,
"price_csv_path": "/x"})
_refused = []
for k in ("price_markup_eur_kwh", "price_taxes_eur_kwh", "export_price_eur_kwh"):
try:
_po.get(k)
except FrameworkOption:
_refused.append(k)
check(f"a plugin cannot read the framework's own options {sorted(_refused)}",
sorted(_refused) == sorted(["price_markup_eur_kwh", "price_taxes_eur_kwh",
"export_price_eur_kwh"]))
check("...and `k in opts` is false for them too, so a probe cannot find them",
not any(k in _po for k in ("price_markup_eur_kwh", "price_taxes_eur_kwh")))
check("...while the options a plugin legitimately needs still come through",
_po.get("price_csv_path") == "/x"
and _po.get("price_timeout_s") == PRICE_OPTION_DEFAULTS["price_timeout_s"])
# The prose and the declaration must agree. A docstring that says "the framework
# adds taxes" over a class declaring it sources them is a 3 c/kWh error with a
# reassuring comment on top.
_doc_drift = []
for pid, cls in sorted(PROVIDERS.items()):
line = next((ln for ln in (cls.__doc__ or "").splitlines()
if ln.strip().startswith("SOURCES:")), "")
named = {w.strip() for w in line.split(":", 1)[-1].split(",") if w.strip()}
declared = {c.value for c in cls.capabilities().sources}
if named != declared:
_doc_drift.append((pid, sorted(named), sorted(declared)))
check(f"every plugin's docstring names exactly the components it declares "
f"({len(PROVIDERS)} checked)", _doc_drift == [])
# The ignored-markup warning: a plugin whose feed is already all-in must SAY it
# is ignoring the operator's numbers rather than dropping them silently.
_buf = io.StringIO()
_h = logging.StreamHandler(_buf)
logging.getLogger("goodwe.price").addHandler(_h)
load_any("manual_csv")
load_any("generic_dayahead")
logging.getLogger("goodwe.price").removeHandler(_h)
_warned = _buf.getvalue()
check("an all-in plugin logs that the configured markup/taxes are ignored",
"double-count" in _warned and "manual_csv" in _warned)
check("...and the wholesale plugin, which really does use them, does not",
"generic_dayahead" not in _warned)
# --------------------------------------------------------------------------- #
print()
print("4b. a plugin cannot reach the markup BY ANY ROUTE (review of PR #16)")
# WHY THIS SECTION EXISTS. The first version of this ticket claimed the markup
# was "not in scope" for a plugin because `PluginOptions` refused it. Review
# wrote a double-counting plugin three more ways, one of them touching nothing
# but `PRICE_OPTION_DEFAULTS` - a public dict in the module every plugin must
# already import for `register`. One closed door in a room with three is not a
# guarantee. Honest wholesale baseline 0.188; each attack lands 0.219.
_banned = set(BANNED_IN_PLUGIN_CODE)
check(f"the banned set covers the framework keys AND the three public routes "
f"review actually used {sorted(_banned)}",
_banned == set(_price.FRAMEWORK_KEYS) | {"PRICE_OPTION_DEFAULTS",
"price_option", "_o"})
# code_leaks() fails OPEN when inspect.getsource() cannot find the .py - the
# right trade for a battery controller, and a silently disarmed guard if nobody
# checks. THIS is the check that makes CI prove the guard is armed.
_srclen = derived(lambda: {pid: len(inspect.getsource(cls))
for pid, cls in PROVIDERS.items()})
check(f"the scan really read every shipped plugin's source, so its fail-open "
f"path is not silently in use {_srclen}",
isinstance(_srclen, dict) and set(_srclen) == set(PROVIDERS)
and _srclen != {} and all(n > 200 for n in _srclen.values()))
_dirty = derived(lambda: {pid: code_leaks(cls)
for pid, cls in PROVIDERS.items()
if code_leaks(cls)})
check(f"...and every shipped plugin is clean {_dirty}", _dirty == {})
# THE DOCSTRING STRIP IS LOAD-BEARING, AND THIS PROVES IT RATHER THAN ASSERTING
# IT. Every plugin is REQUIRED to name its sourced components in prose, and a
# plugin whose feed already carries the markup naturally writes the option key
# out to say the framework must not add it again - which a RAW text scan reads
# as a leak. A check that fires on the documentation the same file mandates is
# a check somebody deletes.
#
# ⚠️ THE STIMULUS IS BUILT HERE NOW, AND SAYING SO IS THE POINT. Until
# TARIFF-07 this asserted on the SHIPPED set, because `engie_dynamic`'s
# docstring happened to spell `price_markup_eur_kwh` in full. It was withdrawn,
# no remaining shipped docstring spells a full key, and grading the shipped set
# would therefore now pass on a tree with the strip DELETED - a check that had
# quietly stopped being one. So the false-positive is constructed, and the
# shipped set is asserted to be clean separately, above.
class _DocumentedPlugin(PriceProvider):
"""A plugin documenting, correctly, that price_markup_eur_kwh is not added.
SOURCES: energy, markup
The framework ADDS: taxes
"""
id = "_documented_not_shipped"
@classmethod
def capabilities(cls):
return ProviderCapabilities(
product=PriceProduct.DAY_AHEAD,
sources=frozenset({Component.ENERGY, Component.MARKUP}))
async def fetch(self, o, since, until, transport):
return [RawPoint(T0, RETAIL_EUR_KWH[0], False)]
_raw_hit = derived(lambda: sorted(
b for b in _banned if b in inspect.getsource(_DocumentedPlugin)))
check(f"a RAW text scan false-positives on a mandated docstring {_raw_hit}, "
f"which is why code_leaks() strips docstrings before searching - and on "
f"the same class code_leaks() reports {code_leaks(_DocumentedPlugin)}",
_raw_hit == ["price_markup_eur_kwh"]
and code_leaks(_DocumentedPlugin) == []
and _dirty == {})
class _AttackPrivateDict(PriceProvider):
"""A1: reaches into PluginOptions' private dict.
SOURCES: energy
The framework ADDS: markup, taxes
"""
id = "_attack_private_dict"
@classmethod
def capabilities(cls):
return ProviderCapabilities(product=PriceProduct.DAY_AHEAD,
sources=frozenset({Component.ENERGY}))
async def fetch(self, o, since, until, transport):
return [RawPoint(T0, 0.10 + o._o["price_markup_eur_kwh"], False)]
class _AttackHelper(PriceProvider):
"""A2: calls a public framework helper with the raw options dict.
SOURCES: energy
The framework ADDS: markup, taxes
"""
id = "_attack_helper"
@classmethod
def capabilities(cls):
return ProviderCapabilities(product=PriceProduct.DAY_AHEAD,
sources=frozenset({Component.ENERGY}))
async def fetch(self, o, since, until, transport):
return [RawPoint(T0, 0.10 + price_option(o._o, "price_markup_eur_kwh"),
False)]
class _AttackPublicConstant(PriceProvider):
"""A2b: touches nothing but a public module constant. THE DECIDING ONE.
SOURCES: energy
The framework ADDS: markup, taxes
"""
id = "_attack_public_constant"
@classmethod
def capabilities(cls):
return ProviderCapabilities(product=PriceProduct.DAY_AHEAD,
sources=frozenset({Component.ENERGY}))
async def fetch(self, o, since, until, transport):
return [RawPoint(T0,
0.10 + PRICE_OPTION_DEFAULTS["price_markup_eur_kwh"],
False)]
_ATTACKS = {"A1 private dict": _AttackPrivateDict,
"A2 public helper": _AttackHelper,
"A2b public constant": _AttackPublicConstant}
for _name, _cls in _ATTACKS.items():
_found = derived(lambda c=_cls: code_leaks(c))
check(f"{_name}: the scan names what it reached for {_found}",
isinstance(_found, list) and _found != [])
check(f"...and register() REFUSES it at import, not at fetch time",
raises(lambda c=_cls: register(c), FrameworkOption))
check(f"...so it never enters the registry", _cls.id not in PROVIDERS)
# THE COST OF THE DEFECT, MEASURED, so the refusal is not grading itself.
_honest_wholesale = derived(lambda: assemble(
PROVIDERS["generic_dayahead"].capabilities(),
[RawPoint(T0, 0.10, False)], MARKUP, TAXES)[0].eur_per_kwh)
_attacked = derived(lambda: assemble(
_AttackPublicConstant.capabilities(),
[RawPoint(T0, 0.10 + MARKUP, False)], MARKUP, TAXES)[0].eur_per_kwh)
check(f"an unrefused attack would bill {_attacked} against an honest "
f"{_honest_wholesale} - one markup high, every hour, silently",
isinstance(_attacked, float) and isinstance(_honest_wholesale, float)
and abs((_attacked - _honest_wholesale) - MARKUP) < 1e-9)
# WHAT THIS DOES *NOT* CLOSE, AND CANNOT. A plugin that declares {ENERGY} while
# its feed is really all-in comes out one markup high while touching no
# framework name at all. No code can check that: the declaration is the
# plugin's unverifiable claim about somebody else's tariff. Stated here so the
# guarantee is read at its true size - the complement is added exactly once,
# GIVEN A CORRECT DECLARATION - and so a reviewer who trusts an absolute
# guarantee does not stop checking. Measured, not asserted:
_misdeclared = derived(lambda: assemble(
ProviderCapabilities(product=PriceProduct.DAY_AHEAD,
sources=frozenset({Component.ENERGY})),
[RawPoint(T0, 0.10 + MARKUP + TAXES, False)], MARKUP, TAXES)[0].eur_per_kwh)
check(f"a MIS-DECLARED all-in feed still doubles ({_misdeclared} vs "
f"{_honest_wholesale}) with no framework name touched - the limit of the "
f"guarantee, stated rather than hidden",
isinstance(_misdeclared, float)
and abs((_misdeclared - _honest_wholesale) - (MARKUP + TAXES)) < 1e-9
and code_leaks(_AttackPrivateDict) != [])
# --------------------------------------------------------------------------- #
print()
print("5. product type is declared, not inferred from the name (AC2, trap 2)")
# ⚠️ NO SHIPPED PLUGIN DECLARES `PriceProduct.REALTIME`, AND THAT IS RECORDED
# HERE RATHER THAN LEFT TO BE REDISCOVERED (TARIFF-07). `eneco_eddy` was its
# sole declarer and it is withdrawn, so `_WIDE`, `Uncertainty.REALTIME_PRODUCT`
# and `WIDEN_FACTOR`'s 2.5 have no shipped caller. They are KEPT - a reviewer
# finding a mechanism with zero callers would reasonably delete it, and the
# first real imbalance product would then have to rebuild the AC2 seam - and
# this pin is what tells that reviewer the emptiness is intentional. See
# DECISIONS/TARIFF-07-withdrawing-three-plugins.md.
#
# ⚠️ IT IS A PIN, NOT A PROHIBITION. If a plugin that really is real-time ever
# ships, this check goes red and the right fix is to EDIT it - and the decision
# record with it - not to delete the seam.
_products = {pid: cls.capabilities().product for pid, cls in PROVIDERS.items()}
_realtime = sorted(p for p, v in _products.items()
if v is PriceProduct.REALTIME)
check(f"no SHIPPED plugin declares real-time/imbalance {_realtime or '[]'} - "
f"§9.2's row 2 is unoccupied since TARIFF-07 withdrew eneco_eddy, and "
f"the widening path below is kept anyway, on purpose",
_realtime == [])
_regimes = {pid: volatility_regime(v) for pid, v in _products.items()}
check(f"...so every shipped plugin is in the NORMAL regime {_regimes}",
set(_regimes.values()) == {"normal"})
# ...and the machinery is still LIVE, graded from a declaration this file makes
# rather than from a plugin. This is the same stimulus AC2 promises a sixth
# supplier: declare REALTIME, get the wide regime, edit nothing in price.py.
_rt_caps = ProviderCapabilities(product=PriceProduct.REALTIME,
sources=frozenset({Component.ENERGY}))
check(f"the widening path still works from a declaration alone "
f"({volatility_regime(_rt_caps.product)}), which is why §9.2 row 2 "
f"having no occupant is not the same as it having no mechanism",
volatility_regime(_rt_caps.product) == "wide"
and volatility_regime(PriceProduct.DAY_AHEAD) == "normal")
check(f"...and it is the PRODUCT that carries it: uncertainty_of() answers "
f"{sorted(u.value for u in uncertainty_of(PriceProduct.REALTIME))} for "
f"real-time and nothing for day-ahead, and widen_factor() turns that "
f"into {widen_factor(uncertainty_of(PriceProduct.REALTIME))}",
uncertainty_of(PriceProduct.REALTIME)
== frozenset({Uncertainty.REALTIME_PRODUCT})
and uncertainty_of(PriceProduct.DAY_AHEAD) == frozenset()
and widen_factor(uncertainty_of(PriceProduct.REALTIME)) == 2.5
and widen_factor(uncertainty_of(PriceProduct.DAY_AHEAD)) == 1.0)
def _accepts_name():
"""Would volatility_regime() take a plugin id? If it would, somebody can
start passing one and AC2 rots without a single check going red."""
try:
volatility_regime("eneco_eddy")
return True
except TypeError:
return False
check("volatility_regime refuses a plugin NAME, which is the AC2 violation "
"written out",
not _accepts_name())
# ⚠️ THE MECHANICAL FORM OF AC2. The framework's own code must contain no
# plugin id at all - if it does, the sixth supplier is an edit to price.py's
# shared machinery rather than a new class. Derived: build the set of ids that
# actually occur, and assert it is empty.
_framework = [_price.assemble, _price.load_prices, _price.volatility_regime,
_price.arbitrage_allowed, _price.plan_is_provisional,
_price.check_egress, _price._validate_raw, _price.PriceRead,
_price._Series, _price._NoData, _price.PluginOptions,
_price.register, _price.price_option]
_src = "\n".join(inspect.getsource(o) for o in _framework)
_leaked = sorted(pid for pid in PROVIDERS if pid in _src)
check(f"no plugin id appears anywhere in the framework's code {_leaked or '[]'}",
_leaked == [])
# ...and the positive half: a plugin defined HERE, in a file price.py has never
# heard of, gets the right regime with no edit anywhere.
@register
class _NewSupplier(PriceProvider):
"""A sixth supplier, invented in the test file. AC2 in one class.
SOURCES: energy
The framework ADDS: markup, taxes
"""
id = "_test_sixth_supplier"
@classmethod
def capabilities(cls):
return ProviderCapabilities(product=PriceProduct.REALTIME,
sources=frozenset({Component.ENERGY}),
egress_hosts=("api.example.invalid",))
async def fetch(self, o, since, until, transport):
body = await transport("https://api.example.invalid/p",
float(o.get("price_timeout_s")))
return _price._points_from_columnar_json(body, expect_unit="EUR / MWh",
scale=0.001,
provisional_default=False)
# ⚠️ THIS CLASS IS ALSO THE ONLY LIVE DECLARER OF `PriceProduct.REALTIME` IN
# THE TREE (TARIFF-07), which makes the widened-threshold path a thing this
# suite exercises end to end - registry, series, regime - and not merely a
# constant section 5 reads. Deleting it would strand the seam twice over.
_sixth = load("_test_sixth_supplier",
transport=canned(columnar_feed(EPEX_EUR_MWH)))
check("a plugin invented outside price.py resolves through the registry",
_sixth.kind == "series" and _sixth.provider == "_test_sixth_supplier")
_sixth_regime = derived(lambda: _sixth.regime)
check(f"...and lands in the wide-threshold regime purely from its declaration "
f"({_sixth_regime})", _sixth_regime == "wide")
check("...and the framework applied its markup and taxes correctly too",
derived(lambda: [p.eur_per_kwh for p in _sixth.points])
== [round(v / 1000 + MARKUP + TAXES, 6) for v in EPEX_EUR_MWH])
del PROVIDERS["_test_sixth_supplier"] # do not leak into later sections
# --------------------------------------------------------------------------- #
print()
print("6. no price data is its own outcome, not an empty list (trap 3, §11.4)")
_none = PriceRead.no_data("the feed timed out")
# ⚠️ THE DISTINCTION THE WHOLE TYPE EXISTS FOR. A day of free electricity and a
# day with no price data are different instructions: arbitrage is POINTLESS
# under the first and FORBIDDEN under the second.
_zero = PriceRead.series([PricePoint(brussels(T0 + timedelta(hours=i)), 0.0,
False) for i in range(3)],
PriceProduct.DAY_AHEAD, "x", T0)
check("a zero-price series and no-price-data are different kinds",
_zero.kind != _none.kind)
check("...arbitrage is allowed under zero prices and forbidden under no data",
arbitrage_allowed(_zero) is True and arbitrage_allowed(_none) is False)
check("...the plan is provisional under no data and settled under zero prices",
plan_is_provisional(_none) is True and plan_is_provisional(_zero) is False)
check("...and the cheapest hour is computable for the zero series",
derived(lambda: min(p.eur_per_kwh for p in _zero.points)) == 0.0)
check("no_data has no .points, so `min(read.points)` cannot invent a free hour",
raises(lambda: _none.points, Absence))
check("...no .product to switch a threshold regime on",
raises(lambda: _none.product, Absence))
check("...no .at(), so an hourly lookup cannot silently succeed",
raises(lambda: _none.at(T0), Absence))
check("...and .get('points', []) - the idiom the whole rework exists to kill - "
"raises rather than returning the default",
raises(lambda: _none.get("points", []), Absence))
check("...and it is not iterable, so `for p in read:` cannot be an empty loop",
raises(lambda: list(_none), Absence))
check("...while its one real field, the reason, is there for an operator",
_none.reason == "the feed timed out")
# M3, INHERITED FROM RIG-01 AND CLOSED HERE. `_NoData.__init__(self,
# reason="no price data")` used to go green: a required field with a DEFAULT is
# the exact idiom this whole type exists to forbid, and it survived because
# every check passed a reason explicitly. It is the identical survivor left
# open on sim/riglock.py's `_Unread.cause` - the shape was borrowed including
# its blind spot, which is what borrowing a shape does if nobody re-runs the
# mutations against the copy.
#
# Checked by SIGNATURE rather than by one call, so any default added to any of
# these constructors later is red without somebody remembering to write a new
# case.
def _defaulted(fn):
"""Parameters of `fn` that carry a default. `self` is skipped."""
try:
ps = list(inspect.signature(fn).parameters.values())
except (TypeError, ValueError) as e: # pragma: no cover
return _Raised(e)
return [q.name for q in ps
if q.name != "self" and q.default is not inspect.Parameter.empty]
_ctors = {"PriceRead.no_data": PriceRead.no_data,
"PriceRead.series": PriceRead.series,
"_NoData.__init__": _price._NoData.__init__,
"_Series.__init__": _price._Series.__init__,
"PriceRead.match": PriceRead.match}
_with_defaults = derived(lambda: {n: d for n, d in
((n, _defaulted(f)) for n, f in _ctors.items())
if d})
check(f"no constructor or handler of an outcome type carries a DEFAULT, so a "
f"reading can never be built out of nothing {_with_defaults}",
_with_defaults == {})
check("...so no_data() with no reason at all is a TypeError at the call site",
raises(lambda: PriceRead.no_data(), TypeError))
check("...and the variant class itself cannot be built bare either",
raises(lambda: _price._NoData(), TypeError))
check("...nor can a series be built without its product or provenance",
raises(lambda: _price._Series([PricePoint(brussels(T0), 0.2, False)]),
TypeError))
# The positive arm, so the four above are not passing because everything raises.
check("...while a fully specified no_data is still perfectly constructible",
PriceRead.no_data("the feed timed out").reason == "the feed timed out")
# `match` must also refuse POSITIONAL handlers - keyword-only is what makes a
# forgotten arm a TypeError rather than a silently swapped one.
check("match() refuses positional handlers, so the two arms cannot be swapped",
raises(lambda: _none.match(lambda r: 1, lambda r: 2), TypeError))
check("an EMPTY series cannot be constructed at all, so 'no prices' can never "
"wear the shape of an answer",
raises(lambda: PriceRead.series([], PriceProduct.DAY_AHEAD, "x", T0),
ValueError))
check("a series built with a plugin NAME instead of a product enum is refused",
raises(lambda: PriceRead.series([PricePoint(brussels(T0), 0.2, False)],
"eneco_eddy", "x", T0), ValueError))
check("no_data with a blank reason is refused - the reason is its only field",
raises(lambda: PriceRead.no_data(""), ValueError))
def _forgot():
return _none.match(series=lambda r: r.points) # no_data omitted
check("a caller who forgets the no_data arm gets a TypeError at the call site, "
"not a fall-through", raises(_forgot, TypeError))
# .at() outside the series raises rather than returning a neighbour or None.
_s = READS["generic_dayahead"]
check("series.at() inside the series returns the covering point",
derived(lambda: _s.at(_s.points[0].start + timedelta(minutes=59))
is _s.points[0]) is True)
check("...and raises outside it rather than returning None to be `or`-ed away",
derived(lambda: (
raises(lambda: _s.at(_s.points[-1].start + timedelta(days=2)),
Absence)
and _s.covers(_s.points[0].start)
and not _s.covers(_s.points[-1].start + timedelta(days=2)))) is True)
# Every failure a site can have must come back as no_data, never as a traceback
# into the tick. Derived: drive each failure and collect the kinds.
async def _boom(url, timeout_s):
raise OSError("connection reset by peer")
_failures = {
"the source is unreachable": load("generic_dayahead", transport=_boom),
"the body is not JSON": load("generic_dayahead", transport=canned(b"<html>")),
"the feed is empty": load("generic_dayahead",
transport=canned(columnar_feed([]))),
# ⚠️ THE ROW SHAPE IS A FAILURE FOR THIS PLUGIN, NOT A SECOND DIALECT. If
# `_points_from_columnar_json()` ever grows a sniff for `data`, this case
# goes from no_data to a series and TARIFF-08's scope rule is broken by a
# check rather than by a review.
"the body is the OTHER shape": load(
"generic_dayahead",
transport=canned(row_shape_body("price_eur_mwh", EPEX_EUR_MWH))),
"the provider id is unknown": load("no_such_supplier"),
"the provider id is RETIRED": load("eneco_eddy"),
"no provider is configured": load("off"),
"the csv is missing": load("manual_csv",
{"price_csv_path": "/no/such/file.csv"},
transport=refuse_transport),
}
_not_nodata = sorted(k for k, r in _failures.items() if r.kind != "no_data")
check(f"every site-level failure returns no_data instead of raising "
f"({len(_failures)} cases)", _not_nodata == [])
_no_reason = derived(lambda: sorted(k for k, r in _failures.items()
if len(r.reason) < 10))
check(f"...each with a reason an operator can act on {_no_reason}",
_no_reason == [])
check("...and none of them is mistakable for a series",
derived(lambda: all(not arbitrage_allowed(r)
for r in _failures.values())) is True)
# ⚠️ The one thing that must NOT degrade to no_data: a plugin reaching for the
# markup. That is a code defect, and swallowing it would hide exactly what
# trap 1 exists to catch.
class _Thief(PriceProvider):
id = "_thief"
@classmethod
def capabilities(cls):
return ProviderCapabilities(product=PriceProduct.DAY_AHEAD,
sources=frozenset({Component.ENERGY}))
async def fetch(self, o, since, until, transport):
return [RawPoint(T0, 0.1 + o.get("price_markup_eur_kwh"), False)]
PROVIDERS["_thief"] = _Thief
check("a plugin that reaches for the markup raises rather than degrading to "
"no_data - that is a bug, not a site condition",
raises(lambda: load("_thief"), FrameworkOption))
del PROVIDERS["_thief"]
# --------------------------------------------------------------------------- #
print()
print("7. manual_csv works fully offline (AC3)")
_had_aiohttp = "aiohttp" in sys.modules
_csv_read = load("manual_csv", {"price_csv_path": CSV_PATH},
transport=refuse_transport)
_csv_prices = derived(lambda: [p.eur_per_kwh for p in _csv_read.points])
check("the offline plugin produced a series without touching the transport - "
"the transport raises if called at all",
_csv_read.kind == "series" and _csv_prices == [0.1834, -0.021, 0.3115])
check(f"...it is all-in already, so the framework added nothing {_csv_prices}",
_csv_prices == [round(v, 6) for v in
[float(ln.split(",")[1]) for ln in
CSV_BODY.strip().splitlines()[1:]]])
check("...including the negative hour",
isinstance(_csv_prices, list) and any(v < 0 for v in _csv_prices))
check("...and the per-point provisional flag came from the file's third column",
derived(lambda: [p.provisional for p in _csv_read.points])
== [ln.split(",")[2] == "true"
for ln in CSV_BODY.strip().splitlines()[1:]])
check("...and no HTTP stack was imported to do it"
if not _had_aiohttp else
"...(aiohttp was already imported before this file ran; not a finding)",
_had_aiohttp or "aiohttp" not in sys.modules)
check("manual_csv declares no egress hosts at all",
PROVIDERS["manual_csv"].capabilities().egress_hosts == ())
_ManualCsv = PROVIDERS["manual_csv"]
check("a CSV with no header is refused rather than parsed as data",
raises(lambda: _ManualCsv.parse("2026-01-15T00:00:00+01:00,0.18\n"),
SourceUnavailable))
check("a CSV start with no offset is refused, naming the fix",
raises(lambda: _ManualCsv.parse("start,eur_per_kwh\n2026-01-15 00:00,0.18\n"),
SourceUnavailable))
check("...and the two-column form (no provisional) is accepted, defaulting to "
"settled",
[p.provisional for p in
_ManualCsv.parse("start,eur_per_kwh\n2026-01-15T00:00:00+01:00,0.18\n")]
== [False])
# --------------------------------------------------------------------------- #
print()
print("8. the two 2026 DST transitions, where a 24-slot array is wrong")
# ⚠️ RUN ON THE 23- AND 25-HOUR DAYS ON PURPOSE. On any normal day a fixed
# 24-slot implementation, a naive-local-hour key and a correct one all agree
# exactly, so a check on 15 January grades nothing about DST.
check("the 2026 transitions are where this repo expects them (last Sundays)",
_price._last_sunday(2026, 3) == 29 and _price._last_sunday(2026, 10) == 25)
def local_day(day_start_utc, n):
return [brussels(day_start_utc + timedelta(hours=i)) for i in range(n)]
# Spring forward: local day 2026-03-29 runs 2026-03-28T23:00Z .. 2026-03-29T22:00Z
_spring = local_day(datetime(2026, 3, 28, 23, tzinfo=UTC), 23)
_spring_end = brussels(datetime(2026, 3, 29, 22, tzinfo=UTC))
check(f"the spring day has 23 hours, ending exactly at the next midnight "
f"({_spring[0].isoformat()} .. {_spring_end.isoformat()})",
_spring[0].hour == 0 and _spring[0].day == 29
and _spring_end.hour == 0 and _spring_end.day == 30)
check(f"...crossing exactly one offset change "
f"{sorted(str(o) for o in {p.utcoffset() for p in _spring})}",
{p.utcoffset() for p in _spring} == {timedelta(hours=1),
timedelta(hours=2)})
check("...and 02:00 local never happens that day, which a 24-slot array would "
"have invented", 2 not in {p.hour for p in _spring})
# Fall back: local day 2026-10-25 runs 2026-10-24T22:00Z .. 2026-10-25T22:00Z
_fall = local_day(datetime(2026, 10, 24, 22, tzinfo=UTC), 25)
_fall_end = brussels(datetime(2026, 10, 25, 23, tzinfo=UTC))
check(f"the autumn day has 25 hours ({_fall[0].isoformat()} .. "
f"{_fall_end.isoformat()})",
_fall[0].hour == 0 and _fall[0].day == 25
and _fall_end.hour == 0 and _fall_end.day == 26)
_wall = [p.replace(tzinfo=None) for p in _fall]
_dups = len(_wall) - len(set(_wall))
check(f"...and exactly one local wall-clock hour occurs TWICE ({_dups}), which "
f"a naive-local-hour key would collide into one mispriced hour",
_dups == 1)
check("...distinguished by their offsets, which is why the canonical format "
"carries one",
len({p.isoformat() for p in _fall}) == 25)
# A normal day, stated as the control: without it, "23" and "25" prove nothing
# about whether the code counts at all.
_normal = local_day(datetime(2026, 1, 14, 23, tzinfo=UTC), 24)
check("...while an ordinary January day has 24 hours and one offset",
len({p.replace(tzinfo=None) for p in _normal}) == 24
and len({p.utcoffset() for p in _normal}) == 1)
check("brussels() refuses a naive datetime rather than assuming an offset",
raises(lambda: brussels(datetime(2026, 1, 15, 0, 0)), ValueError))
check("the offset boundary is 01:00 UTC, not local midnight",
brussels_offset(datetime(2026, 3, 29, 0, 59, tzinfo=UTC)) ==
timezone(timedelta(hours=1))
and brussels_offset(datetime(2026, 3, 29, 1, 0, tzinfo=UTC)) ==
timezone(timedelta(hours=2)))
# A feed that publishes the duplicated hour must survive it. The 25-hour day is
# 25 strictly increasing instants; a plugin keyed on local hours would emit 24
# and be rejected by the boundary validator instead of silently mispricing.
_dst_feed = columnar_feed([50.0 + i for i in range(25)],
start=datetime(2026, 10, 24, 22, tzinfo=UTC))
_dst_read = load("generic_dayahead", transport=canned(_dst_feed))
_dst_n = derived(lambda: len(_dst_read.points))
check(f"a 25-point autumn feed is accepted whole ({_dst_n} points)",
_dst_read.kind == "series" and _dst_n == 25)
check("...and its canonical starts are 25 distinct strings",
derived(lambda: len({p["start"]
for p in _dst_read.canonical()["prices"]})) == 25)
# --------------------------------------------------------------------------- #
print()
print("9. the boundary rejects what cannot be a price, and only that")
# M5. Deleting the `Component.ENERGY not in sources` guard used to go green,
# and the consequence is not cosmetic: `adds` becomes {ENERGY, TAXES},
# `assemble()` never adds an energy component it thinks it is adding, and the
# emitted all-in price silently omits the energy it is supposed to be pricing.
check("capabilities() refuses a feed that declares no ENERGY component",
raises(lambda: ProviderCapabilities(
product=PriceProduct.DAY_AHEAD,
sources=frozenset({Component.MARKUP})), ValueError))
check("...including a completely empty sources set",
raises(lambda: ProviderCapabilities(product=PriceProduct.DAY_AHEAD,
sources=frozenset()), ValueError))
check("...and an unknown component is refused rather than ignored",
raises(lambda: ProviderCapabilities(
product=PriceProduct.DAY_AHEAD,
sources=frozenset({Component.ENERGY, "grid_fees"})), ValueError))
check("...and a product given as a string is refused at the declaration too",
raises(lambda: ProviderCapabilities(product="dynamic_dayahead",
sources=frozenset({Component.ENERGY})),
ValueError))
# THE CONSEQUENCE, derived across every shipped plugin rather than asserted on
# one: the framework must never be in a position to "add" the energy component,
# because there is nothing to add it from.
_adds_energy = derived(lambda: sorted(
pid for pid, cls in PROVIDERS.items()
if Component.ENERGY in cls.capabilities().adds))
check(f"so no shipped plugin ever asks the framework to invent an energy "
f"price {_adds_energy}", _adds_energy == [])
_caps = PROVIDERS["generic_dayahead"].capabilities()
def boundary(points):
return raises(lambda: _price._validate_raw(points, _caps), SourceUnavailable)
check("NaN is refused", boundary([RawPoint(T0, float("nan"), False)]))
check("infinity is refused", boundary([RawPoint(T0, float("inf"), False)]))
check(f"a EUR/MWh figure read as EUR/kWh is refused (>{MAX_PLAUSIBLE_EUR_KWH})",
boundary([RawPoint(T0, 61.4, False)]))
check("a naive start is refused",
boundary([RawPoint(datetime(2026, 1, 15), 0.1, False)]))
check("duplicate starts are refused - the DST collision, caught",
boundary([RawPoint(T0, 0.1, False), RawPoint(T0, 0.2, False)]))
check("out-of-order starts are refused",
boundary([RawPoint(T0 + timedelta(hours=1), 0.1, False),
RawPoint(T0, 0.2, False)]))
check("a plugin returning something that is not a RawPoint is refused",
boundary([PricePoint(brussels(T0), 0.1, False)]))
check("an empty list is refused", boundary([]))
# ...and the negative-price control, so the rules above are not just "reject
# everything". A validator that refused this would cost money on a windy Sunday.
check("a NEGATIVE price at a plausible magnitude is accepted",
not boundary([RawPoint(T0, -0.35, False)]))
check("...and so is a large-but-plausible one at the limit",
not boundary([RawPoint(T0, MAX_PLAUSIBLE_EUR_KWH - 0.01, False)]))
# --------------------------------------------------------------------------- #
print()
print("10. egress is declared by the plugin and enforced (§16)")
_g = PROVIDERS["generic_dayahead"].capabilities()
check("a declared https host is allowed",
check_egress("https://api.energy-charts.info/price?bzn=BE", _g)
== "api.energy-charts.info")
check("an undeclared host is refused even over TLS",
raises(lambda: check_egress("https://evil.example/p", _g),
SourceUnavailable))
# M7, AND THE ASSERTION IS THE FIX, NOT ONLY THE PARSER. Deleting the TLS rule
# used to leave this check GREEN: the old parser stripped a fixed
# len("https://") from the front, so `http://api.energy-charts.info/p` became
# the host `pi.energy-charts.info`, which failed the ALLOW-LIST instead. The
# feed was refused, the check named "plain http is refused" passed, and the
# rule it was named for could be deleted with no consequence - an assertion
# that does not test what it names, inside a security check.
#
# Three checks now, and the first two are what make the third able to fail:
# prove the host is declared over https, prove it parses identically over
# http, and only then assert that http is refused AND that the reason is TLS.
_tls_url = "http://api.energy-charts.info/p"
check("the same host over https is allowed, so it IS on the allow-list",
derived(lambda: check_egress("https://api.energy-charts.info/p", _g))
== "api.energy-charts.info")
check("...and the http form parses to that same declared host, so nothing but "
"the TLS rule can refuse it",
derived(lambda: _tls_url.partition("://")[2].split("/", 1)[0])
== "api.energy-charts.info")
_tls_reason = derived(lambda: _reason(lambda: check_egress(_tls_url, _g)))
check(f"plain http is refused, and refused BY THE TLS RULE - {_tls_reason!r}",
isinstance(_tls_reason, str) and "non-TLS" in _tls_reason)
check("a scheme that is neither is refused the same way, by TLS not by host",
"non-TLS" in str(derived(lambda: _reason(
lambda: check_egress("ftp://api.energy-charts.info/p", _g)))))
# ⚠️ M7b, WHICH I FOUND IN MY OWN FIX FOR M7 AND WHICH IS THE SAME DEFECT ONE
# LAYER DOWN. This check used to read `raises(..., SourceUnavailable)` and
# survived deleting the `if not sep` guard: with no guard,
# "api.energy-charts.info/p".partition("://") leaves scheme = the WHOLE string,
# so the TLS rule refuses it and the check named for the scheme guard passes.
# Refused either way - but the diagnostic an operator gets degrades from "not an
# absolute URL" to "uses 'api.energy-charts.info/p'" as a scheme, which is
# gibberish. Assert the reason, like M7 above.
_noscheme = derived(lambda: _reason(
lambda: check_egress("api.energy-charts.info/p", _g)))
check(f"a URL with no scheme is refused BY THE SCHEME GUARD, with a diagnostic "
f"that names the real problem - {_noscheme!r}",
isinstance(_noscheme, str) and "not an absolute URL" in _noscheme)
check("a userinfo prefix cannot smuggle a declared host past the check",
raises(lambda: check_egress(
"https://api.energy-charts.info@evil.example/p", _g),
SourceUnavailable))
check("an operator-supplied URL to another supplier's host is refused",
load("generic_dayahead",
{"price_feed_url": "https://api.eneco.be/eddy/prices"},
transport=canned(columnar_feed(EPEX_EUR_MWH))
).kind == "no_data")
_declared = {pid: cls.capabilities().egress_hosts
for pid, cls in PROVIDERS.items()}
check(f"every networked plugin declares at least one host, and manual_csv "
f"declares none {_declared}",
all(bool(h) for p, h in _declared.items() if p != "manual_csv")
and _declared["manual_csv"] == ())
check("the timeout the plugin passes to the transport is the configured one, "
"not a hardcoded default",
(lambda t: (load("generic_dayahead", {"price_timeout_s": 7},
transport=t), nth(t.calls, 0)[1] == 7.0)[1])(
canned(columnar_feed(EPEX_EUR_MWH))))
# --------------------------------------------------------------------------- #
print()
print("11. injection price is a separate scalar (AC4, §9.3)")
check(f"the shipped default is config.yaml's "
f"{PRICE_OPTION_DEFAULTS['export_price_eur_kwh']}",
export_price_eur_kwh({}) == PRICE_OPTION_DEFAULTS["export_price_eur_kwh"])
check("an operator's value wins over it",
export_price_eur_kwh({"export_price_eur_kwh": 0.0}) == 0.0)
check("...including a negative one - Belgian injection has gone below zero",
export_price_eur_kwh({"export_price_eur_kwh": -0.02}) == -0.02)
check("it is NOT folded into the consumption series any plugin emits",
derived(lambda: all(export_price_eur_kwh({}) not in
[p.eur_per_kwh for p in r.points]
for r in READS.values())) is True)
check("...and no plugin can reach it to fold it in",
raises(lambda: PluginOptions({}).get("export_price_eur_kwh"),
FrameworkOption))
# --------------------------------------------------------------------------- #
print()
print("12. the startup gate (validate.py rule 7)")
def vbase(**over):
o = {"setpoint_entity": "number.goodwe_setpoint", "max_w": 2000,
"soc_entity": "sensor.soc", "batt_entity": "sensor.batt",
"meter_source": "off",
"maintenance_charge_w": 2000, "maintenance_discharge_w": 2000}
o.update(over)
return o
def why(problems, needle):
return any(needle in p for p in problems)
check("the shipped price defaults start (price_provider is 'off')",
validate_startup_config(vbase(**{k: v for k, v in
PRICE_OPTION_DEFAULTS.items()}),
-5000, 5000) == [])
check("...and so does a configured provider on its own",
validate_startup_config(vbase(price_provider="generic_dayahead"),
-5000, 5000) == [])
check("an unknown provider id is refused rather than defaulted to 'off'",
why(validate_startup_config(vbase(price_provider="nordpool"), -5000, 5000),
"which does not exist"))
# ⚠️ TARIFF-07: A RETIRED ID IS REFUSED BY A DIFFERENT MESSAGE, AND EVERY CHECK
# HERE GRADES THE REASON RATHER THAN THE VERDICT. The failure this section
# exists to catch is NOT "a retired id starts" - it never will, because it is
# absent from PROVIDERS and the unknown-id branch would catch it. It is the
# retired branch COLLAPSING INTO the unknown-id one: same refusal, same exit
# code, and an operator told they mistyped a name they chose from a dropdown
# this add-on offered them. The two messages are asserted to DIFFER and each
# needle is asserted to be absent from the other, which is MUTATION.md's
# "grade the reason, not the verdict" applied to two rungs that answer one
# stimulus.
_unknown = validate_startup_config(vbase(price_provider="nordpool"),
-5000, 5000)
_retired = {pid: validate_startup_config(vbase(price_provider=pid),
-5000, 5000)
for pid in sorted(RETIRED_PROVIDERS)}
check(f"every retired id is refused at startup {sorted(_retired)}",
len(_retired) == 3 and all(len(v) == 1 for v in _retired.values()))
check("...and NOT by the unknown-id message, which would tell an operator they "
"mistyped an id this add-on offered them and accepted for months",
all("which does not exist" not in v[0] for v in _retired.values())
and "was withdrawn" not in _unknown[0])
check("...and each message NAMES ITS OWN ID, so the operator can tell it is "
"about their configuration and not a generic notice",
all(repr(pid) in v[0] for pid, v in _retired.items()))
# UX-08 2c: the endpoints that never existed are in DOCS.md under "Energy price
# providers"; the refusal keeps what the operator acts on.
check("...and (a) says the plugin never fetched a price",
all("never fetched a price" in v[0] for v in _retired.values()))
check("...and (b) says what the site has ACTUALLY been running instead - the "
"half §11.4 made invisible",
all("This site ran on" in v[0] for v in _retired.values()))
check("...and (c) says what to do, naming a live plugin",
all("generic_dayahead" in v[0] and "manual_csv" in v[0]
for v in _retired.values()))
# ⚠️ AND THE SENTENCE CHANGES WITH THE SITE. "You have been on the static
# profile" is FALSE where no profile was ever set, and that site is in a WORSE
# state - no tariff at all - so the message must not flatten the two. Graded by
# asserting the two renderings DIFFER, because a check that only looked for
# "static_profile" would pass on a message that says the same thing to both.
_PROF = ('{"periods": [{"from":"00:00","to":"24:00","days":"all",'
'"eur_per_kwh":0.31}]}')
_with_profile = validate_startup_config(
vbase(price_provider="eneco_eddy", static_profile=_PROF), -5000, 5000)[0]
_without = _retired["eneco_eddy"][0]
check("a retired id on a site WITH a static profile is told it has been "
"running that profile",
"ran on the “Fixed or day/night tariff” (static_profile)"
in _with_profile)
check("...and a site WITHOUT one is told it has been running nothing at all, "
"which is a different and worse state",
"ran on no tariff at all" in _without
and "(static_profile)" not in _without)
check("...so the two renderings are not the same sentence",
_with_profile != _without)
# ⚠️ THE HA OVERLAP, DECIDED. The two entities keep working alone; combining
# them with a provider is what is refused, because the entities win silently.
_p = validate_startup_config(
vbase(price_provider="generic_dayahead", price_now_entity="sensor.now",
price_avg_entity="sensor.avg"), -5000, 5000)
check("a provider AND the Home Assistant price entities is refused - two price "
"sources on one site", len(_p) == 1)
check("...and the message says which one actually wins",
why(_p, "would win over it"))
# TARIFF-03 added a second source with the same silent-loser shape.
_ps = validate_startup_config(
vbase(static_profile='{"periods": [{"from":"00:00","to":"24:00",'
'"days":"all","eur_per_kwh":0.31}]}',
price_now_entity="sensor.now", price_avg_entity="sensor.avg"),
-5000, 5000)
check("a static_profile AND the Home Assistant price entities is refused too - "
"main.py reads one of them for `cheap` and the loser is silent",
len(_ps) == 1 and why(_ps, "(price_now_entity) and “Average price entity” (price_avg_entity) would win"))
check("the Home Assistant entities alone still start, so nothing is taken away "
"from a live install",
validate_startup_config(vbase(price_now_entity="sensor.now",
price_avg_entity="sensor.avg"),
-5000, 5000) == [])
# ⚠️ THIS TRIPWIRE FIRED, AND THIS IS WHAT IT LOOKS LIKE AFTERWARDS.
#
# TARIFF-02 left a check here asserting that NOTHING in app/ consumed
# `load_prices()`, because rule 7's stated reason - "the supplier you configured
# would decide nothing" - was true only for as long as that held. The
# instruction attached to it was: when this goes red, update the message in
# validate.py in the same commit, do not delete the check.
#
# TARIFF-03 landed `app/static_tariff.py`, whose `resolve_tariff()` consumes
# `load_prices()` and turns a series into a tariff plan, a cheap-window flag and
# an arbitrage verdict. The check went red, naming that file, and validate.py's
# message was rewritten in the same commit to say what is true now instead. So
# the tripwire is KEPT and RE-AIMED rather than removed, at the next thing that
# can go quietly wrong:
#
# * the consumer set is PINNED. A second consumer appearing - OPT-04 wiring a
# strategy in, most likely - goes red again, and whoever does that has to
# re-read rule 7's message against what their change makes true.
# * the EXPIRED PHRASE is asserted GONE. If the old wording is ever restored
# by a bad merge, this says so.
# * and the message has to keep naming its consumer, so it cannot decay into
# a refusal with no stated reason at all.
_APP = os.path.join(os.path.dirname(os.path.abspath(__file__)), "app")
# ⚠️ AST, NOT A SUBSTRING, AND THE FIRST DRAFT PROVED WHY. A text scan for
# "load_prices" matched the COMMENT in validate.py that explains this very
# tripwire, so the check went red against its own documentation - the same
# false-positive class as the plugin docstrings in section 4b, hit twice in one
# ticket. Comments and docstrings are not in an AST, so a name/import walk sees
# code and only code.
def _imports_or_calls(path, name):
try:
tree = ast.parse(open(path, encoding="utf-8").read())
except (OSError, SyntaxError):
return False
for node in ast.walk(tree):
if isinstance(node, ast.ImportFrom) and any(a.name == name
for a in node.names):
return True
if isinstance(node, ast.Name) and node.id == name:
return True
if isinstance(node, ast.Attribute) and node.attr == name:
return True
return False
_consumers = derived(lambda: sorted(
f for f in os.listdir(_APP)
if f.endswith(".py") and f != "price.py"
and _imports_or_calls(os.path.join(_APP, f), "load_prices")))
_EXPECTED_CONSUMERS = ["static_tariff.py"]
check(f"load_prices() is consumed by exactly the modules rule 7's message "
f"names {_consumers}. When a SECOND one appears this goes red, and the "
f"fix is to re-read that message against what the new consumer makes "
f"true - not to widen this list without doing so",
_consumers == _EXPECTED_CONSUMERS)
_RULE7 = derived(lambda: open(os.path.join(_APP, "validate.py"),
encoding="utf-8").read())
check("...and the phrase the tripwire expired - 'would decide nothing' about "
"the provider - is GONE from validate.py, so a bad merge that restores "
"the old reason is red rather than quiet",
isinstance(_RULE7, str)
and "supplier you configured would decide nothing" not in _RULE7)
check("...and the refusal still names the consumer that exists, so it cannot "
"decay into a rule with no stated reason",
isinstance(_RULE7, str) and "static_tariff.py" in _RULE7)
# ...and the other half: the registry IS on the startup path, which is what
# makes an unwired seam anchored rather than dead code.
check("...while validate.py does import the registry, so the seam is exercised "
"on every boot",
derived(lambda: "PROVIDERS" in open(
os.path.join(_APP, "validate.py"), encoding="utf-8").read()) is True)
check("manual_csv with no path is refused - the offline fallback with no file "
"is §11.4 reached by accident",
why(validate_startup_config(vbase(price_provider="manual_csv",
price_csv_path=""), -5000, 5000),
"price_csv_path"))
check("...and with a path it starts",
validate_startup_config(vbase(price_provider="manual_csv",
price_csv_path=CSV_PATH),
-5000, 5000) == [])
# ⚠️ price_provider 'off' must NEVER become a refusal: §9.2 puts a large share
# of the installed base on a fixed or static tariff, and §11.4 makes no-price
# data a supported state.
check("'off' is not refused, on any of the other configurations",
all(validate_startup_config(vbase(price_provider="off", **extra),
-5000, 5000) == []
for extra in ({}, {"price_now_entity": "sensor.now",
"price_avg_entity": "sensor.avg"},
{"price_csv_path": ""})))
# --------------------------------------------------------------------------- #
print()
print("13. TARIFF-10 - a configured provider is FETCHED, and trading is still "
"its own switch")
# WHAT SHIPPED, AND WHY THIS SECTION IS NOT A CHECK ON AN `if`. `amain()` read
#
# 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()`, and `arbitrage_enabled` ships FALSE.
# So on a shipped install with a provider configured NOTHING was fetched, no
# plan was resolved, /data/price_history.json was never created, the *Price
# history* row read `empty` for the life of the install, and TARIFF-09's
# short-horizon warning could not fire because no fetch happened to warn about.
# Measured over a 40 h two-instance soak: zero feed requests and
# `present: false` across all 802 samples.
#
# ⚠️ A CHECK THAT PINS THE `if` WOULD NOT HAVE CAUGHT IT, because the `if` was
# EXACTLY WHAT SOMEBODY MEANT TO WRITE. What was wrong is the SET OF
# CONFIGURATIONS FROM WHICH A FETCH CAN BE REACHED, so that is what is graded:
# the guards in `amain()` 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 can arrive at `resolve_tariff` (wherever it has been
# moved to), and then the fetch is DRIVEN and the requests counted, because a
# reachability graph is an argument and a counted request is an observation.
# -- the predicate the gate is built on, before anything is wired to it -------
# ⚠️ ONE SPELLING, AND IT NOW HAS THREE READERS. `str(price_option(opts,
# "price_provider") or "off").strip()` was written out TWICE - in
# `load_prices()` and in `static_tariff.resolve_tariff()` - and main.py's
# startup gate would have been a third. "Is a provider configured" is now a
# DECISION rather than a detail, because it is what starts a background task,
# so the three readers have to agree about the edge cases and `provider_id()`
# is the one place they can. (`validate.py` keeps its own reader on purpose: it
# validates the RAW options blob and must not route through the defaults.)
check("`provider_configured()` is False on the shipped default, on an explicit "
"'off', and on a key that is absent, empty or None - `price_option()` "
"maps the last three onto the default and none of them may read as a "
"configured feed",
not any(_price.provider_configured(o) for o in
({}, {"price_provider": "off"}, {"price_provider": ""},
{"price_provider": None})))
check("...and True on each shipped plugin id, so it is a predicate and not a "
f"constant {sorted(PROVIDERS)}",
all(_price.provider_configured({"price_provider": pid})
for pid in PROVIDERS))
# ⚠️ WHITESPACE IS THE EDGE THE `.strip()` IS FOR, AND NOTHING GRADED IT. 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 at all - and dropping the `.strip()` left all four price suites green
# (measured, -8 B) until this pair was written.
check("...and a padded id is normalised on both sides of the decision: "
"' off ' is still off, and ' generic_dayahead ' is still that plugin",
_price.provider_id({"price_provider": " off "}) == "off"
and not _price.provider_configured({"price_provider": " off "})
and _price.provider_id({"price_provider": " generic_dayahead "})
== "generic_dayahead"
and _price.provider_configured(
{"price_provider": " generic_dayahead "}))
check("...and `load_prices()` answers `no_data` for exactly the dicts "
"`provider_configured()` calls unconfigured, so the startup gate and the "
"fetch cannot disagree about what 'a provider is set' means",
all((run(load_prices(o, datetime(2026, 1, 1, tzinfo=UTC),
datetime(2026, 1, 2, tzinfo=UTC),
transport=None)).kind == "no_data")
== (not _price.provider_configured(o))
for o in ({}, {"price_provider": "off"},
{"price_provider": " off "},
{"price_provider": "manual_csv",
"price_csv_path": CSV_PATH})))
def _coroutine_name(node):
"""`controller.run_tariff_refresh()` -> `run_tariff_refresh`."""
if isinstance(node, ast.Call):
if isinstance(node.func, ast.Attribute):
return node.func.attr
if isinstance(node.func, ast.Name):
return node.func.id
return ast.unparse(node)
def _task_sites(node, guards=()):
"""Every `create_task(...)` under `node`, with the `if`s that guard it.
The guard is kept as SOURCE and evaluated later against an options dict, so
this reports what the tree says rather than what any one ticket expected to
find there. An `else` branch is recorded as `not (<test>)`.
"""
out = []
for child in ast.iter_child_nodes(node):
if isinstance(child, ast.If):
test = ast.unparse(child.test)
for stmt in child.body:
out += _task_sites(stmt, guards + (test,))
for stmt in child.orelse:
out += _task_sites(stmt, guards + (f"not ({test})",))
continue
if isinstance(child, ast.Call) and \
ast.unparse(child.func).endswith("create_task"):
if child.args:
out.append((guards, _coroutine_name(child.args[0])))
continue
out += _task_sites(child, guards)
return out
def _call_graph(app_dir):
"""name -> the names it calls, over every module in `app/`.
⚠️ IT IS KEYED BY NAME AND THEREFORE OVER-APPROXIMATES, AND THAT IS THE
SAFE DIRECTION HERE. Two functions called `run` in two modules are one node;
a method call on an unrelated object joins the same node. So a `reachable`
answer is an upper bound and a `NOT reachable` answer is the strong one -
which is why every positive row below is also DRIVEN, with the requests
counted, rather than being left to this graph (MUTATION.md habit 13).
"""
graph = {}
for name in sorted(os.listdir(app_dir)):
if not name.endswith(".py"):
continue
tree = ast.parse(open(os.path.join(app_dir, name),
encoding="utf-8").read())
for node in ast.walk(tree):
if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
continue
called = set()
for inner in ast.walk(node):
if isinstance(inner, ast.Call):
fn = inner.func
if isinstance(fn, ast.Attribute):
called.add(fn.attr)
elif isinstance(fn, ast.Name):
called.add(fn.id)
graph.setdefault(node.name, set()).update(called)
return graph
def _reaches(graph, start, target):
"""Can `start` arrive at `target` through the call graph?"""
seen, stack = set(), [start]
while stack:
cur = stack.pop()
if cur in seen:
continue
seen.add(cur)
for nxt in graph.get(cur, ()):
if nxt == target:
return True
stack.append(nxt)
return False
def _started(sites, opts):
"""[(coroutine, guards, verdict)] for `opts`. Verdict True/False/'?'.
`'?'` is a guard this cannot decide from the options alone - `p1_source is
not None` is one - and it is reported rather than assumed either way. A
fetch-reaching task whose guard is undecidable would make every row below
vacuous, so that is a check of its own.
"""
out = []
for guards, name in sites:
verdict = True
for guard in guards:
try:
answer = bool(eval(guard, vars(_main), {"opts": opts}))
except NameError:
verdict = "?"
break
if not answer:
verdict = False
break
out.append((name, guards, verdict))
return out
_MAIN_TREE = derived(lambda: ast.parse(open(os.path.join(_APP, "main.py"),
encoding="utf-8").read()))
_AMAIN = derived(lambda: next((n for n in ast.walk(_MAIN_TREE)
if isinstance(n, ast.AsyncFunctionDef)
and n.name == "amain"), None))
_SITES = derived(lambda: _task_sites(_AMAIN))
_GRAPH = derived(lambda: _call_graph(_APP))
_FETCH_ENTRY = "resolve_tariff"
print(f" create_task sites found in amain(): "
f"{[(n, list(g)) for g, n in _SITES]}")
print(f" call graph: {len(_GRAPH)} named functions across app/")
print(" ⚠️ what this instrument CANNOT see, largest hole first: a coroutine "
"started by anything other than `create_task` (gather, ensure_future, a "
"thread, a timer); a guard that is not an enclosing `if` in amain() (a "
"while, a try/except, an early return above the site, or a refusal "
"INSIDE the coroutine); any module outside app/; and - because the graph "
"is keyed by NAME - two same-named functions as one node, which makes "
"every `reachable` answer an UPPER bound.")
# -- the instrument is calibrated before anything is graded with it (habit 1) --
check(f"the scanner finds amain()'s create_task sites at all - it found "
f"{len(_SITES) if not isinstance(_SITES, _Raised) else _SITES}, and at "
f"zero every row below would be vacuously true",
not isinstance(_SITES, _Raised) and len(_SITES) >= 2)
check(f"...and the call graph can tell reaching from not-reaching: "
f"`run_tariff_refresh` arrives at {_FETCH_ENTRY!r} and `run_control` - "
f"the control tick, which must never await a fetch - does not. A graph "
f"that answered the same for both would pass every row below however the "
f"gate was wired",
_reaches(_GRAPH, "run_tariff_refresh", _FETCH_ENTRY)
and not _reaches(_GRAPH, "run_control", _FETCH_ENTRY))
def _fetchers(opts):
"""The coroutines amain() STARTS, for `opts`, that can reach the feed."""
return sorted(n for n, _, v in _started(_SITES, opts)
if v is True and _reaches(_GRAPH, n, _FETCH_ENTRY))
def _undecidable_fetchers(opts):
return sorted(n for n, _, v in _started(_SITES, opts)
if v == "?" and _reaches(_GRAPH, n, _FETCH_ENTRY))
# The four configurations. `_T10_BASE` carries no price keys at all, so the
# shipped defaults in PRICE_OPTION_DEFAULTS are what answer - which is the
# install the defect was measured on.
_T10_BASE = {"setpoint_entity": "number.sp", "max_w": 5000,
"soc_entity": "sensor.soc", "batt_entity": "sensor.batt",
"meter_source": "off", "maintenance_charge_w": 2000,
"maintenance_discharge_w": 2000}
_T10_PROFILE = json.dumps({"periods": [
{"name": "night", "days": "all", "from": "22:00", "to": "07:00",
"eur_per_kwh": 0.20},
{"name": "day", "days": "all", "from": "07:00", "to": "22:00",
"eur_per_kwh": 0.284}]})
_T10_CASES = (
("shipped default: no provider, no trading", {}, False),
("PROVIDER configured, trading OFF (the defect)",
{"price_provider": "generic_dayahead"}, True),
("PROVIDER configured, trading ON",
{"price_provider": "generic_dayahead", "arbitrage_enabled": True}, True),
("no provider, trading ON on a static profile",
{"arbitrage_enabled": True, "static_profile": _T10_PROFILE}, True),
)
print(" startup reachability of resolve_tariff(), by configuration:")
for _label, _over, _want in _T10_CASES:
print(f" {'reachable' if _fetchers(dict(_T10_BASE, **_over)) else '- '}"
f" {_label}")
check(f"the shipped defaults really do leave `arbitrage_enabled` off "
f"({PRICE_OPTION_DEFAULTS['arbitrage_enabled']!r}) and `price_provider` "
f"off ({PRICE_OPTION_DEFAULTS['price_provider']!r}) - the precondition "
f"every row below varies from, and if a later card changes either "
f"default these rows stop meaning what they say. Section 1 is what pins "
f"this dict to config.yaml",
PRICE_OPTION_DEFAULTS["price_provider"] == "off"
and PRICE_OPTION_DEFAULTS["arbitrage_enabled"] is False
and not _arb.enabled(dict(_T10_BASE))
and not _price.provider_configured(dict(_T10_BASE)))
check(f"AC1: with a `price_provider` set and `arbitrage_enabled` at its shipped "
f"default, the startup path REACHES the feed "
f"{_fetchers(dict(_T10_BASE, price_provider='generic_dayahead'))}. This "
f"is the shipped defect: it read `[]`, so nothing was ever fetched",
_fetchers(dict(_T10_BASE, price_provider="generic_dayahead")) != [])
check(f"AC2: with `price_provider: off` and trading off - the true shipped "
f"default - NO task reaches the feed "
f"{_fetchers(dict(_T10_BASE))}, so an install that asked for no prices "
f"acquires no periodic work from this card",
_fetchers(dict(_T10_BASE)) == [])
check("...and the guard on the fetch-reaching task is decidable from the "
"OPTIONS alone. A gate moved onto a local variable would make every row "
"in this section vacuous rather than red",
_undecidable_fetchers(dict(_T10_BASE, price_provider="generic_dayahead"))
== [])
# ⚠️ THE `or` IS LOAD-BEARING AND THIS IS THE ROW THAT SAYS SO. `price_provider:
# off` + a `static_profile` + `arbitrage_enabled: true` is a configuration
# validate.py ACCEPTS and that trades (measured: ARBITRAGE_CHARGE -5000 W at
# `cycle_cost_threshold_eur: 0.05` on a 0.20/0.284 day/night profile). Gating
# the refresh on the provider ALONE would leave that site's `tariff_plan` at
# None forever and silently disarm a strategy the UI says is running.
_T10_OLD = {lbl: _arb.enabled(dict(_T10_BASE, **over))
for lbl, over, _ in _T10_CASES}
_T10_NEW = {lbl: bool(_fetchers(dict(_T10_BASE, **over)))
for lbl, over, _ in _T10_CASES}
print(f" old gate (arbitrage.enabled alone) -> {list(_T10_OLD.values())}")
print(f" shipped gate -> {list(_T10_NEW.values())}")
check(f"the gate is a STRICT WIDENING: every configuration that started the "
f"refresh before still starts it, so nothing was taken away from a live "
f"install {[l for l in _T10_OLD if _T10_OLD[l] and not _T10_NEW[l]]}",
all(_T10_NEW[l] for l in _T10_OLD if _T10_OLD[l]))
check("...and it is a widening rather than a no-op: at least one configuration "
"fetches now that did not before, which is the whole ticket",
any(_T10_NEW[l] and not _T10_OLD[l] for l in _T10_OLD))
# -- and the fetch is DRIVEN, because a graph is an argument and a counted ----
# -- request is an observation (habit 4) -------------------------------------
_CAPTURE = derived(lambda: json.load(open(
os.path.join(HERE, "scenarios",
"energy-charts-be-price-2026-09-09_2026-09-10-twoday.json"),
encoding="utf-8")))
def _restamped_body(now):
"""The COMMITTED capture, with only its instants moved onto `now`.
⚠️ THE SHAPE AND THE PRICES ARE THE CAPTURE'S (TARIFF-08, habit 19). What
is synthetic is the CLOCK and nothing else: `run_tariff_refresh` reads the
wall clock and there is no seam to inject one, so a 2026-09-09 capture
would fall outside every horizon it asks for. Section 2b replays the
capture unmodified against the parser; this one is grading the WIRING.
"""
base = int(now.timestamp()) // 3600 * 3600
doc = dict(_CAPTURE)
doc["unix_seconds"] = [base + 900 * i
for i in range(len(_CAPTURE["price"]))]
return json.dumps(doc).encode()
class _T10Hass:
async def limits(self, entity):
return (-5000.0, 5000.0, 1.0)
async def number(self, entity, invert=False):
return {"sensor.grid": 0.0, "sensor.soc": 55.0,
"sensor.batt": 0.0}.get(entity)
async def set_number(self, entity, value):
return True
async def call(self, domain, service, data):
return True
class _T10Store:
data = {}
def set(self, *a):
pass
def get_time(self, key):
return None
class _T10Pub:
enabled = False
def publish(self, *a, **k):
pass
def close(self):
pass
def _drive_startup(over):
"""Start exactly what amain() would start for `opts`, and watch the wire.
⚠️ THE TASK IS STARTED ONLY IF THE SHIPPED GUARD SAYS SO. That is the whole
point: the coroutine is not called because this file knows its name, it is
called because `_fetchers()` evaluated amain()'s own guard against these
options. A gate that goes back to `arbitrage.enabled(opts)` leaves the
coroutine uncalled and the request count at zero.
⚠️ AND IT RUNS UNDER A DEADLINE (habit 5). The refresh loop is infinite by
design; the failure mode of a mutation that never resolves a plan is a HANG,
which reads like flaky infrastructure rather than like a named check.
"""
opts = dict(_T10_BASE, **over)
tmp = tempfile.mkdtemp(prefix="tariff10-")
_tmpdirs_t10.append(tmp)
store_path = os.path.join(tmp, "price_history.json")
seen = []
async def _counting_get(url, timeout_s):
seen.append(url)
return _restamped_body(datetime.now(timezone.utc))
real_get = _price.https_get
_price.https_get = _counting_get
try:
async def _go():
stop = asyncio.Event()
ctl = _main.Controller(opts, _T10Hass(), _T10Store(), _T10Pub(),
stop=stop,
peak_db_path=os.path.join(tmp, "tariff.db"),
price_history_path=store_path)
names = _fetchers(opts)
if "run_tariff_refresh" not in names:
# Nothing amain() starts reaches the feed. Anything ELSE that
# does is a task this harness cannot drive, and it is a FAILURE
# rather than a shrug - see the check below.
return ctl, names
task = asyncio.create_task(ctl.run_tariff_refresh())
for _ in range(250): # ~5 s ceiling
if ctl.tariff_at is not None:
break
await asyncio.sleep(0.02)
ctl.stopping = True
stop.set()
await asyncio.wait_for(task, timeout=5)
return ctl, names
ctl, names = asyncio.run(_go())
finally:
_price.https_get = real_get
try:
n_points = len(list(ctl.tariff_plan.read.points))
except Exception: # noqa: BLE001 - Absence
n_points = 0
return {"requests": seen, "started": names,
"store_present": os.path.exists(store_path),
"points": n_points,
"history": ctl.price_history.words}
_tmpdirs_t10 = []
_on = derived(lambda: _drive_startup({"price_provider": "generic_dayahead"}))
_off = derived(lambda: _drive_startup({}))
_static_armed = derived(lambda: _drive_startup(
{"arbitrage_enabled": True, "static_profile": _T10_PROFILE}))
print(f" provider + trading OFF -> {len(_on['requests'])} request(s), "
f"store written {_on['store_present']}, {_on['points']} point plan, "
f"Price history row: {_on['history'][:70]!r}")
print(f" no provider -> {len(_off['requests'])} request(s), "
f"store written {_off['store_present']}, "
f"Price history row: {_off['history'][:40]!r}")
check(f"AC1, DRIVEN: a shipped-default install with a provider configured "
f"actually ASKS THE FEED - {len(_on['requests'])} request(s), "
f"{_on['requests'][:1]}. The soak measured ZERO here over 40 hours",
len(_on["requests"]) >= 1)
check(f"...and a plan RESOLVES from it: {_on['points']} points, against the "
f"{len(_CAPTURE['price'])} the capture publishes",
_on["points"] == len(_CAPTURE["price"]))
check(f"...and /data/price_history.json is WRITTEN - present={_on['store_present']}. "
f"The soak measured `present: false` on all 802 samples",
_on["store_present"] is True)
check(f"...and the *Price history* status row POPULATES rather than reading "
f"`empty` forever: {_on['history'][:60]!r}",
isinstance(_on["history"], str)
and not _on["history"].startswith("empty")
and not _on["history"].startswith("unavailable"))
check(f"AC2, DRIVEN: `price_provider: off` issues NO request and creates no "
f"store - {len(_off['requests'])} request(s), "
f"present={_off['store_present']}",
_off["requests"] == [] and _off["store_present"] is False)
check(f"...and neither does the one configuration that keeps its task without "
f"a provider - trading armed on a static profile makes "
f"{len(_static_armed['requests'])} request(s), because "
f"`resolve_tariff()` answers a provider-off site out of the static "
f"profile without opening a socket. NOBODY acquires a periodic HTTPS "
f"call from this card",
_static_armed["requests"] == []
and "run_tariff_refresh" in _static_armed["started"])
check(f"...and no OTHER startup task can reach the feed - if one appears, this "
f"harness cannot drive it and every driven row above stops covering the "
f"startup path {_on['started']}",
_on["started"] == ["run_tariff_refresh"])
# -- AC3, and it is the property this card is most dangerous to: ARBITRAGE -----
# -- OFF STILL MEANS NO TRADE, EVER. Graded by DRIVING `Controller.cycle()` ---
# -- with a provider plan resolved - the state TARIFF-10 newly creates - and ---
# -- moving exactly one option (habit 7). ------------------------------------
#
# ⚠️ THE BASELINE IS THE HALF THAT MAKES THIS A CHECK. "No trade" is satisfied
# by a fixture in which nothing could have traded anyway - a flat series, a SoC
# outside the bounds, a peak guard that never ran - and a check like that stays
# green however the gate is wired. So the SAME plan, the SAME SoC, the SAME P1
# history and the SAME threshold are run with `arbitrage_enabled: true`, and
# that run must COMMAND THE BATTERY. If the armed row stops trading, the
# unarmed row stops being evidence and both go red together.
def _t10_sample(when, net_w):
"""One P1 telegram through the shipped derivation."""
imp, exp = split_signed(net_w)
return make_sample("ha_signed", imp, exp, phases=1,
ingest_ts=when, ingest_mono=when.timestamp())
class _T10TradeHass(_T10Hass):
def __init__(self):
self.writes = []
async def set_number(self, entity, value):
self.writes.append(float(value))
return True
def _t10_plan(now, cheap_now):
"""A 24 h FIRM series whose cheapest hour is now, or whose dearest is.
Firm, because refusal 3 drops provisional hours from the trade set and a
plan of carried hours cannot trade whatever the gate says - which would
make the armed baseline below vacuous exactly as a flat series would.
"""
top = now.replace(minute=0, second=0, microsecond=0)
lo, hi = 0.05, 0.60
return TariffPlan(
PriceRead.series(
[PricePoint(start=top + timedelta(hours=i),
eur_per_kwh=(lo if (i < 3) == cheap_now else hi),
provisional=False)
for i in range(24)],
PriceProduct.DAY_AHEAD, "generic_dayahead", now),
None, "provider")
def _t10_trade_cycle(armed, cheap_now):
"""One real `Controller.cycle()` on a resolved provider plan."""
tmp = tempfile.mkdtemp(prefix="tariff10t-")
_tmpdirs_t10.append(tmp)
opts = dict(_T10_BASE, meter_source="ha_dsmr", meter_entity="sensor.grid",
heartbeat_s=10, price_provider="generic_dayahead",
cycle_cost_threshold_eur=0.10, arbitrage_enabled=armed)
async def _go():
hass = _T10TradeHass()
ctl = _main.Controller(opts, hass, _T10Store(), _T10Pub(),
stop=asyncio.Event(),
peak_db_path=os.path.join(tmp, "tariff.db"),
price_history_path=os.path.join(tmp, "ph.json"))
ctl.auto = True
ctl.dev_min, ctl.dev_max = -5000.0, 5000.0
now = datetime.now(timezone.utc)
# ⚠️ A WHOLE QUARTER OF TELEGRAMS, AND FIVE MINUTES IS NOT ENOUGH.
# Without any, `p1_enabled` is on, `inputs_ok` is False and cycle()
# returns before it reaches the strategy at all. With only five
# minutes, whether the quarter is COVERED depends on where the wall
# clock happens to sit inside it: run late in a quarter and the guard
# is blind, engages conservatively, and `decide()` refuses for §9.1's
# reason instead of trading - which would make the armed baseline
# below vacuous on some runs and not others. Measured: the same four
# rows came back `peak guard is holding` at one time of day and
# `ARBITRAGE_CHARGE` at another. 900 s at the shipped 5 s telegram
# cadence covers any quarter from any starting phase.
for k in range(181):
ctl.p1.submit(_t10_sample(now - timedelta(seconds=5 * (180 - k)),
300.0))
ctl.tariff_plan, ctl.tariff_at = _t10_plan(now, cheap_now), now
await ctl.read_inputs()
ctl.tariff_plan, ctl.tariff_at = _t10_plan(now, cheap_now), now
await ctl.cycle()
return ctl, hass
ctl, hass = asyncio.run(_go())
return {"arb": None if ctl.arb is None else ctl.arb.action,
"target": ctl.target, "reason": ctl.reason,
"writes": hass.writes,
"guard": None if ctl.peak_guard is None else ctl.peak_guard.engaged}
_T10_TRADE = derived(lambda: {(a, c): _t10_trade_cycle(a, c)
for a in (False, True) for c in (True, False)})
for _key in sorted(_T10_TRADE, key=str):
_r = _T10_TRADE[_key]
print(f" arbitrage_enabled={str(_key[0]):5s} cheapest hour is now="
f"{str(_key[1]):5s} -> arb={str(_r['arb']):16s} "
f"commanded {_r['target']:+.0f} W, reason {_r['reason'][:40]!r}")
# ⚠️ THE PRECONDITION, AND IT IS THE MASKING HAZARD IN THIS FIXTURE (habit 1).
# `decide()` refuses outright while the peak guard is engaged, §9.1 outranking
# §10.1 - so a fixture whose guard is holding answers "no trade" in EVERY row,
# the armed one included, and the AC3 rows below would grade the guard rather
# than the gate. It is asserted, not assumed.
check(f"the peak guard is CLEAR in all four rows, so what refuses a trade "
f"below is the arbitrage gate and not §9.1's interlock "
f"{ {k: _T10_TRADE[k]['guard'] for k in sorted(_T10_TRADE, key=str)} }",
all(_T10_TRADE[k]["guard"] is False for k in _T10_TRADE))
check("the ARMED baseline actually trades on the plan TARIFF-10 now resolves - "
"it charges in the cheap hour and discharges in the dear one. Without "
"this row, 'trading off never trades' would be satisfied by a fixture "
"that could not have traded either way",
_T10_TRADE[(True, True)]["arb"] == "ARBITRAGE_CHARGE"
and _T10_TRADE[(True, False)]["arb"] == "ARBITRAGE_DISCHARGE")
check(f"...and the site is actually COMMANDED there, which is the only "
f"observable that moves energy: {_T10_TRADE[(True, True)]['target']:+.0f} W "
f"on the cheap hour against "
f"{_T10_TRADE[(True, False)]['target']:+.0f} W on the dear one",
_T10_TRADE[(True, True)]["target"] < -1000.0
and _T10_TRADE[(True, False)]["target"] > 1000.0)
check(f"AC3: with `arbitrage_enabled: false` and the SAME resolved provider "
f"plan, the strategy is never consulted and NOTHING trades - "
f"arb={_T10_TRADE[(False, True)]['arb']!r} / "
f"{_T10_TRADE[(False, False)]['arb']!r} in both directions",
_T10_TRADE[(False, True)]["arb"] is None
and _T10_TRADE[(False, False)]["arb"] is None)
check(f"...and the command is the LOOP's, not a trade that merely went "
f"unnamed: {_T10_TRADE[(False, True)]['target']:+.0f} W / "
f"{_T10_TRADE[(False, False)]['target']:+.0f} W, reason "
f"{_T10_TRADE[(False, True)]['reason'][:30]!r} - the same small "
f"self-consumption figure on both plans, where the armed rows differ by "
f"10 kW between them",
all(abs(_T10_TRADE[(False, c)]["target"]) < 1000.0
and "arbitrage" not in _T10_TRADE[(False, c)]["reason"]
for c in (True, False)))
check("...and the trading gate is untouched by this card: it is still "
"`arbitrage.enabled()` and it is still the only thing the price "
"strategy is gated on - the four rows above differ in that option and "
"in nothing else",
_arb.enabled({"arbitrage_enabled": True})
and not _arb.enabled({})
and not _arb.enabled({"price_provider": "generic_dayahead"}))
# -- AC4: the four surfaces that DESCRIBE this, pinned as a PAIR with it ------
# ⚠️ THIS IS A TEXT CHECK AND IT IS WEAK ON PURPOSE (test_translations.py §3's
# argument). It cannot know a sentence is TRUE. What it CAN do is refuse to let
# the code and the prose move independently: the left side of each pairing is
# DRIVEN above, the right side is read off the file, and the two must agree. It
# shipped as False against True - the docs described Option A for as long as
# both existed - and an overclaim in a doc is a defect in this repository.
_T10_DOCS = derived(lambda: open(os.path.join(HERE, "DOCS.md"),
encoding="utf-8").read())
_T10_CFG = derived(lambda: open(os.path.join(HERE, "config.yaml"),
encoding="utf-8").read())
_T10_TR = derived(lambda: {lang: open(os.path.join(HERE, "translations",
f"{lang}.yaml"),
encoding="utf-8").read()
for lang in ("en", "nl")})
_T10_FETCHES = bool(_on["requests"])
_T10_DOCS_SAYS = "selecting a provider is what starts the fetch" \
in _T10_DOCS.lower()
check(f"AC4: the BEHAVIOUR and DOCS.md agree about what a configured provider "
f"does on a shipped-default install - it fetches = {_T10_FETCHES}, DOCS "
f"says it fetches = {_T10_DOCS_SAYS}. These two are the pair that has to "
f"move together",
_T10_FETCHES == _T10_DOCS_SAYS)
# The claim that had rotted in the OTHER direction: OPT-04 shipped the
# consumer, so "nothing consumes the price series yet" had been false for a
# release before this card made the first half false too.
_T10_STALE = {"config.yaml": "consumes\n # the series yet" in _T10_CFG
or "nothing consumes the price series" in _T10_CFG,
"en.yaml": "nothing consumes the price series yet"
in _T10_TR["en"],
"nl.yaml": "er is nog niets dat de prijsreeks gebruikt"
in _T10_TR["nl"]}
check(f"...and no surface still says the price series has no consumer - untrue "
f"since OPT-04, and it sat beside the claim this card is fixing in all "
f"three files {[k for k, v in _T10_STALE.items() if v]}",
not any(_T10_STALE.values()))
check("...and BOTH translations tell an operator which switch does which - "
"the provider option says it is what starts the fetch, and the trading "
"option says turning it off does not turn the feed off. One language "
"updated and not the other is how this surface rots",
"starts the price fetch" in _T10_TR["en"]
and "does not turn the price feed off" in _T10_TR["en"]
and "start het ophalen van prijzen" in _T10_TR["nl"]
and "zet de prijsfeed niet uit" in _T10_TR["nl"])
# --------------------------------------------------------------------------- #
print()
print("14. TARIFF-14 - the point whose OWN interval covers the instant")
# ⚠️ THE RESOLUTION IS THE WHOLE SECTION, AND ITS ABSENCE IS WHY THE DEFECT
# SHIPPED. Every price fixture in this repository before TARIFF-08 was the
# invented HOURLY row shape, and on an hourly series `s <= u < s + 1 h` and
# "until the next point's start" AGREE AT EVERY INSTANT - so the suite had
# been exercising the one resolution that cannot expose the bug. Section 8
# counts points on the 23- and 25-hour days and never asks `at()` for a
# quarter-hour. Belgium moved to a 15-minute MTU on 2025-10-01 and the live
# day-ahead feed returns 96 points at 900 s.
# ⚠️ BRUSSELS MIDNIGHT, NOT UTC MIDNIGHT, so that every wall-clock time in
# a check name below is the time the series really renders - "00:15" and
# "23:45" are what `canonical()` writes, not an offset away from it.
Q0 = datetime(2026, 1, 15, 0, 0, tzinfo=timezone(timedelta(hours=1)))
HOUR = timedelta(hours=1)
def spaced(gaps, first=0.10, step=0.01, provisional=None, start=Q0):
"""A series whose consecutive gaps are exactly `gaps`, in SECONDS.
`len(points) == len(gaps) + 1`. Prices ascend by `step`, so a point can be
named by VALUE as well as by identity and a sweep that compared only
prices could still tell two points apart.
"""
n = len(gaps) + 1
flags = [False] * n if provisional is None else list(provisional)
pts, t = [], start
for i in range(n):
pts.append(PricePoint(brussels(t), round(first + step * i, 6),
bool(flags[i])))
if i < len(gaps):
t += timedelta(seconds=gaps[i])
return PriceRead.series(pts, PriceProduct.DAY_AHEAD, "tariff14", start)
def gaps_of(s):
u = [p.start.astimezone(UTC) for p in s.points]
return [int((b - a).total_seconds()) for a, b in zip(u, u[1:])]
# The three shapes the acceptance criteria name, plus the two degenerate ones.
Q = spaced([900] * 95) # the shipped 96-point Belgian day
H = spaced([3600] * 23) # what every older fixture carried
# ⚠️ FCST-04'S STEADY STATE, NOT AN EDGE CASE. Eight firm quarter-hours, then
# the carried HOURLY points the fill adds for the hours nobody published. The
# seam between them is a 900 s gap and the tail is 3600 s.
MIX = spaced([900] * 8 + [3600] * 3,
provisional=[False] * 9 + [True] * 3)
# The same seam made the END of the series - the last point is the FIRST
# carried hourly one, and the gap behind it is 900 s. This is the fixture the
# tail decision is graded on.
MIX_TAIL = spaced([900] * 8, provisional=[False] * 8 + [True])
ONE = spaced([])
print("14a. the fixtures are not degenerate - asserted before anything else")
check(f"the quarter-hourly fixture is the live shape - 96 points, spacing "
f"{sorted(set(gaps_of(Q)))} s",
len(Q.points) == 96 and set(gaps_of(Q)) == {900})
check(f"the hourly fixture is 24 points at {sorted(set(gaps_of(H)))} s - and "
f"it is a REGRESSION guard, not a witness: the shipped rule and this "
f"one agree at every instant of it, which is exactly why nothing caught "
f"this",
len(H.points) == 24 and set(gaps_of(H)) == {3600})
check(f"...while the MIXED fixture really is mixed, spacing "
f"{sorted(set(gaps_of(MIX)))} s - the pair the FCST-04 soak measured in "
f"70 % of samples, so a window derived from ONE global spacing would be "
f"wrong in the NORMAL steady state",
set(gaps_of(MIX)) == {900, 3600}
and set(gaps_of(MIX_TAIL)) == {900}
and gaps_of(MIX)[-1] == 3600 and gaps_of(MIX_TAIL)[-1] == 900)
check("...and every point of every fixture carries a DISTINCT price, so 'the "
"covering point' names one point and a wrong answer cannot coincide "
"with the right one",
all(len({p.eur_per_kwh for p in s.points}) == len(s.points)
for s in (Q, H, MIX, MIX_TAIL)))
check("...and the mixed fixtures carry BOTH firm and carried points, which is "
"what makes them FCST-04's output rather than a spacing exercise",
all(any(p.provisional for p in s.points)
and any(not p.provisional for p in s.points)
for s in (MIX, MIX_TAIL)))
print()
print("14b. the reading the shipped scan got wrong, stated as literals")
# ⚠️ ASSERTED AGAINST TYPED-OUT INDICES AND PRICES, not against a window this
# file recomputed by price.py's own rule. The sweep below is exhaustive and
# the sweep is derived; these four are the anchors that keep it honest.
_q15 = derived(lambda: Q.at(Q0 + timedelta(minutes=15)))
check(f"at(00:15) on the 900 s series is the 00:15 point at 0.11 EUR/kWh - the "
f"shipped scan answered the 00:00 point at 0.10, because the :00 "
f"point's HOUR had not run out yet (saw {_q15!r})",
_q15 is Q.points[1] and derived(lambda: _q15.eur_per_kwh) == 0.11)
_q195 = derived(lambda: Q.at(Q0 + timedelta(minutes=195)))
check(f"at(03:15) is the 03:15 point at 0.23 - the shipped scan answered "
f"02:30 at 0.20 here, and that is the SECOND failure mode: from the "
f"second hour on the first point still inside its own hour wins, so the "
f"error COMPOUNDS with depth into the series (saw {_q195!r})",
_q195 is Q.points[13] and derived(lambda: _q195.eur_per_kwh) == 0.23)
check("at(00:30) and at(00:45) are their own quarters too, so the answer is "
"not merely 'one point later'",
derived(lambda: Q.at(Q0 + timedelta(minutes=30))) is Q.points[2]
and derived(lambda: Q.at(Q0 + timedelta(minutes=45))) is Q.points[3])
check("on the MIXED series the quarter-hourly half answers per QUARTER and "
"the carried hourly half answers per HOUR, in one series - at(00:15) is "
"the 00:15 point and at(02:30) is the 02:00 one",
derived(lambda: MIX.at(Q0 + timedelta(minutes=15))) is MIX.points[1]
and derived(lambda: MIX.at(Q0 + timedelta(minutes=150))) is MIX.points[8]
and derived(lambda: MIX.points[8].start.astimezone(UTC)) ==
Q0 + timedelta(hours=2))
print()
print("14c. the exhaustive sweep, at 900 s, 3600 s and mixed")
def windows(s):
"""`(start_utc, width)` per point, computed HERE from the gaps.
Deliberately NOT read off `_Series._cover`: a check built from the table it
grades compares a thing against itself. The rule is restated - a point runs
to the next start, capped at an hour; the last point copies the width in
front of it; a lone point gets an hour.
"""
u = [p.start.astimezone(UTC) for p in s.points]
out = []
for i, t in enumerate(u):
if i + 1 < len(u):
g = u[i + 1] - t
elif i:
g = t - u[i - 1]
else:
g = HOUR
out.append((t, g if g < HOUR else HOUR))
return out
def sweep(s):
"""Every probe whose covering point is unambiguous, and what `at()` said."""
wrong = []
for i, (t, w) in enumerate(windows(s)):
for off in (timedelta(0), w / 2, w - timedelta(seconds=1)):
got = derived(lambda: s.at(t + off))
if got is not s.points[i]:
wrong.append(f"{(t + off).isoformat()} -> {got!r}, "
f"wanted {s.points[i].eur_per_kwh}")
return wrong
for _name, _s in (("900 s", Q), ("3600 s", H), ("mixed 900/3600", MIX),
("mixed, 900 s tail", MIX_TAIL)):
_w = derived(lambda s=_s: sweep(s))
check(f"{_name}: all {3 * len(_s.points)} probes inside the series return "
f"the point whose OWN interval covers them "
f"({_w if _w != [] else 'no misses'})", _w == [])
print()
print("14d. the one-hour cap, which is what keeps FCST-04's holes holes")
# ⚠️ NOT DECORATION. FCST-04 leaves an hour OUT when nothing published it and
# yesterday has no value either, and price_forecast.py's module note names
# `at()` raising Absence for it as the behaviour that sends the caller to the
# static profile (§11.4 for that hour). Uncapped, "until the next point's
# start" would stretch the point before a three-hour hole across the whole
# hole - filling holes with a stale price, which is the defect this module
# exists to refuse, introduced by the fix for a different one.
HOLE = PriceRead.series(
[PricePoint(brussels(Q0 + timedelta(hours=i)), 0.10 + i / 100.0, False)
for i in (0, 1, 4, 5)],
PriceProduct.DAY_AHEAD, "tariff14", Q0)
_in_hole = [Q0 + timedelta(hours=2), Q0 + timedelta(hours=2, minutes=30),
Q0 + timedelta(hours=3), Q0 + timedelta(hours=3, minutes=59)]
check(f"a three-hour hole stays a hole: all {len(_in_hole)} instants inside it "
f"raise Absence, while the point BEFORE it still covers its own hour and "
f"the point after it answers from its own start",
all(raises(lambda u=u: HOLE.at(u), Absence) for u in _in_hole)
and derived(lambda: HOLE.at(Q0 + timedelta(hours=1, minutes=59)))
is HOLE.points[1]
and derived(lambda: HOLE.at(Q0 + timedelta(hours=4))) is HOLE.points[2])
check(f"...and the cap is stated once, as MAX_POINT_SPAN = "
f"{_price.MAX_POINT_SPAN}, rather than spelled into the lookup",
_price.MAX_POINT_SPAN == HOUR)
# ⚠️ AND THE OTHER HALF OF THE HOLE, WHICH IS THE ONE §8.3 READS. FCST-04
# fills a whole hour nobody published with ONE carried point, so a
# quarter-hourly day can carry an hourly carried point in the middle of it.
# The instant inside that hour is covered by the CARRIED point - and the
# shipped scan answered the FIRM quarter 52 minutes earlier, because that was
# the first point still inside its own hour. `_act()`'s `here.provisional`
# guard and `price_forecast._walk()`'s `series.at(u).provisional` both read the
# flag off whatever that lookup returned, so the flag was FIRM on an hour
# nobody published.
SANDWICH = PriceRead.series(
[PricePoint(brussels(Q0 + timedelta(minutes=15 * i)),
round(0.10 + i / 1000.0, 6), False) for i in range(44)]
+ [PricePoint(brussels(Q0 + timedelta(hours=11)), 0.500, True)]
+ [PricePoint(brussels(Q0 + timedelta(hours=12, minutes=15 * i)),
round(0.20 + i / 1000.0, 6), False) for i in range(4)],
PriceProduct.DAY_AHEAD, "tariff14", Q0)
_sand_t = Q0 + timedelta(hours=11, minutes=37)
def shipped_at(s, u):
"""The rule TARIFF-14 replaced, as the COUNTERFACTUAL only - nothing is
graded against it. It is here so the fixture can be SHOWN to separate the
two rules; a check whose data cannot reach the failure it names grades
nothing, and on an hourly series these two agree at every instant."""
uu = u.astimezone(UTC)
for p in s.points:
st = p.start.astimezone(UTC)
if st <= uu < st + HOUR:
return p
return None
check("the sandwich fixture is FCST-04's real output - 44 firm quarters, ONE "
"carried hourly point filling the hour nobody published, then firm "
"quarters again",
len(SANDWICH.points) == 49
and SANDWICH.points[44].provisional is True
and sum(1 for p in SANDWICH.points if p.provisional) == 1)
_sand_now = derived(lambda: SANDWICH.at(_sand_t))
_sand_old = derived(lambda: shipped_at(SANDWICH, _sand_t))
check(f"an instant inside the carried hour is covered by the CARRIED point, "
f"so §8.3's flag is read off the hour it belongs to. The shipped scan "
f"answered the FIRM quarter 52 minutes earlier "
f"({derived(lambda: _sand_old.start.isoformat())}, provisional="
f"{derived(lambda: _sand_old.provisional)}), which is a carried hour "
f"wearing a published hour's flag",
_sand_now is SANDWICH.points[44]
and derived(lambda: _sand_now.provisional) is True
and _sand_old is SANDWICH.points[43]
and derived(lambda: _sand_old.provisional) is False)
print()
print("14e. the LAST point's window - decided and documented, not inherited")
# There is no next start and §9.3 says nothing about how wide the final
# interval is, so THE LAST POINT IS GIVEN THE SAME SPAN AS THE POINT BEFORE IT
# (one hour when it is the only point). The alternative - a flat one-hour tail
# - is defensible for an hourly series and wrong for the live one: it answers
# 00:30 TOMORROW with today's last price, which is reaching for a neighbour
# across a day boundary.
_ql = Q.points[-1].start.astimezone(UTC)
_hl = H.points[-1].start.astimezone(UTC)
_ml = MIX_TAIL.points[-1].start.astimezone(UTC)
check("the 900 s day's last point covers its own QUARTER: at(+14:59) answers "
"and at(+15:00) raises, so today's 23:45 price cannot be returned for "
"00:30 tomorrow",
derived(lambda: Q.at(_ql + timedelta(minutes=14, seconds=59)))
is Q.points[-1]
and raises(lambda: Q.at(_ql + timedelta(minutes=15)), Absence))
check("the hourly series' last point still covers an HOUR, exactly as it "
"shipped - no static or hourly site changes behaviour",
derived(lambda: H.at(_hl + timedelta(minutes=59))) is H.points[-1]
and raises(lambda: H.at(_hl + HOUR), Absence))
check("the MIXED series ending at the firm/carried seam takes the 900 s in "
"front of it, so its last 45 minutes raise Absence. That is the SAFE "
"direction and it is CHOSEN: §11.4 sends the caller to the static "
"profile rather than trading on a span the series never demonstrated, "
"and the point is carried, so §8.3 forbids irreversible action on it "
"anyway",
derived(lambda: MIX_TAIL.at(_ml + timedelta(minutes=14, seconds=59)))
is MIX_TAIL.points[-1]
and raises(lambda: MIX_TAIL.at(_ml + timedelta(minutes=15)), Absence)
and MIX_TAIL.points[-1].provisional)
# ⚠️ HABIT 1. Without this the three checks above would all pass under a
# single global tail rule of 15 minutes, or of an hour, depending which two
# you read - so the claim that the tail FOLLOWS THE SERIES is asserted as its
# own check, at one instant where the three fixtures must disagree.
_t30 = [derived(lambda s=s, l=l: s.covers(l + timedelta(minutes=30)))
for s, l in ((Q, _ql), (H, _hl), (MIX_TAIL, _ml))]
check(f"...and those tails are genuinely three readings of one rule and not "
f"one constant: 30 minutes past its own last point the hourly series "
f"answers and both 900 s ones do not {_t30}",
_t30 == [False, True, False])
check("a series of ONE point has no gap in front of it to copy, so it gets an "
"hour - the shipped behaviour, kept deliberately for the degenerate case",
derived(lambda: ONE.at(Q0 + timedelta(minutes=59, seconds=59)))
is ONE.points[0]
and raises(lambda: ONE.at(Q0 + HOUR), Absence))
print()
print("14f. order is established by the lookup, not assumed of the caller")
# ⚠️ `at()` now reads a point's NEIGHBOURS, so it WOULD depend on sortedness.
# The index is sorted at construction so that it cannot. `_validate_raw()`
# refuses out-of-order and duplicate starts on the `load_prices()` path
# (section 9 above), `price_history._parse_points()` re-sorts what it reads off
# disk and FCST-04's fill emits ascending - but `PriceRead.series()` itself has
# never enforced any of it, and an assumption that holds today and is unstated
# is how this class of defect recurs.
_REV = PriceRead.series(tuple(reversed(Q.points)), PriceProduct.DAY_AHEAD,
"tariff14", Q0)
check("the reversed fixture really is reversed, so the check below is not "
"grading two identical series",
_REV.points[0] is Q.points[-1] and _REV.points[-1] is Q.points[0]
and _REV.points != Q.points)
_rev_bad = derived(lambda: [p.start.isoformat() for p in Q.points
if _REV.at(p.start) is not p])
check(f"...and a series handed its 96 points in REVERSE order answers at() "
f"identically at every one of them "
f"({_rev_bad if _rev_bad != [] else 'no misses'})",
_rev_bad == [])
check("...while the load path refuses an unsorted feed outright, so the two "
"defences are stated rather than one being quietly relied on",
raises(lambda: _price._validate_raw(
[RawPoint(T0 + timedelta(minutes=15), 0.1, False),
RawPoint(T0, 0.2, False)], _caps), SourceUnavailable))
check("a DUPLICATED start does not raise anywhere - not at construction and "
"not on the 30 s control path. The later list entry takes the real span "
"and the earlier one becomes unreachable through at(); load_prices() "
"refuses the shape outright, and this says what happens to anyone who "
"builds one by hand",
derived(lambda: PriceRead.series(
[PricePoint(brussels(Q0), 0.10, False),
PricePoint(brussels(Q0), 0.20, False),
PricePoint(brussels(Q0 + timedelta(minutes=15)), 0.30, False)],
PriceProduct.DAY_AHEAD, "tariff14", Q0).at(Q0).eur_per_kwh) == 0.20)
print()
print("14g. covers() shared it; window() did not")
check("covers() shared the defect EXACTLY, because it has no window rule of "
"its own - it asks at() - so the one change fixes both. On the 900 s "
"day it is now True 14:59 past the last point and False 15:00 past it, "
"where an hour-wide window said True for another 45 minutes",
derived(lambda: Q.covers(_ql + timedelta(minutes=14, seconds=59))) is True
and derived(lambda: Q.covers(_ql + timedelta(minutes=15))) is False
and derived(lambda: Q.covers(_ql + timedelta(minutes=30))) is False
and derived(lambda: Q.covers(Q0 + timedelta(minutes=15))) is True)
check("window() did NOT share it, and the reason is worth a check rather than "
"a rediscovery: it grades a point's START against a half-open range and "
"never asks how long a point lasts, so it carries no resolution "
"assumption at all. 00:07:30 .. 00:22:30 holds exactly the 00:15 point "
"- while at(00:07:30) is the 00:00 one, which is what makes it a "
"DIFFERENT question and not a coverage query",
derived(lambda: Q.window(Q0 + timedelta(minutes=7, seconds=30),
Q0 + timedelta(minutes=22, seconds=30)))
== (Q.points[1],)
and derived(lambda: Q.at(Q0 + timedelta(minutes=7, seconds=30)))
is Q.points[0])
print()
print("14h. the 'no data is not free' contract, unchanged")
check("at() answers with a PricePoint or raises Absence - never None, at any "
"resolution, so `series.at(t) or fallback` cannot become a price of zero",
all(isinstance(derived(lambda s=s: s.at(s.points[0].start)), PricePoint)
for s in (Q, H, MIX, MIX_TAIL, ONE))
and raises(lambda: Q.at(Q0 - timedelta(seconds=1)), Absence)
and raises(lambda: Q.at(Q0 + timedelta(days=2)), Absence))
_absence_words = _reason(lambda: Q.at(Q0 + timedelta(days=2)))
check(f"...and the refusal still names the range the series runs, so an "
f"operator can tell 'outside the horizon' from 'into a hole'",
Q.points[0].start.isoformat() in _absence_words
and Q.points[-1].start.isoformat() in _absence_words)
check("...and it never invents a neighbour: the instant one second before the "
"series starts is refused, not answered with the first point",
raises(lambda: Q.at(Q0 - timedelta(seconds=1)), Absence)
and raises(lambda: MIX.at(Q0 - timedelta(seconds=1)), Absence)
and raises(lambda: H.at(Q0 - timedelta(seconds=1)), Absence))
# --------------------------------------------------------------------------- #
for _d in _tmpdirs_t10:
shutil.rmtree(_d, ignore_errors=True)
# --------------------------------------------------------------------------- #
print()
print("15. TARIFF-15 - the operator's own tariff card, as two coefficients")
#
# WHAT THIS SECTION IS DEFENDING. `custom_formula` lets an operator type the
# `B x index + A` line off their own supplier's tariff card, because there is
# no machine-readable source of Belgian tariff coefficients anywhere and the
# customer is the only party who can see their own. That makes the card the
# ONLY artefact that can grade them - which is exactly what the startup check
# does, and what the checks below grade it doing.
#
# ⚠️ EVERY EXPECTATION HERE IS SOMEBODY ELSE'S PUBLISHED NUMBER. The two cards
# print the formula, the reference index they assumed AND the resulting price,
# so this file compares the SHIPPED code against 15,55 and 7,522 rather than
# against its own restatement of the same arithmetic. That is section 2b's
# rule - grade against something nobody here wrote - applied to arithmetic.
print()
print("15a. the two REAL cards reproduce their PRINTED prices, through the "
"shipped code")
# ⚠️ NON-DEGENERACY FIRST (habit 1). Two cards that agreed on every field
# would be one card counted twice, and the pair is what says the arithmetic is
# general rather than tuned to one supplier.
check(f"the two cards really are two - factor {ENECO_CARD['factor']} vs "
f"{ENGIE_CARD['factor']}, offset {ENECO_CARD['offset']} vs "
f"{ENGIE_CARD['offset']}, index "
f"{ENECO_CARD['card']['index_eur_per_mwh']} vs "
f"{ENGIE_CARD['card']['index_eur_per_mwh']}",
ENECO_CARD["factor"] != ENGIE_CARD["factor"]
and ENECO_CARD["offset"] != ENGIE_CARD["offset"]
and ENECO_CARD["card"]["index_eur_per_mwh"]
!= ENGIE_CARD["card"]["index_eur_per_mwh"]
and ENECO_CARD["card"]["expect_incl_vat"]
!= ENGIE_CARD["card"]["expect_incl_vat"])
# The suppliers' own printed prices, transcribed from the cards and from
# nowhere in app/.
PRINTED = {"Eneco (0.102 x 133.9905 + 1) x 1.06": (ENECO_CARD, 15.55),
"ENGIE (0.1 x 61.96 + 0.9) x 1.06": (ENGIE_CARD, 7.522)}
for _what, (_card, _printed) in sorted(PRINTED.items()):
_spec = derived(lambda c=_card: parse_price_formula(json.dumps(c)))
_got = derived(lambda sp=_spec: in_card_units(
sp, sp.card_index_eur_per_mwh))
check(f"{_what} = {_got} c EUR/kWh through the shipped module, which "
f"rounds to the {_printed} the card prints",
isinstance(_got, float)
and round(_got, len(str(_printed).split('.')[-1])) == _printed)
# ...and the same two cards end to end through `load_prices()`, i.e. through
# the registered plugin, the columnar parser and `assemble()` - not through a
# module function this file called directly. The feed body carries the card's
# OWN index value, so the point that comes out must be the card's own printed
# price in EUR/kWh plus the framework's taxes and nothing else.
for _what, (_card, _printed) in sorted(PRINTED.items()):
_idx = _card["card"]["index_eur_per_mwh"]
_read = derived(lambda c=_card, i=_idx: load(
"custom_formula", {"price_formula": json.dumps(c)},
transport=canned(columnar_feed([i]))))
_all_in = derived(lambda r=_read: r.points[0].eur_per_kwh)
_want = round(_printed / 100.0 + TAXES, 6)
check(f"{_what}: end to end through load_prices() the hour comes out at "
f"{_all_in} EUR/kWh all-in, which is the card's printed "
f"{_printed} c EUR/kWh plus price_taxes_eur_kwh and nothing else",
isinstance(_all_in, float) and abs(_all_in - _want) < 5e-5)
print()
print("15b. the card self-check REFUSES at startup, naming the discrepancy")
# ⚠️ THIS IS THE WHOLE SAFETY ARGUMENT AND IT IS GRADED ON THE REASON, NOT THE
# VERDICT (MUTATION.md, TEST-16). Several rules here can refuse the same blob -
# a wrong `units`, a missing key, a bad number - so a check that only asked
# "was it refused" would pass on a tree where the card comparison is deleted.
def spoilt(**over):
"""Eneco's card with one field replaced. Everything else is the real one."""
c = json.loads(json.dumps(ENECO_CARD))
for k, v in over.items():
if k.startswith("card_"):
c["card"][k[5:]] = v
else:
c[k] = v
return c
def refusal(blob):
return _reason(lambda: parse_price_formula(json.dumps(blob)))
_honest = refusal(ENECO_CARD)
check("the CONTROL first: the real card is NOT refused, or every refusal "
"below would be graded against a rule that refuses everything",
_honest == "")
# ⚠️ THE EXPENSIVE ONE, AND THE REASON THIS PROVIDER IS SAFE TO SHIP. A
# Belgian card's Tariefformule line is the ENERGY component alone; the add-on
# adds price_taxes_eur_kwh on top. An operator who folds ~12 c EUR/kWh of
# network charge into `offset` is billed for it twice, and `code_leaks()`
# cannot see it because it is CONFIG, not code. The card is what catches it:
# the printed estimate is the energy component, so the inflated offset cannot
# reproduce it.
_NETWORK_C_KWH = 12.0
_double = refusal(spoilt(offset=ENECO_CARD["offset"] + _NETWORK_C_KWH))
_double_price = derived(lambda: in_card_units(
_cf.FormulaSpec(index="epex_be", resolution="hourly", factor=0.102,
offset=ENECO_CARD["offset"] + _NETWORK_C_KWH,
units="c_eur_per_kwh", vat_pct=6, card_month="2026-09",
card_index_eur_per_mwh=133.9905,
card_expect_incl_vat=15.55), 133.9905))
check(f"network charges folded into `offset` move the card's own price to "
f"{_double_price} c EUR/kWh against a printed 15.55, so it CANNOT pass "
f"- the counterfactual first, or the refusal below grades nothing",
isinstance(_double_price, float) and abs(_double_price - 15.55) > 10)
check(f"...and it is refused, by the CARD rule, naming the discrepancy and "
f"the network-charge cause ({_double[:60]!r})",
"the card says" in _double
and "network charges are not in the offset" in _double)
_CORRUPTIONS = {
"VAT left out (vat_pct 0)": spoilt(vat_pct=0),
"VAT applied twice (the card's price already carries it)":
spoilt(card_expect_incl_vat=round(15.55 * 1.06, 4)),
"the coefficients in c EUR and the expected price in EUR":
spoilt(card_expect_incl_vat=0.1555),
"the index typed in EUR/kWh instead of EUR/MWh":
spoilt(card_index_eur_per_mwh=0.1339905),
"a transposed digit in the expected price (15.05 for 15.55)":
spoilt(card_expect_incl_vat=15.05),
"a transposed digit in the factor (0.120 for 0.102)":
spoilt(factor=0.120),
}
for _name, _blob in sorted(_CORRUPTIONS.items()):
_why = refusal(_blob)
check(f"{_name}: refused, and by the CARD rule rather than by another one "
f"({_why[:48]!r})",
"the card says" in _why)
# ⚠️ THE TOLERANCE IS A REAL EDGE AND IT IS GRADED FROM BOTH SIDES, or
# "within 0.01" and "within anything" would be the same check.
_inside = refusal(spoilt(card_expect_incl_vat=round(
in_card_units(parse_price_formula(json.dumps(ENECO_CARD)), 133.9905)
+ CARD_TOLERANCE * 0.5, 6)))
_outside = refusal(spoilt(card_expect_incl_vat=round(
in_card_units(parse_price_formula(json.dumps(ENECO_CARD)), 133.9905)
+ CARD_TOLERANCE * 2.0, 6)))
check(f"half a tolerance out passes and twice a tolerance out does not, so "
f"{CARD_TOLERANCE} is the number being enforced rather than a shape",
_inside == "" and "the card says" in _outside)
# A card that is absent is refused, and NOT by the generic unknown-field rule.
check(f"a formula with NO card block is refused, and the message says why the "
f"card is required rather than merely that a field is missing",
"no \"card\" block" in refusal({k: v for k, v in ENECO_CARD.items()
if k != "card"}))
print()
print("15c. the index path is generic_dayahead's, replayed from the SAME "
"captured bytes")
# A networked plugin with no capture is what section 2b's roster forbids. This
# one reads the same endpoint with the same parser, so it is replayed from the
# same real bodies rather than from a fixture written here.
_CF_OPTS = {"price_formula": json.dumps(ENECO_CARD)}
_cf_replay = {n: load("custom_formula", _CF_OPTS, transport=canned(b))
for n, b in _REAL.items()}
check(f"the SHIPPED custom_formula parses all three captured real bodies "
f"{[r.words for r in _cf_replay.values()]}",
all(r.kind == "series" for r in _cf_replay.values()))
_cf_counts = derived(lambda: {n: len(r.points)
for n, r in _cf_replay.items()})
check(f"...and emits one point per published instant at quarter_hourly "
f"{_cf_counts}",
_cf_counts == {n: len(json.loads(b.decode("utf-8"))["price"])
for n, b in _REAL.items()})
# The URL is the same one, derived from the same helper. Behaviour, off the
# injected transport, not source text.
_cf_t = canned(_REAL[_TD])
run(load_prices(opts(price_provider="custom_formula", **_CF_OPTS),
_w0, _w0 + timedelta(hours=48), transport=_cf_t))
check(f"...and asks for the same Brussels range generic_dayahead asks for "
f"{nth(_cf_t.calls, 0)[0]!r}",
nth(_cf_t.calls, 0)[0] == asked_url(_w0, _w0 + timedelta(hours=48)))
# The price at each instant is the document's own index through the card's
# arithmetic - computed HERE from the captured bytes, by different arithmetic.
_doc = json.loads(_REAL[_QH].decode("utf-8"))
_want_cf = [round((0.102 * v + 1.00) * 1.06 / 100.0 + TAXES, 6)
for v in _doc["price"]]
_got_cf = derived(lambda: [p.eur_per_kwh
for p in _cf_replay[_QH].points])
check(f"every one of the {len(_want_cf)} points is (0.102 x index + 1) x 1.06 "
f"c EUR/kWh plus taxes, against the captured document's own index "
f"column (first three {_got_cf[:3] if isinstance(_got_cf, list) else _got_cf})",
_got_cf == _want_cf)
# ...and the index really varies over that day, or one constant would grade the
# whole column.
check(f"...and the captured index really varies across the day "
f"({min(_doc['price'])} .. {max(_doc['price'])} EUR/MWh), so that "
f"comparison is 96 different numbers and not one repeated",
len(set(_doc["price"])) > 50)
print()
print("15d. resolution: the contract's series, not the feed's")
# ⚠️ THIS IS NOT TARIFF-14's HOURLY ASSUMPTION COMING BACK. It decides how many
# points are EMITTED; the window each one covers is still derived from its
# neighbours by _cover_table(). Both halves are graded.
_hourly_card = dict(ENECO_CARD, resolution="hourly")
_h_read = load("custom_formula", {"price_formula": json.dumps(_hourly_card)},
transport=canned(_REAL[_QH]))
_q_read = _cf_replay[_QH]
check(f"the same 96-point body is 24 points at `hourly` and 96 at "
f"`quarter_hourly` ({len(_h_read.points)} vs {len(_q_read.points)}), so "
f"the option is doing something",
len(_h_read.points) == 24 and len(_q_read.points) == 96)
# The hourly value is the MEAN of that hour's quarters, computed here from the
# captured document rather than from the module.
_by_hour = {}
for _t, _v in zip(_doc["unix_seconds"], _doc["price"]):
_by_hour.setdefault(
datetime.fromtimestamp(_t, UTC).replace(minute=0), []).append(_v)
_want_h = [round((0.102 * (sum(v) / len(v)) + 1.00) * 1.06 / 100.0 + TAXES, 6)
for _k, v in sorted(_by_hour.items())]
_got_h = derived(lambda: [p.eur_per_kwh for p in _h_read.points])
check(f"...and each hourly point is the MEAN of its four quarters through the "
f"card's arithmetic (first three {_got_h[:3] if isinstance(_got_h, list) else _got_h})",
_got_h == _want_h)
# ⚠️ NON-DEGENERACY: an hour whose four quarters were equal would make "the
# mean" and "the first quarter" the same number, and the check above would
# grade nothing.
_differs = sum(1 for _k, v in _by_hour.items() if len(set(v)) > 1)
check(f"...and {_differs} of the {len(_by_hour)} hours really have quarters "
f"that DIFFER, so the mean is distinguishable from taking the first one",
_differs >= 20)
# An already-hourly feed averages one member per hour - the identity - so the
# option cannot silently assume the granularity it was handed.
_h1_pairs = [(datetime.fromtimestamp(t, UTC), v) for t, v in
zip(json.loads(_REAL[_H1].decode("utf-8"))["unix_seconds"],
json.loads(_REAL[_H1].decode("utf-8"))["price"])]
check("an ALREADY hourly feed is the identity under `hourly`, so the option "
"assumes nothing about the granularity it was handed",
derived(lambda: to_resolution(_h1_pairs, "hourly")) == _h1_pairs)
# and TARIFF-14: the window is still the neighbours', not an assumed hour.
_h_pts = _h_read.points
check("...and the hourly series' coverage is still DERIVED from the "
"neighbours - at() answers 59 minutes into a point and the next point "
"at 60, which is _cover_table()'s rule and not a rule this card added",
derived(lambda: _h_read.at(_h_pts[0].start + timedelta(minutes=59))
is _h_pts[0]) is True
and derived(lambda: _h_read.at(_h_pts[0].start + timedelta(minutes=60))
is _h_pts[1]) is True)
print()
print("15e. every refusal in the table, and the two that are WARNINGS")
_TABLE = {
"units absent": (spoilt(units=""), "units", "Choose one of"),
"units not one of the enumerated ones":
(spoilt(units="cents"), "units", "Choose one of"),
"factor == 0 (every spread becomes 0, arbitrage never fires)":
(spoilt(factor=0), "factor", "static_profile"),
"factor < 0 (the dearest hour becomes the cheapest)":
(spoilt(factor=-0.102), "factor", "turns the market upside down"),
"vat_pct above 30 (the 6 / 1.06 / 106 confusion)":
(spoilt(vat_pct=106), "vat_pct", "between 0 and 30"),
"vat_pct below 0": (spoilt(vat_pct=-6), "vat_pct", "between 0 and 30"),
"a non-numeric coefficient": (spoilt(factor="0.102"), "factor",
"not a number"),
"a coefficient that is an EXPRESSION rather than a number":
(spoilt(factor="0.1*x"), "factor", "Write a plain number"),
"a non-finite coefficient": (spoilt(offset=float("inf")), "offset",
"not finite"),
"an unknown index": (spoilt(index="belpex_h"), "index", "can only fetch"),
"an unknown resolution": (spoilt(resolution="daily"), "resolution",
"Choose one of"),
"an unknown top-level field (which is how 'expr' is answered)":
(spoilt(expr="0.102*spot+1"), "expr", "It takes only"),
"an unknown field inside the card": ({**ENECO_CARD,
"card": dict(ENECO_CARD["card"],
vat="6")},
"vat", "unknown field(s)"),
}
for _name, (_blob, _names, _needle) in sorted(_TABLE.items()):
_why = refusal(_blob)
check(f"{_name}: refused, naming the field and the consequence "
f"({_why[:48]!r})",
_names in _why and _needle in _why)
# ⚠️ AND THE ARM THAT MAKES THOSE MEAN SOMETHING: a NaN is not merely refused
# by "not a number", and `True` is an int in Python.
check("NaN is refused as NOT FINITE rather than as not a number, so the two "
"rungs are distinguishable",
"not finite" in refusal(spoilt(factor=float("nan"))))
check("...and a bool is refused as not a number, because `True` is an int "
"here and would price the whole day at the index",
"not a number" in refusal(spoilt(factor=True)))
# The plausibility band. ⚠️ IT IS A SEPARATE QUANTITY FROM THE CARD CHECK: a
# unit error applied to BOTH sides cancels out of the card comparison and
# cannot cancel out of this one.
_both_sides = spoilt(factor=0.00102, offset=0.01,
card_expect_incl_vat=0.1555)
_bs_spec = derived(lambda: parse_price_formula(json.dumps(_both_sides)))
check("a unit error applied to BOTH sides passes the card self-check - stated "
"as a check, because it is the limit of that guard and not a bug in it",
not isinstance(_bs_spec, _Raised)
and abs(card_discrepancy(_bs_spec)) <= CARD_TOLERANCE)
_band = derived(lambda: implausible_all_in(_bs_spec, 0.0))
check(f"...and the all-in band is what catches it, naming the band "
f"({str(_band)[:48]!r})",
isinstance(_band, str) and "plausible band" in _band)
check("...and the REAL card is inside the band, so the band is not simply "
"refusing everything",
implausible_all_in(parse_price_formula(json.dumps(ENECO_CARD)),
0.06) is None)
check(f"...and the band really is {PLAUSIBLE_LO_EUR_KWH}-"
f"{PLAUSIBLE_HI_EUR_KWH} EUR/kWh at both ends, graded from inside and "
f"outside each",
implausible_all_in(parse_price_formula(json.dumps(ENECO_CARD)),
PLAUSIBLE_HI_EUR_KWH) is not None
and implausible_all_in(parse_price_formula(json.dumps(
spoilt(factor=0.0001, offset=0.0009,
card_expect_incl_vat=round((0.0001 * 133.9905 + 0.0009)
* 1.06, 6)))), 0.0) is not None)
# ⚠️ A STALE CARD IS A WARNING AND MUST NEVER BECOME A REFUSAL: ENGIE freezes
# a contract's coefficients for its duration, so a two-year-old card can be
# exactly right and a site refused over one is a site that cannot start.
_old_card = spoilt(card_month="2019-01")
check("an OLD card is not refused - the parse succeeds",
refusal(_old_card) == "")
check("...and it is warned about, naming the month and saying why it is not "
"refused",
"2019-01" in (stale_card(parse_price_formula(json.dumps(_old_card)))
or "")
and "still starts" in (stale_card(
parse_price_formula(json.dumps(_old_card))) or ""))
check("...while a card from this month draws no warning, so the rule is the "
"AGE and not the presence of the field",
stale_card(parse_price_formula(json.dumps(ENECO_CARD)),
today=date(2026, 9, 13)) is None)
print()
print("15f. NO EXPRESSION EVALUATION OF ANY KIND - and the check that says so")
# WHY THIS IS A CHECK AND NOT A PROMISE. This string is settable over HTTP: the
# add-on serves `POST config/save` -> `POST /addons/self/options` on its own
# ingress page, so `eval` on it is remote code execution in a root container
# holding SUPERVISOR_TOKEN, with homeassistant_api and hassio_api, driving an
# inverter over RS485. And the deciding argument is not security: two
# coefficients can be GRADED against the card's printed price and an arbitrary
# expression cannot be graded against anything, so an expression would make
# the invisible-wrong-coefficient problem strictly worse.
_EVAL_NAMES = ("eval", "exec", "compile", "__import__", "literal_eval",
"eval_expr", "evaluate", "Interpreter", "simple_eval")
def eval_calls(source):
"""Every call in `source` whose target NAME is an evaluator. AST, not grep.
⚠️ IT WALKS THE TREE RATHER THAN MATCHING TEXT, so `eval` inside a comment
or a docstring - and this file's own prose about eval - is not a hit, and
`ast.literal_eval(...)` written as `literal_eval(...)` is.
"""
found = []
for node in ast.walk(ast.parse(source)):
if not isinstance(node, ast.Call):
continue
fn = node.func
name = (fn.id if isinstance(fn, ast.Name)
else fn.attr if isinstance(fn, ast.Attribute) else "")
if name in _EVAL_NAMES:
found.append(name)
return sorted(set(found))
_CF_SRC = open(os.path.join(HERE, "app", "custom_formula.py"),
encoding="utf-8").read()
check(f"app/custom_formula.py evaluates NOTHING: {eval_calls(_CF_SRC) or '[]'}",
eval_calls(_CF_SRC) == [])
check("...and neither does the plugin class that reads it",
eval_calls(dedent(inspect.getsource(PROVIDERS["custom_formula"]))) == [])
# ⚠️ THE RED DIRECTION, EXERCISED ON EVERY RUN. A scanner that found nothing
# because it scans nothing is the third failure mode in MUTATION.md, and this
# is the cheap way to keep it honest.
check("...and the scanner CAN go red: planted `eval` and `literal_eval` calls "
"are both found",
eval_calls("def f(s):\n return eval(s)\n") == ["eval"]
and eval_calls("import ast\ndef f(s):\n"
" return ast.literal_eval(s)\n") == ["literal_eval"])
# and the structural half: the coefficients are NUMBERS, so there is nothing
# for an evaluator to be reached from even if one appeared.
# ⚠️ TARIFF-16 ADDED A FIFTH STRING FIELD (`option`) AND THE COUNT ALONE WOULD
# HAVE ABSORBED IT SILENTLY, so the field NAMES are asserted beside the count
# and the new one is pinned to a CLOSED SET rather than to "it is a str". A
# string field that took free text would be the grammar this module refuses to
# have, wearing a different key.
_T15_FIELDS = derived(lambda: {
k: type(v).__name__ for k, v in
vars(parse_price_formula(json.dumps(ENECO_CARD))).items()})
print(f" FormulaSpec fields: {_T15_FIELDS}")
check("the spec carries only fixed scalars - no field holds a string that is "
"later interpreted",
derived(lambda: sorted(_T15_FIELDS.values())) ==
["float"] * 5 + ["str"] * 5)
check(f"...and the five strings are named, not counted: four enumerations and "
f"a card month, plus TARIFF-16's `option`, which is a KEY OF `_LEGS` "
f"and not free text {sorted(_cf._LEGS)}",
derived(lambda: sorted(k for k, t in _T15_FIELDS.items()
if t == "str"))
== ["card_month", "index", "option", "resolution", "units"]
and derived(lambda: _T15_FIELDS and parse_price_formula(
json.dumps(ENECO_CARD)).option) in _cf._LEGS)
check("...and a field that LOOKS like an expression is refused as not a "
"number rather than parsed as one",
"not a number" in refusal(spoilt(factor="0.102 * spot")))
print()
print("15g. it declares {ENERGY, MARKUP}, so the framework adds taxes ONCE")
_cf_caps = PROVIDERS["custom_formula"].capabilities()
check(f"custom_formula sources {sorted(c.value for c in _cf_caps.sources)} - "
f"a Belgian card's Tariefformule line is the ENERGY component and the "
f"contract's own markup, and nothing else",
_cf_caps.sources == frozenset({Component.ENERGY, Component.MARKUP}))
check(f"...so the framework adds exactly "
f"{sorted(c.value for c in _cf_caps.adds)}",
_cf_caps.adds == frozenset({Component.TAXES}))
# THE COST OF THE ALTERNATIVE, MEASURED, so the declaration is not grading
# itself. Declaring ALL_IN would drop the taxes silently - on every hour, on
# behalf of an operator who typed exactly what their card says.
_as_declared = derived(lambda: assemble(
_cf_caps, [RawPoint(T0, 0.1555, False)], MARKUP, TAXES)[0].eur_per_kwh)
_as_all_in = derived(lambda: assemble(
ProviderCapabilities(product=PriceProduct.DAY_AHEAD, sources=ALL_IN),
[RawPoint(T0, 0.1555, False)], MARKUP, TAXES)[0].eur_per_kwh)
check(f"declaring ALL_IN would price every hour {_as_declared} -> "
f"{_as_all_in} EUR/kWh, exactly one taxes component low, on behalf of "
f"an operator who typed what their card said",
isinstance(_as_declared, float) and isinstance(_as_all_in, float)
and abs((_as_declared - _as_all_in) - TAXES) < 1e-9)
check("and the configured markup is reported as IGNORED under it, because the "
"contract's formula already IS the markup",
(lambda: (lambda b, h: (
logging.getLogger("goodwe.price").addHandler(h),
load_any("custom_formula"),
logging.getLogger("goodwe.price").removeHandler(h),
"custom_formula" in b.getvalue()
and "double-count" in b.getvalue())[-1])(
*(lambda b: (b, logging.StreamHandler(b)))(io.StringIO())))())
print()
print("15h. the startup preview - a factor of 1000 visible in one line")
_prev_buf = io.StringIO()
_prev_h = logging.StreamHandler(_prev_buf)
_prev_log = logging.getLogger("goodwe.price")
_prev_level = _prev_log.level
# ⚠️ THE PREVIEW IS AT INFO AND THE DEFAULT ROOT LEVEL IS WARNING, so a check
# that forgot this line would grade an empty buffer and read as "the preview
# is missing". main.py runs the add-on at INFO - that is where `tariff
# refreshed: %s` is emitted from - so this makes the check's logger match the
# shipped one rather than relaxing anything.
_prev_log.setLevel(logging.INFO)
_prev_log.addHandler(_prev_h)
_price._PREVIEWED.discard("custom_formula")
load("custom_formula", _CF_OPTS, transport=canned(_REAL[_QH]))
_first_run = _prev_buf.getvalue()
load("custom_formula", _CF_OPTS, transport=canned(_REAL[_QH]))
_second_run = _prev_buf.getvalue()[len(_first_run):]
_prev_log.removeHandler(_prev_h)
_prev_log.setLevel(_prev_level)
check("the preview says it prints the index in EUR/MWh beside the all-in "
"EUR/kWh",
"index EUR/MWh -> all-in EUR/kWh" in _first_run)
# ⚠️ AND THE ROW, NOT ONLY THE HEADING. The check above was the whole of this
# for one draft, and deleting the index column from the ROW was a -13 B
# SURVIVOR of the entire suite: the sentence stayed true while the thing it
# describes was gone. MUTATION.md's third failure mode, in the check written
# to close it. The expectation is built here from the captured document.
_row_index = _doc["price"][0]
_row_price = (0.102 * _row_index + 1.00) * 1.06 / 100.0 + TAXES
_want_row = f"{_row_index:9.3f} -> {_row_price:9.5f}"
check(f"...and each ROW really carries the index BESIDE its own price "
f"({_want_row!r}), which is the pair a heading alone does not print",
_want_row in _first_run)
check("...and the min/median/max and the day's spread, in one line",
"min " in _first_run and "median " in _first_run
and "max " in _first_run and "spread " in _first_run)
_want_min = round(min((0.102 * v + 1.00) * 1.06 / 100.0 + TAXES
for v in _doc["price"][:96]), 5)
check(f"...and the figures are the SERIES' own, not a restatement: the "
f"minimum printed is {_want_min:.5f}, computed here from the captured "
f"document",
f"min {_want_min:.5f}" in _first_run)
check("...and it is emitted ONCE per process, not on every half-hourly "
"refresh - a log line must not become a log flood on the one path "
"§11.4 already makes hard to read",
"index EUR/MWh -> all-in EUR/kWh" not in _second_run)
# ⚠️ AND IT CANNOT STOP A BATTERY. A preview that raises is a log line that
# failed, not a fetch that failed.
class _BoomPreview(PriceProvider):
"""A plugin whose preview raises. AC: prices are unaffected.
SOURCES: energy
The framework ADDS: markup, taxes
"""
id = "_preview_boom"
@classmethod
def capabilities(cls):
return ProviderCapabilities(product=PriceProduct.DAY_AHEAD,
sources=frozenset({Component.ENERGY}))
async def fetch(self, opts, since, until, transport):
return [RawPoint(T0, 0.1, False)]
def startup_preview(self, points):
raise RuntimeError("the preview is broken")
PROVIDERS["_preview_boom"] = _BoomPreview
_boom_read = derived(lambda: load("_preview_boom", transport=refuse_transport))
del PROVIDERS["_preview_boom"]
check("a preview that RAISES does not cost the site its prices - the series "
"still comes back",
not isinstance(_boom_read, _Raised) and _boom_read.kind == "series")
print()
print("15i. the startup gate refuses it, so nothing is discovered at 03:00")
check("custom_formula with a correct card starts",
validate_startup_config(vbase(price_provider="custom_formula",
price_formula=json.dumps(ENECO_CARD)),
-5000, 5000) == [])
check("custom_formula with NO formula is refused, naming the option and "
"showing the shape",
why(validate_startup_config(vbase(price_provider="custom_formula"),
-5000, 5000),
"(price_formula) is empty"))
check("a formula that does not reproduce its card is refused AT STARTUP, "
"with the discrepancy named",
why(validate_startup_config(
vbase(price_provider="custom_formula",
price_formula=json.dumps(spoilt(offset=13.0))),
-5000, 5000), "the card says"))
check("...and a formula that is not JSON at all is refused with the shape it "
"wanted, rather than with a traceback",
why(validate_startup_config(
vbase(price_provider="custom_formula", price_formula="0.102*spot+1"),
-5000, 5000), "price_formula is not valid near character"))
check(f"...and a card outside the plausible band is refused too, WITH THE "
f"SHIPPED taxes in place - the flat "
f"{PRICE_OPTION_DEFAULTS['price_taxes_eur_kwh']} must not be able to "
f"carry a hundredfold error over the floor",
why(validate_startup_config(
vbase(price_provider="custom_formula",
price_formula=json.dumps(_both_sides)),
-5000, 5000), "plausible band"))
_masked = implausible_all_in(_bs_spec,
PRICE_OPTION_DEFAULTS["price_taxes_eur_kwh"])
check(f"...and that is a REAL trap, measured: the same card plus the shipped "
f"{PRICE_OPTION_DEFAULTS['price_taxes_eur_kwh']} of taxes totals "
f"{price_eur_kwh(_bs_spec, _bs_spec.card_index_eur_per_mwh) + PRICE_OPTION_DEFAULTS['price_taxes_eur_kwh']:.5f} "
f"EUR/kWh, which is INSIDE the band - a floor applied to the all-in "
f"figure would have passed it",
isinstance(_masked, str)
and PLAUSIBLE_LO_EUR_KWH <= (
price_eur_kwh(_bs_spec, _bs_spec.card_index_eur_per_mwh)
+ PRICE_OPTION_DEFAULTS["price_taxes_eur_kwh"])
<= PLAUSIBLE_HI_EUR_KWH)
# ⚠️ AND THE ONE ROW OF THE TABLE THAT IS A WARNING RATHER THAN A REFUSAL,
# graded AT THE GATE and not only at the parser - which is where the two could
# quietly become the same thing.
check("...while a card older than six months still STARTS: ENGIE freezes a "
"contract's coefficients for its duration, so an old card is often "
"exactly right and a site refused over one is a site that cannot run",
validate_startup_config(
vbase(price_provider="custom_formula",
price_formula=json.dumps(_old_card)), -5000, 5000) == [])
# ⚠️ THE ID IS IN THE SCHEMA, or the Supervisor refuses the options blob before
# the add-on runs and none of the sentences above is ever reached.
check("the id is offered by config.yaml's schema, so a site configured on it "
"reaches these messages rather than a Supervisor schema error",
"custom_formula" in _schema_ids)
# =========================================================================== #
# 12. TARIFF-11 - THE RENDERED SURFACE, GRADED AS A SURFACE
# =========================================================================== #
#
# WHAT THIS SECTION IS FOR, and it is the only thing on the card that is not a
# one-line fix. Four defects shipped together and every one of them survived 29
# green suites for the SAME reason: **nothing in this repository graded the
# strings a human is shown.** Every existing tariff check inspects the
# `TariffPlan` OBJECT - `plan.read.points`, `plan.firm_points()`, `verdict.kind`
# - and all four defects were in the SENTENCES built out of that object:
#
# 1. `TariffPlan.words` counted POINTS and called them HOURS, so a 33-hour
# plan logged `dynamic_dayahead from generic_dayahead, 132 h`;
# 2. §8.3's and the flat rung's refusals did the same, and those reach an
# operator through main.py's *Arbitrage* row, not only the log;
# 3. the plan reached NO status row at all - `rows["Tariff"]` was
# `static_status()`'s own sentence and read `'no static profile'` at all
# 802 samples of a 40-hour soak on a site whose feed was answering;
# 4. `rows["Arbitrage"]` rendered "trading is off" and "no decision was
# reached this cycle" as the SAME string, naming an option that was on.
#
# ⚠️ SO THIS SECTION GRADES `status_payload()["rows"]` AND THE REFUSAL
# SENTENCES, NOT THE OBJECTS BEHIND THEM. It renders through the shipped
# `Controller`, parses each number OUT OF THE ROW BY ITS OWN SLOT, and compares
# it against a figure this file computed from the FIXTURE'S CONSTRUCTION
# PARAMETERS - never from the property the row was built out of. That is
# MUTATION.md habit 4: if both sides of a comparison come from the same
# expression, the check is decoration.
#
# ⚠️ AND IT IS DELIBERATELY NOT A SUBSTRING TEST OVER A SENTENCE THIS FILE ALSO
# WROTE. Three checks in this chain turned out to be graded by something they
# did not own - a docstring, a deleted plugin's text, the HEADING above a table
# whose ROWS had been emptied (MUTATION.md, TARIFF-15 M13). `"132 points" in
# row` is that mistake again: it is satisfied by the number appearing anywhere,
# including in the other number's slot, which habit 14 shows is a ZERO-BYTE
# edit away. Every NUMBER below is asserted as a parsed slot against an integer
# derived here; every other claim is an INEQUALITY between two renderings that
# must not be equal. The ONE substring test is the rung-attribution needle
# (`"the tariff is flat"`, `"carry a published price"`) that TEST-16 already
# owns and grades on - it asks WHICH RUNG spoke, not what this file wrote.
_T11_NOW = datetime(2026, 1, 14, 0, 0, tzinfo=timezone.utc)
_T11_BASE = dict(_T10_BASE, meter_entity="sensor.grid")
class _T11Hass(_T10Hass):
"""`_T10Hass`, with ONE reading switchable to `None`.
⚠️ THE `None` IS THE WHOLE STIMULUS FOR DEFECT 4. `inputs_ok` is False the
moment any pack's SoC does not answer, and `cycle()` then returns at the
input hold (`bad_for < stale_input_s`, 15 s by default) BEFORE it reaches
`self.arb = None`. A fixture whose sensors always answer cannot reach the
row this section is about.
"""
def __init__(self, soc=55.0):
self.soc = soc
async def number(self, entity, invert=False):
return {"sensor.grid": 0.0, "sensor.soc": self.soc,
"sensor.batt": 0.0}.get(entity)
def _t11_points(firm_h: int, carried_h: int, base=None) -> list:
"""`firm_h` QUARTER-HOURLY firm hours, then `carried_h` HOURLY carried ones.
FCST-04's real mixed shape, and the reason a single spacing is the wrong
model: the feed prints four points an hour and §8.3's carry-forward emits
exactly ONE point per unpublished hour, so a normal weekday-morning plan is
900 s at the front and 3600 s at the back. The soak measured that mix in
70 % of samples.
⚠️ THE PRICES ASCEND AND THE TWO BANDS DO NOT OVERLAP, so no check below
can pass by reading the wrong point: a mutation that grades a carried point
as firm moves a price as well as a count.
"""
t0 = base or _T11_NOW
pts = [PricePoint(t0 + timedelta(hours=h, minutes=15 * q),
round(0.100 + 0.001 * (4 * h + q), 5), False)
for h in range(firm_h) for q in range(4)]
pts += [PricePoint(t0 + timedelta(hours=firm_h + h),
round(0.500 + 0.001 * h, 5), True)
for h in range(carried_h)]
return pts
def _t11_plan(firm_h, carried_h, *, origin="provider",
provider="generic_dayahead", product=PriceProduct.DAY_AHEAD):
return TariffPlan(
PriceRead.series(_t11_points(firm_h, carried_h), product, provider,
_T11_NOW), None, origin)
def _t11_want(firm_h: int, carried_h: int) -> dict:
"""What a row about `_t11_points(firm_h, carried_h)` MUST say.
⚠️ COMPUTED FROM THE FIXTURE'S TWO CONSTRUCTION PARAMETERS AND FROM
NOTHING ELSE (habit 4). Not `len(plan.read.points)`, not `plan.hours`, not
`len(plan.firm_points())` - those are the expressions the row is built out
of, and a check that compares a rendered number against the property that
rendered it compares a thing against itself. `4 * firm_h` and
`firm_h + carried_h` are this file's own arithmetic over the shape it
asked for.
The spacing rule, stated rather than re-counted: consecutive quarters are
900 s apart, the last quarter of the last firm hour is 900 s before the
first carried hour, and consecutive carried hours are 3600 s apart - so
3600 appears only when there are at least TWO carried hours.
"""
gaps = ([900] if firm_h else []) + ([3600] if carried_h >= 2 else [])
return {"points": 4 * firm_h + carried_h, "hours": firm_h + carried_h,
"firm": 4 * firm_h, "carried": carried_h,
"spacings": gaps or [3600]}
class _NoRows(dict):
"""What a failed render hands back: a dict of `_Raised`, never a crash.
⚠️ FCST-06's LESSON, IN THE OPERAND RATHER THAN IN THE CONDITION. A
mutation that makes a render raise must report as ONE named red per check,
not as a traceback that takes the rest of the section with it and grades
none of it - a red COUNT is only a lower bound until the run is seen to
reach the end of its section.
"""
def __init__(self, e):
super().__init__()
self.e = e
def __missing__(self, key):
return _Raised(self.e)
class _NoCtl:
"""A controller a failed render never built. Every attribute is `_Raised`."""
def __init__(self, e):
self._e = e
def __getattr__(self, name):
return _Raised(self._e)
def _t11_render(over=None, *, plan=None, plan_age_s=0.0, soc=55.0, cycles=1,
age_arb_by_s=None, then_soc_none=False):
"""Drive the SHIPPED controller and hand back what a human would read.
Returns `(rows, ctl)` - `rows` is `dict(status_payload()["rows"])`, i.e.
the actual HTTP surface, and `ctl` is there so a check can ask the loop for
a figure to grade the row AGAINST (its own `reason`, its own `arb_at`).
⚠️ `read_inputs()` THEN `cycle()`, IN THAT ORDER, because that is what
`run_control()` does. A harness that called only `cycle()` would leave
every reading at `None` and grade an input-hold row it had manufactured
itself.
"""
opts = dict(_T11_BASE, **(over or {}))
tmp = tempfile.mkdtemp(prefix="tariff11-")
_tmpdirs_t10.append(tmp)
async def _go():
stop = asyncio.Event()
ctl = _main.Controller(opts, _T11Hass(soc), _T10Store(), _T10Pub(),
stop=stop,
peak_db_path=os.path.join(tmp, "t11.db"),
price_history_path=os.path.join(tmp, "ph.json"))
if plan is not None:
ctl.tariff_plan = plan
ctl.tariff_at = (datetime.now(timezone.utc)
- timedelta(seconds=plan_age_s))
for _ in range(cycles):
await ctl.read_inputs()
await ctl.cycle()
if then_soc_none:
# ⚠️ THE REAL STIMULUS FOR THE SECOND HALF OF DEFECT 4, and it is
# a control-flow one: a healthy cycle takes a decision, the SoC
# then stops answering, and the NEXT cycle returns at the input
# hold before it reaches the arbitrage step at all. `self.arb`
# still holds the previous decision - the row rendered it as a
# current one, with nothing saying the site had stopped deciding.
ctl.hass.soc = None
await ctl.read_inputs()
await ctl.cycle()
if age_arb_by_s is not None and ctl.arb_at is not None:
# ⚠️ THE CLOCK IS MOVED, NOT SLEPT THROUGH. The property under test
# is "the row reports the age of the decision", and a real 15 s
# wait would put a quarter of a minute into every run for a number
# the check can set exactly.
ctl.arb_at -= timedelta(seconds=age_arb_by_s)
return dict(ctl.status_payload()["rows"]), ctl
try:
return asyncio.run(_go())
except Exception as e: # noqa: BLE001
return _NoRows(e), _NoCtl(e)
# -- 12a. the instrument first: a parser that can tell right from wrong ------
# ⚠️ CALIBRATED BEFORE ANYTHING IS GRADED WITH IT (habit 1, and MUTATION.md's
# eleventh instance of habit 4 was in a measuring instrument rather than in a
# check). A slot parser that returns `None` for everything makes every row
# below vacuous; one that matches a number wherever it appears reads a
# transposition as a pass. So it is driven against a row that is RIGHT and a
# row that is WRONG IN EXACTLY THE WAY THE DEFECT WAS, and both answers are
# asserted.
_T11_SLOTS = {
# UX-08 2d: the row is in plain words; every figure keeps its own slot.
"points": r"(\d+) prices over \d+ hours",
"hours": r"\d+ prices over (\d+) hours",
"firm": r"(\d+) published and \d+ estimated",
"carried": r"\d+ published and (\d+) estimated",
"horizon": r"for (\d+) hours ahead",
"age_s": r"Price plan from (\d+) s ago",
# UX-10: the spacing is in words ("one every 15 minutes or every hour").
"spacings": r"one (every [a-z0-9 ]+?)(?:,|\.|$)",
# ⚠️ THE *Arbitrage* ROW'S OWN AGE SLOT, and it is deliberately the same
# pattern for "decided N s ago" and "the last one was taken N s ago" -
# WHICH of the two sentences was rendered is graded by inequality below,
# and this only has to read the number out of whichever one it is.
"arb_age_s": r"(\d+) s ago",
# ⚠️ THE REFUSAL SENTENCES GET SLOTS TOO, for the rows' reason: `"1 of 96"
# in reason` is satisfied by that pair standing in the HOUR clause, which
# is the transposition the defect actually was.
"r83_firm_pts": r"only (\d+) of \d+ points carry a published price",
"r83_all_pts": r"only \d+ of (\d+) points carry a published price",
"r83_firm_h": r"published price - (\d+) of \d+ hours",
"r83_all_h": r"published price - \d+ of (\d+) hours",
"flat_pts": r"every one of the (\d+) firm points, spanning \d+ hours",
"flat_h": r"every one of the \d+ firm points, spanning (\d+) hours",
}
def _spacing_s(raw):
"""`every 15 minutes or every hour` -> `[900, 3600]`; raises otherwise."""
unit = {"second": 1, "minute": 60, "hour": 3600}
out = []
for part in raw.split(" or "):
m = re.fullmatch(r"every (?:(\d+) )?(second|minute|hour)s?", part.strip())
if m is None:
raise ValueError(f"no spacing in {part!r}")
out.append(int(m.group(1) or 1) * unit[m.group(2)])
return out
def _slot(row, name):
"""The number in ONE NAMED SLOT of a rendered row, or `_Raised`.
⚠️ BY SLOT, NEVER BY PRESENCE. `"33" in row` is true of a row that prints
33 in the POINT slot, which is a zero-byte transposition away and is
exactly MUTATION.md habit 14's finding. A slot that does not match is a
FAILED derivation and reports as one; it is not a miss that passes.
"""
def _get():
if not isinstance(row, str):
raise TypeError(f"no row to read: {row!r}")
m = re.search(_T11_SLOTS[name], row)
if m is None:
raise ValueError(f"no {name!r} slot in {row[:90]!r}")
raw = m.group(1)
return (_spacing_s(raw) if name == "spacings" else int(raw))
return derived(_get)
_T11_GOOD = ("x. Price plan from 7 s ago, for 48 hours ahead: day-ahead "
"prices from the price feed, 132 prices over 33 hours, "
"36 published and 96 estimated, one every 15 minutes or every "
"hour")
_T11_SHIPPED_DEFECT = ("x. Price plan from 7 s ago, for 48 hours ahead: "
"day-ahead prices from the price feed, 132 prices over "
"132 hours, 96 published and 36 estimated, one every "
"15 minutes or every hour")
# The slots divide into the ones a *Tariff*/​*Arbitrage* ROW carries and the
# ones a REFUSAL sentence carries; each group is calibrated against its own
# well-formed example, because a slot asked of the wrong kind of sentence is
# correctly absent and would make this check red for the wrong reason.
_T11_ROW_SLOTS = ("points", "hours", "firm", "carried", "horizon", "age_s",
"spacings", "arb_age_s")
_T11_REF_SLOTS = ("r83_firm_pts", "r83_all_pts", "r83_firm_h", "r83_all_h",
"flat_pts", "flat_h")
_T11_GOOD_REF = ("§8.3: only 1 of 96 points carry a published price - 1 of 24 "
"hours - and the rest are provisional; the tariff is flat - "
"every one of the 96 firm points, spanning 24 hours, costs")
print(f" slot parser on a correct row -> "
f"{ {k: _slot(_T11_GOOD, k) for k in _T11_ROW_SLOTS} }")
print(f" slot parser on a refusal -> "
f"{ {k: _slot(_T11_GOOD_REF, k) for k in _T11_REF_SLOTS} }")
check("the slot parser reads every field of a well-formed row and of a "
"well-formed refusal - at a single unreadable slot every check below "
"this line would be graded on a failed derivation rather than on the "
"sentence",
all(not isinstance(_slot(_T11_GOOD, k), _Raised) for k in _T11_ROW_SLOTS)
and all(not isinstance(_slot(_T11_GOOD_REF, k), _Raised)
for k in _T11_REF_SLOTS)
and sorted(_T11_ROW_SLOTS + _T11_REF_SLOTS) == sorted(_T11_SLOTS)
and _slot(_T11_GOOD, "points") == 132
and _slot(_T11_GOOD, "hours") == 33)
check("...and it TELLS THE TWO DEFECTS APART from the correct row: the shipped "
"point<->hour conflation (132 h for 33 h) and a firm/carried "
"transposition both move a slot, so the parser is not merely finding "
"numbers somewhere in a sentence",
_slot(_T11_SHIPPED_DEFECT, "hours") == 132
and _slot(_T11_SHIPPED_DEFECT, "firm") == 96
and _slot(_T11_SHIPPED_DEFECT, "points")
== _slot(_T11_GOOD, "points"))
check("...and a missing slot is a FAILED derivation, not a silent pass - the "
"difference between 'the row does not say that' and 'the row says "
"nothing', which is why `derived()` exists in this file",
isinstance(_slot("nothing useful here", "points"), _Raised))
# -- 12b. the fixture SET, and the degeneracy it is built to avoid ------------
# ⚠️ ONE ARRANGEMENT CANNOT SEPARATE FIVE NUMBERS (habit 10). On the shipped
# healthy plan - an all-firm quarter-hourly day - `points` and `firm` are the
# SAME INTEGER and `carried` is 0, so a row that printed the point count in the
# firm slot would pass. On an hourly static profile `points` and `hours` are
# the same integer, which is precisely why the defect shipped: every fixture
# that existed before TARIFF-08 was hourly. So the fixtures are a SET, and the
# table below is COMPUTED and PRINTED - a pair of fields no row separates shows
# up as a named survivor and takes this check red, rather than being absent
# from a list nobody re-derived.
_T11_SHAPES = (
("all-firm quarter-hourly, the live healthy plan", 33, 0),
("mixed: 9 published hours, 24 carried - the weekday morning", 9, 24),
("§11.4 fallback: nothing published, 12 hourly carried", 0, 12),
("late morning: 6 published hours, 3 carried", 6, 3),
)
_T11_FIELDS = ("points", "hours", "firm", "carried", "horizon")
_T11_TABLE = [(lbl, dict(_t11_want(f, c), horizon=48))
for lbl, f, c in _T11_SHAPES]
print(" fixture set (points / hours / firm / carried / horizon):")
for _lbl, _w in _T11_TABLE:
print(f" {_w['points']:>4} {_w['hours']:>4} {_w['firm']:>4} "
f"{_w['carried']:>4} {_w['horizon']:>4} {_lbl}")
_T11_UNSEP = sorted(
f"{a}=={b}" for i, a in enumerate(_T11_FIELDS) for b in _T11_FIELDS[i + 1:]
if not any(w[a] != w[b] for _lbl, w in _T11_TABLE))
print(f" field pairs NO fixture separates: {_T11_UNSEP or 'none'}")
check(f"every pair of the five numbers the *Tariff* row prints is told apart "
f"by at least one fixture - unseparated pairs {_T11_UNSEP}. A pair that "
f"is equal in every fixture is one the checks below cannot tell "
f"transposed, which is a ZERO-BYTE edit (habit 14)",
_T11_UNSEP == [])
check("...and the set contains BOTH the degenerate hourly arrangement (points "
"== hours, where the defect was invisible for three cards) and the "
"quarter-hourly one where they differ 4x, so the row is pinned on the "
"shape that hid it as well as on the shape that exposes it",
any(w["points"] == w["hours"] for _l, w in _T11_TABLE)
and any(w["points"] == 4 * w["hours"] for _l, w in _T11_TABLE))
# -- 12c. THE *Tariff* ROW ITSELF ---------------------------------------------
_T11_ROWS = [(lbl, f, c,
derived(lambda f=f, c=c: _t11_render(
{"price_provider": "generic_dayahead"},
plan=_t11_plan(f, c))[0]["Tariff"]))
for lbl, f, c in _T11_SHAPES]
for _lbl, _f, _c, _row in _T11_ROWS:
print(f" Tariff row | {_lbl}")
print(f" {_row}")
_T11_BAD = [f"{lbl}: {k} slot reads {_slot(row, k)!r}, fixture says "
f"{_t11_want(f, c)[k]!r}"
for lbl, f, c, row in _T11_ROWS
for k in ("points", "hours", "firm", "carried", "spacings")
if _slot(row, k) != _t11_want(f, c)[k]]
check(f"AC: EVERY number the rendered *Tariff* row prints matches what this "
f"file computed from the fixture's own two parameters, in its OWN slot, "
f"across all four shapes - disagreements {_T11_BAD}. This is the check "
f"that did not exist: `words` said `132 h` about 33 hours for three "
f"cards and no suite could see it, because no suite read the sentence",
_T11_BAD == [])
check(f"...and the HORIZON reaches the row as hours, from the option rather "
f"than from the plan - "
f"{[_slot(r, 'horizon') for _l, _f, _c, r in _T11_ROWS]} against "
f"`price_horizon_h` = "
f"{price_option(dict(_T11_BASE), 'price_horizon_h')}. The soak measured "
f"48.0 at every sample and `words` never said 48",
all(_slot(r, "horizon")
== int(float(price_option(dict(_T11_BASE), "price_horizon_h")))
for _l, _f, _c, r in _T11_ROWS))
# ⚠️ ORIGIN AND PRODUCT ARE GRADED BY DISAGREEMENT, NOT BY A SUBSTRING. Both
# are enum-ish labels, so `"provider" in row` is satisfied by the word
# "provider" this row already contains twice for other reasons. Two plans over
# the IDENTICAL point list, differing in exactly one of the two fields, must
# render differently - which is false of any row that drops the field.
# ⚠️ AND THE FIXTURE VARIES `origin` **ALONE**. The first draft of this pair
# gave the fallback plan `provider="static_profile"` as well - which is what a
# real §11.4 plan carries - so the two rows differed for TWO reasons and the
# mutation that drops `origin` from the sentence entirely (tag <- read.provider,
# -190 B) was a SURVIVOR of the whole suite. That is MUTATION.md habit 4 in the
# check written to close this card: a difference-claim whose two sides differ in
# more than the thing it names cannot grade that thing. The provider string is
# held EQUAL below, as its own precondition check.
_T11_ID_PTS = (9, 24)
_T11_PLAN_PROV = _t11_plan(*_T11_ID_PTS, origin="provider")
_T11_PLAN_FALL = _t11_plan(*_T11_ID_PTS, origin="fallback")
_T11_AS_PROVIDER = derived(lambda: _t11_render(
{"price_provider": "generic_dayahead"},
plan=_T11_PLAN_PROV)[0]["Tariff"])
_T11_AS_FALLBACK = derived(lambda: _t11_render(
{"price_provider": "generic_dayahead"},
plan=_T11_PLAN_FALL)[0]["Tariff"])
check(f"the origin pair differs in `origin` AND IN NOTHING ELSE (habit 1): "
f"provider strings {_T11_PLAN_PROV.read.provider!r} == "
f"{_T11_PLAN_FALL.read.provider!r}, products equal, point lists equal, "
f"origins {_T11_PLAN_PROV.origin!r} != {_T11_PLAN_FALL.origin!r}. Give "
f"the fallback its own provider string and the row below differs for a "
f"second reason, which is how the -190 B mutation survived the first "
f"draft",
_T11_PLAN_PROV.read.provider == _T11_PLAN_FALL.read.provider
and _T11_PLAN_PROV.product == _T11_PLAN_FALL.product
and _T11_PLAN_PROV.origin != _T11_PLAN_FALL.origin
and [p.canonical() for p in _T11_PLAN_PROV.read.points]
== [p.canonical() for p in _T11_PLAN_FALL.read.points])
_T11_AS_REALTIME = derived(lambda: _t11_render(
{"price_provider": "generic_dayahead"},
plan=_t11_plan(*_T11_ID_PTS, product=PriceProduct.REALTIME))[0]["Tariff"])
print(f" origin='provider' -> {_T11_AS_PROVIDER[-88:]!r}")
print(f" origin='fallback' -> {_T11_AS_FALLBACK[-88:]!r}")
print(f" product=REALTIME -> {_T11_AS_REALTIME[-88:]!r}")
check("AC: the plan's ORIGIN reaches the row - two plans over the IDENTICAL "
"point list, one observed from the feed and one §11.4 standing in for a "
"dead one, render DIFFERENTLY. Every count in them is equal, so a row "
"that dropped `origin` could not tell an operator that the prices on "
"screen are the static profile rather than the market",
_T11_AS_PROVIDER != _T11_AS_FALLBACK
and all(_slot(_T11_AS_PROVIDER, k) == _slot(_T11_AS_FALLBACK, k)
for k in ("points", "hours", "firm", "carried")))
check("...and so does the PRODUCT, on the same terms - §9.2's product is what "
"the optimiser widens its thresholds on, and it was on no surface",
_T11_AS_REALTIME != _T11_AS_PROVIDER
and all(_slot(_T11_AS_REALTIME, k) == _slot(_T11_AS_PROVIDER, k)
for k in ("points", "hours", "firm", "carried")))
# ⚠️ THE TWO TARIFF SOURCES ARE NOT COMPUTED AT THE SAME MOMENT AND THE ROW
# MAY NOT IMPLY THEY WERE. `static_status()` runs on the 30 s I/O-free tick;
# the provider plan comes off the 1800 s refresh and is served for up to
# TARIFF_MAX_AGE_S - 5400 s since TARIFF-12. So the plan half carries its own
# age, and the age is graded against a number this file SET rather than
# against one the row supplied.
_T11_FRESH = derived(lambda: _t11_render(
{"price_provider": "generic_dayahead"},
plan=_t11_plan(9, 24), plan_age_s=0.0)[0]["Tariff"])
_T11_OLD = derived(lambda: _t11_render(
{"price_provider": "generic_dayahead"},
plan=_t11_plan(9, 24), plan_age_s=4000.0)[0]["Tariff"])
print(f" plan 0 s old -> age slot {_slot(_T11_FRESH, 'age_s')!r}")
print(f" plan 4000 s old -> age slot {_slot(_T11_OLD, 'age_s')!r}")
check(f"AC/trap: the row does NOT present the 30 s tick's sentence and a "
f"4000-second-old plan as one measurement - the plan half is stamped "
f"with its own age, and the age MOVES with `tariff_at`: "
f"{_slot(_T11_FRESH, 'age_s')} s against {_slot(_T11_OLD, 'age_s')} s "
f"for the SAME plan object. A constant, or an age taken from the render "
f"clock, gives the same number twice",
isinstance(_slot(_T11_OLD, "age_s"), int)
and 4000 <= _slot(_T11_OLD, "age_s") <= 4010
and _slot(_T11_FRESH, "age_s") <= 5)
check(f"...and the two halves are still BOTH there, in order: the tick's "
f"static sentence for this provider-only site - "
f"{static_status(dict(_T11_BASE), _T11_NOW)[1]!r} - leads, and the plan "
f"follows it. Replacing the row wholesale would lose the cheap-window "
f"evidence TARIFF-03 put there",
_T11_FRESH.startswith(static_status(dict(_T11_BASE), _T11_NOW)[1]))
# ⚠️ AND THE STATE THE SOAK ACTUALLY MEASURED: a provider configured, and no
# plan. 802 samples of `'no static profile'`, which is the row a site with no
# provider prints too - so the surface could not tell "trading on a live feed"
# from "no tariff machinery at all".
_T11_NOPLAN = derived(lambda: _t11_render(
{"price_provider": "generic_dayahead"}, plan=None)[0]["Tariff"])
_T11_NOPROV = derived(lambda: _t11_render({}, plan=None)[0]["Tariff"])
print(f" provider set, no plan -> {_T11_NOPLAN!r}")
print(f" no provider at all -> {_T11_NOPROV!r}")
check(f"AC: a site with a `price_provider` and NO usable plan says so, and "
f"reads differently from a site that configured no provider at all. "
f"Both printed {_T11_NOPROV!r} before this card - all 802 soak samples, "
f"on an install whose feed was answering",
_T11_NOPLAN != _T11_NOPROV
and str(int(_main.TARIFF_MAX_AGE_S)) in _T11_NOPLAN)
# -- 12d. THE *Arbitrage* ROW: THREE FACTS THAT RENDERED AS TWO ---------------
# ⚠️ THE STIMULUS IS A DRIVEN CYCLE WITH A SENSOR ANSWERING `None`, not a
# hand-set attribute. The defect is a control-flow one - `cycle()` returns at
# the input hold BEFORE `self.arb = None` - and a fixture that assigned
# `ctl.arb = None` itself would have reproduced the symptom without ever
# touching the path that causes it.
_T11_OFF = derived(lambda: _t11_render({}, soc=None))
_T11_ON_HELD = derived(lambda: _t11_render({"arbitrage_enabled": True},
soc=None))
_T11_ON_OK = derived(lambda: _t11_render({"arbitrage_enabled": True},
soc=55.0))
_T11_ON_STALE = derived(lambda: _t11_render({"arbitrage_enabled": True},
soc=55.0, age_arb_by_s=13))
_T11_ON_DROPPED = derived(lambda: _t11_render({"arbitrage_enabled": True},
soc=55.0, then_soc_none=True))
print(f" trading off, inputs missing -> {_T11_OFF[0]['Arbitrage']!r}")
print(f" trading ON, inputs missing -> {_T11_ON_HELD[0]['Arbitrage']!r}")
print(f" trading ON, inputs fine -> {_T11_ON_OK[0]['Arbitrage']!r}")
print(f" trading ON, decision 13 s old-> {_T11_ON_STALE[0]['Arbitrage']!r}")
print(f" trading ON, SoC then dropped -> {_T11_ON_DROPPED[0]['Arbitrage']!r}")
check(f"the stimulus is the one the card names, and it is asserted rather "
f"than assumed (habit 1): `arbitrage.enabled(opts)` is "
f"{_arb.enabled(_T11_ON_HELD[1].o)} while `controller.arb` is "
f"{_T11_ON_HELD[1].arb!r}. If a later edit makes `cycle()` reach the "
f"arbitrage step under an input hold, this goes red rather than the "
f"rows below going quietly vacuous",
_arb.enabled(_T11_ON_HELD[1].o) is True
and _T11_ON_HELD[1].arb is None
and not _T11_ON_HELD[1].inputs_ok)
check(f"AC, AND THE WHOLE OF DEFECT 4: 'trading is off' and 'trading is on "
f"and no decision was reached this cycle' RENDER DIFFERENTLY. They are "
f"different facts with different remedies and this row printed "
f"`off (arbitrage_enabled)` for both - naming an option that is `true` "
f"in the options blob",
_T11_OFF[0]["Arbitrage"] != _T11_ON_HELD[0]["Arbitrage"])
# ⚠️ THE "Why" ROW OF THE SAME RENDER, NOT A SECOND READ OF `why_words`. Since
# UX-10 the waiting sentence counts its seconds when it is read, so reading the
# property again here, after the other renders above, could cross a second
# ("1 second" in the row, "2 seconds" now) - CI run 953 on PR #141 did.
check(f"...and the armed-but-held row carries the CONTROL LOOP'S OWN REASON "
f"({_T11_ON_HELD[0]['Why']!r}), so the operator is told which input is "
f"missing rather than being sent to look at a switch that is already on. "
f"The reason comes from main.py, not from this file",
_T11_ON_HELD[0]["Why"].startswith("A reading is missing")
and _T11_ON_HELD[0]["Why"] in _T11_ON_HELD[0]["Arbitrage"])
check(f"...and a cycle that DID reach the step renders the strategy's own "
f"sentence - {_T11_ON_OK[1].arb.explain()[:50]!r} - rather than a "
f"re-derivation of it, which is the guarantee OPT-04 put on this row and "
f"the one thing about it that was already right",
_T11_ON_OK[1].arb is not None
and _T11_ON_OK[1].arb.explain() in _T11_ON_OK[0]["Arbitrage"]
and _T11_ON_OK[0]["Arbitrage"] != _T11_ON_HELD[0]["Arbitrage"])
check(f"...and all THREE states are pairwise distinct, so no pair of them can "
f"be collapsed by a later edit without a named red here",
len({_T11_OFF[0]["Arbitrage"], _T11_ON_HELD[0]["Arbitrage"],
_T11_ON_OK[0]["Arbitrage"]}) == 3)
_T11_AGE = _slot(_T11_ON_STALE[0]["Arbitrage"], "arb_age_s")
check(f"AC: a decision the CURRENT cycle did not take is rendered as one - "
f"age slot {_T11_AGE} s against the 13 s this file moved `arb_at` back "
f"by. The card's second half: during an input hold the row went on "
f"showing the last decision, up to `stale_input_s` (15 s) stale, with "
f"nothing on the page saying so",
isinstance(_T11_AGE, int) and 13 <= _T11_AGE <= 18
and _T11_ON_STALE[0]["Arbitrage"] != _T11_ON_OK[0]["Arbitrage"])
# ⚠️ AND THE SAME CLAIM ON THE STIMULUS THAT ACTUALLY PRODUCES IT. The row
# above moves `arb_at` by hand so the AGE can be graded against an exact
# number; this one drives the control-flow path itself - one healthy cycle,
# then a sensor that stops answering - and asserts that the shipped controller
# reaches the state the card describes, rather than that this file can simulate
# it. Without this row a mutation that never sets `cycle_at` would still pass,
# because the hand-aged fixture would go on rendering `stale`.
# ⚠️ `derived()`, NOT A BARE COMPARISON, AND THE BATTERY IS WHY. The mutation
# that never stamps `cycle_at` leaves it at `__init__`'s None, and
# `datetime < None` is a TypeError - in the f-string OPERAND as much as in the
# condition. Driven, the first draft of this check produced a TRACEBACK and
# NO NAMED RED, grading none of the checks after it: FCST-06's finding, in the
# check written for this card. "It answered nothing" and "it broke on the way
# to answering" must not grade the same.
_T11_DROP_STALE = derived(
lambda: _T11_ON_DROPPED[1].arb_at < _T11_ON_DROPPED[1].cycle_at)
check(f"DRIVEN, no clock touched: a decision stands "
f"({_T11_ON_DROPPED[1].arb is not None}) while the LATEST cycle returned "
f"at the input hold without reaching the arbitrage step "
f"(arb_at < cycle_at = {_T11_DROP_STALE!r}), and the row says so instead "
f"of presenting the standing decision as this cycle's",
_T11_ON_DROPPED[1].arb is not None
and not _T11_ON_DROPPED[1].inputs_ok
and _T11_DROP_STALE is True
and _T11_ON_DROPPED[0]["Arbitrage"] != _T11_ON_OK[0]["Arbitrage"])
check(f"...and it names the reason the cycle stopped "
f"({_T11_ON_DROPPED[1].why_words!r}) beside the age of the decision it is "
f"still showing, so 'the strategy decided this' and 'the strategy last "
f"decided this' are not one sentence",
_T11_ON_DROPPED[1].why_words in _T11_ON_DROPPED[0]["Arbitrage"]
and isinstance(_slot(_T11_ON_DROPPED[0]["Arbitrage"], "arb_age_s"), int))
check("...and `main.py`'s 'if and only if' is graded as BEHAVIOUR rather than "
"as prose: a comment is not a check, and three findings in this chain "
"were graded by a sentence their author also wrote. What makes the "
"comment true is that the row reads the OPTION - so with the option on "
"and no decision taken, the row does not equal the option-off row, which "
"is the row above",
_arb.enabled(_T11_ON_HELD[1].o)
and _T11_ON_HELD[1].arb is None
and _T11_ON_HELD[0]["Arbitrage"] != _T11_OFF[0]["Arbitrage"])
# -- 12e. the two REFUSAL sentences, which reach this same row ----------------
# ⚠️ THESE ARE OPERATOR-FACING THROUGH `self.arb.explain()`, WHICH IS WHY THEY
# ARE HERE AND NOT ONLY IN test_static_tariff.py. `§8.3: only 1 of 96 hours
# carry a published price` is 96 POINTS, which is 24 hours - the same
# conflation TARIFF-14 removed from `at()`, left standing in the prose that
# describes it.
_T11_A9 = {"peak_guard_active": False, "min_spread_eur_kwh": 0.0,
"uncertainty": frozenset()}
_T11_ONEFIRM = TariffPlan(
PriceRead.series(
[PricePoint(_T11_NOW + timedelta(minutes=15 * i),
round(0.10 + 0.001 * i, 5), i != 0) for i in range(96)],
PriceProduct.DAY_AHEAD, "generic_dayahead", _T11_NOW), None, "provider")
_T11_FLAT = TariffPlan(
PriceRead.series(
[PricePoint(_T11_NOW + timedelta(minutes=15 * i), 0.10, False)
for i in range(96)],
PriceProduct.DAY_AHEAD, "generic_dayahead", _T11_NOW), None, "provider")
_r83 = derived(lambda: _T11_ONEFIRM.arbitrage(**_T11_A9).reason)
_rflat = derived(lambda: _T11_FLAT.arbitrage(**_T11_A9).reason)
print(f" §8.3 refusal : {_r83[:118]}")
print(f" flat refusal : {_rflat[:118]}")
# 96 quarter-hourly points span 24 hours; ONE firm point spans one of them.
# Both figures are this file's arithmetic over the fixture it built.
# The fixture is 96 points at 15-minute spacing, built four lines above, so it
# spans 96 // 4 = 24 hours and its single firm point occupies ONE of them.
# Both expectations are this file's arithmetic over the shape it asked for.
_T11_R83 = {k: _slot(_r83, k) for k in
("r83_firm_pts", "r83_all_pts", "r83_firm_h", "r83_all_h")}
print(f" §8.3 slots : {_T11_R83}")
print(f" flat slots : "
f"{ {k: _slot(_rflat, k) for k in ('flat_pts', 'flat_h')} }")
check(f"AC: §8.3's refusal reports POINTS and HOURS as two quantities, each in "
f"its OWN slot - {_T11_R83} over a fixture of 96 quarter-hourly points "
f"spanning 24 hours, of which one is firm. It read `only 1 of 96 hours` "
f"about a 24-hour day, and this sentence reaches the status page",
_T11_R83["r83_firm_pts"] == 1 and _T11_R83["r83_all_pts"] == 96
and _T11_R83["r83_firm_h"] == 1 and _T11_R83["r83_all_h"] == 96 // 4)
check(f"...and the two clauses are not the same pair twice: the POINT clause "
f"and the HOUR clause disagree, which is exactly what the shipped "
f"sentence could not express",
_T11_R83["r83_all_pts"] != _T11_R83["r83_all_h"])
check(f"...and the flat-tariff refusal does the same - it counted the same 96 "
f"points and called them `firm hours`: slots "
f"{_slot(_rflat, 'flat_pts')} points, {_slot(_rflat, 'flat_h')} hours",
_slot(_rflat, "flat_pts") == 96
and _slot(_rflat, "flat_h") == 96 // 4)
check("...and neither sentence lost what TEST-16 put in it: §8.3's refusal is "
"still attributable to the FIRM-PAIR rung by its own words, and does not "
"degrade into the flat rung's sentence about a series whose prices "
"differ by 0.095 EUR/kWh",
"§8.3" in _r83 and "carry a published price" in _r83
and "the tariff is flat" not in _r83
and "the tariff is flat" in _rflat)
# -- 12f. END TO END, against the COMMITTED capture --------------------------
# ⚠️ EVERY FIXTURE ABOVE IS SYNTHETIC, AND habit 19 IS WHY THIS ROW EXISTS.
# The shapes above are this file's own arithmetic; they cannot tell whether the
# SHIPPED refresh puts a real plan on the row. This drives `run_tariff_refresh`
# against `scenarios/energy-charts-be-price-*.json` - bytes captured from
# api.energy-charts.info - and grades the row against the CAPTURE'S OWN point
# count, which neither main.py nor static_tariff.py shaped.
_T11_N_CAP = len(_CAPTURE["price"])
def _t11_e2e():
tmp = tempfile.mkdtemp(prefix="tariff11-e2e-")
_tmpdirs_t10.append(tmp)
opts = dict(_T11_BASE, price_provider="generic_dayahead")
async def _get(url, timeout_s):
return _restamped_body(datetime.now(timezone.utc))
real = _price.https_get
_price.https_get = _get
try:
async def _go():
stop = asyncio.Event()
ctl = _main.Controller(opts, _T11Hass(), _T10Store(), _T10Pub(),
stop=stop,
peak_db_path=os.path.join(tmp, "e.db"),
price_history_path=os.path.join(tmp, "p.json"))
task = asyncio.create_task(ctl.run_tariff_refresh())
for _ in range(250): # ~5 s ceiling, habit 5
if ctl.tariff_at is not None:
break
await asyncio.sleep(0.02)
ctl.stopping = True
stop.set()
await asyncio.wait_for(task, timeout=5)
await ctl.read_inputs()
await ctl.cycle()
return dict(ctl.status_payload()["rows"])
return asyncio.run(_go())
finally:
_price.https_get = real
_T11_E2E = derived(_t11_e2e)
_T11_E2E_ROW = "" if isinstance(_T11_E2E, _Raised) else _T11_E2E["Tariff"]
print(f" end to end, {_T11_N_CAP} captured prices at 900 s:")
print(f" {_T11_E2E_ROW}")
check(f"AC, DRIVEN THROUGH THE SHIPPED REFRESH: the *Tariff* row reports the "
f"{_T11_N_CAP} points the COMMITTED capture publishes, over "
f"{_T11_N_CAP // 4} hours, on a quarter-hourly feed. Both numbers come "
f"from the captured document's own length - not from the plan the code "
f"under test built out of it (habit 19)",
_slot(_T11_E2E_ROW, "points") == _T11_N_CAP
and _slot(_T11_E2E_ROW, "hours") == _T11_N_CAP // 4
and _slot(_T11_E2E_ROW, "points") != _slot(_T11_E2E_ROW, "hours"))
check(f"...and the row an operator reads names the SPACING that makes those "
f"two numbers differ - {_slot(_T11_E2E_ROW, 'spacings')!r} s. The plan "
f"carried mixed 900/3600 s for 70 % of a 40-hour soak and no surface "
f"said so",
_slot(_T11_E2E_ROW, "spacings") == [900])
check("...and this is the same wording FCST-04's *Price history* row already "
"used, so the two rows describing the same feed can be compared rather "
"than being two conventions on one page",
"prices over" in _T11_E2E_ROW
and "prices over" in ("" if isinstance(_T11_E2E, _Raised)
else _T11_E2E["Price history"]))
# --------------------------------------------------------------------------- #
print()
print("16. TARIFF-17 - the three buckets, and the only one that moves a "
"DECISION")
# ⚠️ WHAT THIS SECTION IS FOR, AND WHY A SPREAD ASSERTION WOULD GRADE THE WRONG
# THING. The optimiser reads exactly two quantities off a series: the SPREAD
# (`max - min` over the firm points) and which hour is cheapest
# (`min(firm, key=...)`, static_tariff.arbitrage()). Every price component
# reaches them through one of three buckets:
#
# LEVEL additive, identical every hour -> reaches NEITHER, provably. An
# additive constant cancels
# exactly in `max - min`, and
# adding it to every point leaves
# the argmin where it was.
# SPREAD multiplicative on spot -> scales the spread against a
# fixed cycle_cost_threshold_eur.
# ORDER varies by hour, block, day-type -> changes WHICH HOUR IS CHEAPEST.
#
# `price_taxes_eur_kwh` is LEVEL. It is therefore NOT a wrong number and never
# was: it cannot distort either quantity. The defect TARIFF-17 closes is that a
# component belonging in the ORDER bucket - the Walloon and Brussels DSOs'
# day/night distribution bands - had nowhere to live.
#
# ⚠️ SO THE REGRESSION BELOW GRADES THE ARGMIN, NOT THE MAGNITUDE. A
# spread-magnitude check passes on an implementation that reads the table ONCE
# and adds the same number to every point - which is the LEVEL bucket wearing a
# period table, and is the single likeliest way this feature gets broken. 16d
# is that mutation's named red.
_T17_UTC0 = datetime(2026, 1, 14, 23, 0, tzinfo=UTC) # 00:00 Brussels, Thu
# A plain Belgian winter weekday: solar-flattened midday, evening peak. Nothing
# here is chosen to make the table win - 16b proves no SCALAR can reproduce it.
_T17_SPOT = (0.070, 0.065, 0.062, 0.060, 0.061, 0.068,
0.082, 0.110, 0.125, 0.100, 0.070, 0.050,
0.038, 0.030, 0.035, 0.048, 0.072, 0.120,
0.160, 0.180, 0.150, 0.115, 0.090, 0.078)
_T17_RAW = [RawPoint(_T17_UTC0 + timedelta(hours=h), _T17_SPOT[h], False)
for h in range(24)]
_T17_CAPS = ProviderCapabilities(product=PriceProduct.DAY_AHEAD,
sources=frozenset({Component.ENERGY}),
egress_hosts=("example.invalid",))
# ORES's 2026 residential distribution row, EUR/kWh. The tariff card prints
# `Enkelvoudig | Dag | Nacht | Uitsl. nacht` for every Walloon DSO and
# `Normaal | Uitsl. nacht` for Fluvius, which is why Flanders is a scalar and
# Wallonia is not. Exclusive night is a separate physical circuit and is not
# modelled here.
_T17_ENKELVOUDIG, _T17_DAG, _T17_NACHT = 0.1198, 0.1327, 0.0739
_T17_TABLE_JSON = {"periods": [
{"name": "dag", "from": "07:00", "to": "22:00", "days": "mon-fri",
"eur_per_kwh": _T17_DAG},
{"name": "nacht", "from": "22:00", "to": "07:00", "days": "mon-fri",
"eur_per_kwh": _T17_NACHT},
{"name": "nacht", "from": "00:00", "to": "24:00", "days": "sat,sun",
"eur_per_kwh": _T17_NACHT}]}
_T17_TABLE = derived(lambda: parse_taxes_profile(_T17_TABLE_JSON))
def _t17_band(weekday, hour):
"""ORES's band, written HERE, by different arithmetic than the module uses.
The check that grades `taxes_at()` must not get its expected value from
`taxes_at()`, or it compares a number against itself - MUTATION.md habit 2.
"""
return _T17_DAG if (weekday < 5 and 7 <= hour < 22) else _T17_NACHT
def _t17_cheapest(points):
p = min(points, key=lambda q: q.eur_per_kwh)
return brussels(p.start).hour
def _t17_spread(points):
return round(max(p.eur_per_kwh for p in points)
- min(p.eur_per_kwh for p in points), 6)
print()
print("16a. the fixture is not degenerate, and the two arms differ in EXACTLY "
"ONE way - asserted before anything is concluded from them")
# ⚠️ MUTATION.md habit 1 and habit 7. Three of the checks in this chain turned
# out to be graded by something they did not own, and a fourth survived a whole
# suite because its fixture differed in two ways at once. So: the two arms are
# ONE `assemble()` call each, over the SAME caps object, the SAME RawPoint list
# and the SAME markup, and the only argument that differs is `taxes`.
_T17_FLAT_OUT = derived(lambda: assemble(_T17_CAPS, _T17_RAW, MARKUP,
_T17_ENKELVOUDIG))
_T17_TAB_OUT = derived(lambda: assemble(_T17_CAPS, _T17_RAW, MARKUP,
_T17_TABLE))
check("the table parses, through static_tariff's OWN grammar",
not isinstance(_T17_TABLE, _Raised))
# Graded on this file's own two calls: the arms may differ only in `taxes`.
_T17_ARMS = {t.id: [ast.unparse(a) for a in n.value.args[0].body.args]
for n in ast.parse(open(__file__, encoding="utf-8").read()).body
if isinstance(n, ast.Assign) and isinstance(n.value, ast.Call)
and isinstance(n.value.func, ast.Name)
and n.value.func.id == "derived" and n.value.args
and isinstance(n.value.args[0], ast.Lambda)
and isinstance(n.value.args[0].body, ast.Call)
for t in n.targets if isinstance(t, ast.Name)
and t.id in ("_T17_FLAT_OUT", "_T17_TAB_OUT")}
print(f" arms: {_T17_ARMS}")
check(f"the two arms are the same feed: same caps, same {len(_T17_RAW)} "
f"RawPoints and the same markup {MARKUP} - the two `assemble()` calls "
f"share their first three arguments by NAME and differ only in the fourth",
sorted(_T17_ARMS) == ["_T17_FLAT_OUT", "_T17_TAB_OUT"]
and [len(v) for v in _T17_ARMS.values()] == [4, 4]
and _T17_ARMS["_T17_FLAT_OUT"][:3] == _T17_ARMS["_T17_TAB_OUT"][:3]
== ["_T17_CAPS", "_T17_RAW", "MARKUP"]
and _T17_ARMS["_T17_FLAT_OUT"][3] != _T17_ARMS["_T17_TAB_OUT"][3])
_T17_BANDS = derived(lambda: sorted({p.eur_per_kwh
for p in _T17_TABLE.periods}))
check(f"...and the table is genuinely hour-varying: {_T17_BANDS} EUR/kWh, two "
f"distinct bands {round(_T17_DAG - _T17_NACHT, 4)} apart. A one-band "
f"table is a scalar and would grade nothing",
_T17_BANDS == sorted({_T17_DAG, _T17_NACHT}) and len(_T17_BANDS) == 2)
_T17_RAW_MIN = derived(lambda: min(range(24), key=lambda h: _T17_SPOT[h]))
check(f"...and the UNTAXED feed's own cheapest hour is {_T17_RAW_MIN:02d}:00, "
f"which is inside the DAY band - so the night band has something to win "
f"against. A feed already cheapest at night would pass under any "
f"implementation",
_T17_RAW_MIN == 13 and _t17_band(3, _T17_RAW_MIN) == _T17_DAG)
check(f"...and the flat scalar this is measured against is ORES's OWN "
f"enkelvoudig rate {_T17_ENKELVOUDIG}, which sits BETWEEN the two bands "
f"- so 'the table is just bigger' is not available as an explanation",
_T17_NACHT < _T17_ENKELVOUDIG < _T17_DAG)
print()
print("16b. the LEVEL bucket, as a FAMILY: NO scalar can do what the table "
"does")
# ⚠️ THIS IS WHAT MAKES 16c's CONCLUSION ATTRIBUTABLE. A single flat/table pair
# differs in two things at once - the arithmetic level AND the shape - and
# MUTATION.md's eleven instances are all a fixture that could not separate
# them. So the flat arm is not one scalar, it is the whole FAMILY of scalars
# swept across and beyond the table's own range: if every one of them leaves
# the cheapest hour and the spread exactly where the untaxed feed had them,
# then whatever 16c measures cannot be a level effect, because level effects
# have been shown to be inert on this feed.
_T17_SWEEP = (0.0, _T17_NACHT, _T17_ENKELVOUDIG, _T17_DAG, 1.0,
PRICE_OPTION_DEFAULTS["price_taxes_eur_kwh"])
_T17_LEVEL = derived(lambda: [
(s, _t17_cheapest(assemble(_T17_CAPS, _T17_RAW, MARKUP, s)),
_t17_spread(assemble(_T17_CAPS, _T17_RAW, MARKUP, s)))
for s in _T17_SWEEP])
for _s, _h, _sp in (_T17_LEVEL if isinstance(_T17_LEVEL, list) else []):
print(f" flat taxes {_s:<7.4f} -> cheapest hour {_h:02d}, "
f"spread {_sp:.4f}")
check(f"every one of the {len(_T17_SWEEP)} scalars leaves the cheapest hour at "
f"{_T17_RAW_MIN:02d}:00 - the untaxed feed's own - so a LEVEL component "
f"provably cannot reach the argmin",
isinstance(_T17_LEVEL, list)
and {h for _s, h, _sp in _T17_LEVEL} == {_T17_RAW_MIN})
check("...and leaves the spread IDENTICAL to the last digit, which is the "
"other half of the same statement: an additive constant cancels in "
"`max - min`",
isinstance(_T17_LEVEL, list)
and len({sp for _s, _h, sp in _T17_LEVEL}) == 1)
check("...so `price_taxes_eur_kwh` was never a wrong number. It is inert on "
"both quantities the optimiser reads, and the defect TARIFF-17 closes is "
"a MISSING CAPABILITY rather than a bad constant",
isinstance(_T17_LEVEL, list)
and _T17_LEVEL[0][1:] == _T17_LEVEL[-1][1:])
print()
print("16c. THE REGRESSION: a Walloon bihoraire profile moves WHICH HOUR IS "
"CHEAPEST")
_T17_H_FLAT = derived(lambda: _t17_cheapest(_T17_FLAT_OUT))
_T17_H_TAB = derived(lambda: _t17_cheapest(_T17_TAB_OUT))
print(f" flat {_T17_ENKELVOUDIG} : cheapest hour {_T17_H_FLAT}, "
f"spread {_t17_spread(_T17_FLAT_OUT):.4f}")
print(f" ORES dag/nacht : cheapest hour {_T17_H_TAB}, "
f"spread {_t17_spread(_T17_TAB_OUT):.4f}")
check(f"the SAME feed is cheapest at {_T17_H_FLAT:02d}:00 under a flat scalar "
f"and at {_T17_H_TAB:02d}:00 under ORES's dag/nacht table. THIS is the "
f"property: a component in the ORDER bucket changes the hour a battery "
f"would buy in, and no scalar can",
_T17_H_FLAT == 13 and _T17_H_TAB == 3 and _T17_H_FLAT != _T17_H_TAB)
check(f"...and the hour it moves TO is a night-band hour, which is the "
f"mechanism rather than a coincidence: cheap network hours coincide with "
f"cheap spot hours, so flattening them understates the spread "
f"({_t17_spread(_T17_FLAT_OUT):.4f} against {_t17_spread(_T17_TAB_OUT):.4f} "
f"EUR/kWh) and declines cycles that would have paid",
_t17_band(3, _T17_H_TAB) == _T17_NACHT
and _t17_spread(_T17_TAB_OUT) > _t17_spread(_T17_FLAT_OUT))
# ⚠️ AND THE SPREAD CLAUSE ABOVE IS A REPORTED CONSEQUENCE, NOT THE CHECK. The
# card is explicit that a spread-magnitude assertion grades the wrong bucket
# and would pass on a LEVEL-bucket implementation; the argmin above is what
# goes red for one. Both are printed so a reader can see which is which.
print()
print("16d. driven through the SHIPPED DECISION, not through assemble()'s "
"output")
# ⚠️ habit 4. `min(firm, key=...)` lives in static_tariff.arbitrage(), and that
# is the expression a strategy reads. A check that stops at the assembled list
# grades the list; this one grades the verdict an optimiser is handed.
def _t17_verdict(points):
read = PriceRead.series(points, PriceProduct.DAY_AHEAD, "t17",
datetime.now(UTC))
plan = TariffPlan(read, None, "provider")
return plan.arbitrage(peak_guard_active=False,
min_spread_eur_kwh=PRICE_OPTION_DEFAULTS[
"cycle_cost_threshold_eur"],
uncertainty=frozenset())
_T17_V_FLAT = derived(lambda: _t17_verdict(_T17_FLAT_OUT))
_T17_V_TAB = derived(lambda: _t17_verdict(_T17_TAB_OUT))
_T17_BUY = derived(lambda: (
_T17_V_FLAT.match(allowed=lambda a: brussels(a.cheapest.start).hour,
refused=lambda r: r.reason),
_T17_V_TAB.match(allowed=lambda a: brussels(a.cheapest.start).hour,
refused=lambda r: r.reason)))
print(f" arbitrage().cheapest hour: flat {_T17_BUY[0]!r}, "
f"table {_T17_BUY[1]!r}")
check("BOTH arms are ALLOWED at the shipped cycle_cost_threshold_eur, so the "
"comparison below is between two verdicts and not between a verdict and "
"a refusal",
isinstance(_T17_BUY, tuple) and _T17_BUY[0] == 13 and _T17_BUY[1] == 3)
check("...and the BUY HOUR the optimiser is handed moves 13:00 -> 03:00 on the "
"same feed. The add-on would charge the battery at midday on a flat tax "
"basis and at 03:00 on ORES's real one",
isinstance(_T17_BUY, tuple) and _T17_BUY[0] != _T17_BUY[1])
print()
print("16e. assemble() evaluates the table at EACH POINT'S OWN INSTANT")
# ⚠️ THE MUTATION THIS IS AIMED AT is hoisting the lookup out of the loop -
# reading the table once and adding one number to every point. That leaves the
# series the right MAGNITUDE and the wrong SHAPE, which is exactly the LEVEL
# bucket, and 16c goes red for it. This check says the same thing per point and
# against `_t17_band()` - arithmetic written in this file, not borrowed from
# the module under test.
_T17_DELTAS = derived(lambda: [
(brussels(p.start).hour,
round(p.eur_per_kwh - _T17_SPOT[brussels(p.start).hour] - MARKUP, 6))
for p in _T17_TAB_OUT])
_T17_WANT = derived(lambda: [(h, round(_t17_band(3, h), 6)) for h in range(24)])
check(f"the tax component added to each of the 24 points is that point's own "
f"band, hour by hour, graded against this file's arithmetic: "
f"{_T17_DELTAS[:4]} ... {_T17_DELTAS[-2:]}",
_T17_DELTAS == _T17_WANT)
check(f"...and there really are TWO distinct deltas in one series "
f"{sorted({d for _h, d in _T17_DELTAS})}, so 'evaluated per point' is "
f"distinguishable from 'evaluated once'. Under a scalar there is exactly "
f"one, which is the whole difference between the buckets",
len({d for _h, d in _T17_DELTAS}) == 2
and len({round(p.eur_per_kwh - _T17_SPOT[brussels(p.start).hour]
- MARKUP, 6) for p in _T17_FLAT_OUT}) == 1)
print()
print("16f. the SCALAR path is unchanged - arithmetically, not merely in "
"intent")
# ⚠️ AC1, and the reference site is on it. `taxes_at()` on a float returns that
# float, and `0.0 + markup` is exactly `markup` in IEEE 754 for every finite
# markup - so the pre-TARIFF-17 expression is reproduced here and compared
# point for point rather than reasoned about.
def _t17_old_assemble(caps, raw, markup, taxes):
"""price.py's assemble() as it stood at c518007. Byte-for-byte arithmetic."""
add = 0.0
if Component.MARKUP in caps.adds:
add += float(markup)
if Component.TAXES in caps.adds:
add += float(taxes)
return [PricePoint(start=brussels(p.start),
eur_per_kwh=round(p.eur_per_kwh + add, 6),
provisional=bool(p.provisional))
for p in raw]
_T17_SCALARS = []
for _caps in (_T17_CAPS,
ProviderCapabilities(product=PriceProduct.DAY_AHEAD,
sources=frozenset({Component.ENERGY,
Component.MARKUP}),
egress_hosts=("example.invalid",)),
ProviderCapabilities(product=PriceProduct.STATIC, sources=ALL_IN)):
for _t in (0.0, TAXES, _T17_ENKELVOUDIG, -0.02):
_T17_SCALARS.append(
([p.eur_per_kwh for p in assemble(_caps, _T17_RAW, MARKUP, _t)],
[p.eur_per_kwh
for p in _t17_old_assemble(_caps, _T17_RAW, MARKUP, _t)]))
check(f"over {len(_T17_SCALARS)} (capability, scalar) combinations x "
f"{len(_T17_RAW)} points, every assembled price equals the OLD "
f"expression's to the last digit - `==`, not `near()`",
all(a == b for a, b in _T17_SCALARS))
check("...and the combinations are not all the same answer, or the line above "
"would be one comparison repeated",
len({tuple(a) for a, _b in _T17_SCALARS}) > 1)
print()
print("16g. the new key is FRAMEWORK-OWNED, and a plugin that names it is "
"still refused")
check("`price_taxes_profile` is in FRAMEWORK_KEYS",
"price_taxes_profile" in _price.FRAMEWORK_KEYS)
check("...and BANNED_IN_PLUGIN_CODE is DERIVED from that set, so it arrived "
"there without a second list being edited",
"price_taxes_profile" in BANNED_IN_PLUGIN_CODE
and set(_price.FRAMEWORK_KEYS) <= set(BANNED_IN_PLUGIN_CODE))
check("...so PluginOptions refuses to hand it over, naming the framework",
raises(lambda: PluginOptions(opts()).get("price_taxes_profile"),
FrameworkOption)
and "framework" in _reason(
lambda: PluginOptions(opts()).get("price_taxes_profile")))
class _T17Rogue(PriceProvider):
"""A plugin that reaches for the taxes TABLE instead of the scalar.
The shape of the defect: it would add the site's own per-hour levies a
SECOND time, per hour, and the resulting price is wrong by one tax band
with nothing in any log reporting it.
"""
id = "t17_rogue"
@classmethod
def capabilities(cls):
return ProviderCapabilities(product=PriceProduct.DAY_AHEAD,
sources=frozenset({Component.ENERGY}),
egress_hosts=("example.invalid",))
async def fetch(self, opts, since, until, transport):
extra = opts.get("price_taxes_profile") # the leak
return [RawPoint(since, 0.1 + float(bool(extra)), False)]
_T17_LEAK = derived(lambda: code_leaks(_T17Rogue))
check(f"code_leaks() finds it in a plugin's own code: {_T17_LEAK}",
_T17_LEAK == ["price_taxes_profile"])
check("...and register() refuses to install it, so this is an inability and "
"not a convention",
raises(lambda: register(_T17Rogue), Exception)
and "price_taxes_profile" in _reason(lambda: register(_T17Rogue)))
check("...and the docstring above, which says 'taxes' and 'price' in prose, is "
"NOT what made it fire - docstrings are stripped before the scan",
"price_taxes_profile" not in
"".join(_T17Rogue.__doc__ or "") + (_T17Rogue.capabilities.__doc__ or ""))
print()
print("16h. it reaches BOTH shipped plugins that take taxes - and NEITHER of "
"the ones that do not")
# ⚠️ TARIFF-15's `custom_formula` declares {ENERGY, MARKUP}, so the framework
# adds TAXES for it; `generic_dayahead` declares {ENERGY} and gets both. The
# table must move the argmin under each. `manual_csv` publishes an all-in
# figure and is the NEGATIVE control - the table must be routed through
# `caps.adds` and not bolted onto every provider.
_T17_BY_PLUGIN = derived(lambda: {
pid: (_t17_cheapest(assemble(cls.capabilities(), _T17_RAW, MARKUP,
_T17_ENKELVOUDIG)),
_t17_cheapest(assemble(cls.capabilities(), _T17_RAW, MARKUP,
_T17_TABLE)),
sorted(c.value for c in cls.capabilities().adds))
for pid, cls in sorted(PROVIDERS.items())})
for _pid, _row in (sorted(_T17_BY_PLUGIN.items())
if isinstance(_T17_BY_PLUGIN, dict) else []):
print(f" {_pid:<18} adds {str(_row[2]):<22} cheapest hour "
f"flat {_row[0]:02d} -> table {_row[1]:02d}")
check("custom_formula declares {ENERGY, MARKUP}, so the framework adds TAXES "
"for it - and the table moves its cheapest hour 13 -> 03",
isinstance(_T17_BY_PLUGIN, dict)
and _T17_BY_PLUGIN["custom_formula"][:2] == (13, 3)
and _T17_BY_PLUGIN["custom_formula"][2] == ["taxes"])
check("generic_dayahead declares {ENERGY} and gets markup AND taxes - and the "
"table moves its cheapest hour 13 -> 03 too",
isinstance(_T17_BY_PLUGIN, dict)
and _T17_BY_PLUGIN["generic_dayahead"][:2] == (13, 3)
and _T17_BY_PLUGIN["generic_dayahead"][2] == ["markup", "taxes"])
check("manual_csv publishes an all-in figure, adds NOTHING, and is left where "
"it was - the table is routed through caps.adds, not bolted on",
isinstance(_T17_BY_PLUGIN, dict)
and _T17_BY_PLUGIN["manual_csv"][2] == []
and _T17_BY_PLUGIN["manual_csv"][0] == _T17_BY_PLUGIN["manual_csv"][1]
== 13)
check(f"...and that is EVERY registered plugin, enumerated from PROVIDERS "
f"rather than typed: {sorted(_T17_BY_PLUGIN)}",
isinstance(_T17_BY_PLUGIN, dict)
and set(_T17_BY_PLUGIN) == set(PROVIDERS) and len(PROVIDERS) >= 3)
print()
print("16i. Brussels WALL CLOCK, on the 25-hour day - inherited from the "
"grammar, not re-derived")
# ⚠️ THE 23- AND 25-HOUR DAYS ARE WHERE A SECOND IMPLEMENTATION WOULD DIVERGE,
# which is why there is not a second one: `StaticProfile.at()` renders the
# instant at the Brussels offset and matches on the LOCAL weekday and minute.
# 2026-10-25 is the autumn transition: local 02:00 happens twice, both passes
# are night, and the night->day boundary is still local 07:00.
_T17_DST0 = datetime(2026, 10, 24, 22, 0, tzinfo=UTC) # 00:00 CEST, Sunday
_T17_DST_RAW = [RawPoint(_T17_DST0 + timedelta(hours=h), 0.10, False)
for h in range(25)]
_T17_DST = derived(lambda: [
(brussels(p.start).hour, brussels(p.start).utcoffset().seconds // 3600,
round(p.eur_per_kwh - 0.10 - MARKUP, 6))
for p in assemble(_T17_CAPS, _T17_DST_RAW, MARKUP, _T17_TABLE)])
print(f" local hours: {[h for h, _o, _d in _T17_DST]}")
check("the 25-hour day assembles 25 points and the local hour 02 appears "
"TWICE, once at +02:00 and once at +01:00",
isinstance(_T17_DST, list) and len(_T17_DST) == 25
and [(h, o) for h, o, _d in _T17_DST].count((2, 2)) == 1
and [(h, o) for h, o, _d in _T17_DST].count((2, 1)) == 1)
check("...and 2026-10-25 is a SUNDAY, so ORES's weekend row prices all 25 "
"hours at the night band - a table that matched on the UTC hour, or on a "
"24-slot array, would have run off the end of the day",
isinstance(_T17_DST, list)
and {d for _h, _o, d in _T17_DST} == {round(_T17_NACHT, 6)})
# The weekday boundary, to show the band edge really is wall clock: the FIRST
# day-band point sits at local 07:00 in both halves of the year, which is
# 06:00 UTC in January and 05:00 UTC in July.
_T17_EDGE = derived(lambda: tuple(
min(brussels(p.start) for p in assemble(
_T17_CAPS,
[RawPoint(base + timedelta(hours=h), 0.10, False) for h in range(24)],
MARKUP, _T17_TABLE)
if round(p.eur_per_kwh - 0.10 - MARKUP, 6) == round(_T17_DAG, 6))
for base in (datetime(2026, 1, 14, 23, 0, tzinfo=UTC), # Thu, CET
datetime(2026, 7, 15, 22, 0, tzinfo=UTC)))) # Thu, CEST
check(f"...and the dag band opens at local 07:00 on BOTH a CET and a CEST "
f"weekday - {[e.isoformat() for e in _T17_EDGE]} - so it is the same UTC "
f"instant only on one of them",
isinstance(_T17_EDGE, tuple)
and [e.hour for e in _T17_EDGE] == [7, 7]
and len({e.astimezone(UTC).hour for e in _T17_EDGE}) == 2)
print()
print("16j. a MALFORMED table: refused at startup, and loud rather than "
"silent if it ever gets past")
_T17_GAP = {"periods": [{"name": "dag", "from": "07:00", "to": "22:00",
"days": "all", "eur_per_kwh": _T17_DAG}]}
_T17_GAP_MSG = _reason(lambda: parse_taxes_profile(_T17_GAP))
print(f" {_T17_GAP_MSG[:110]}")
check(f"a table that leaves an hour unpriced is refused by the SAME tiling "
f"rule static_profile has, and the refusal names the key the operator "
f"typed",
_T17_GAP_MSG.startswith("price_taxes_profile leaves ")
and "no period" in _T17_GAP_MSG)
check("...and the fetch path does NOT turn it into a traceback into the 30 s "
"tick, nor silently fall back to the scalar: it is a §11.4 no-data read "
"naming the key",
run(load_prices(opts(price_provider="generic_dayahead",
price_taxes_profile=_T17_GAP),
_T17_UTC0, _T17_UTC0 + timedelta(hours=24),
canned(b""))).match(
series=lambda _r: False,
no_data=lambda r: r.reason.startswith(
"price_taxes_profile cannot be used:")))
check("...and the value that is NOT a table at all - the shipped \"\" - leaves "
"the scalar path exactly where it was",
taxes_profile(opts()) is None
and taxes_profile(opts(price_taxes_profile="")) is None
and taxes_profile(opts(price_taxes_profile="null")) is None)
print()
print("16k. END TO END through the SHIPPED load_prices(), on the SHIPPED "
"plugin - because everything above could be true with the option unread")
# ⚠️ WHAT THIS CATCHES THAT 16c CANNOT. 16c hands `assemble()` a parsed table
# directly. `load_prices()` is what has to READ the option, parse it and pass
# it, and a mutation that drops any of those three leaves every check above
# green - the framework would be capable of an ORDER-bucket component and no
# site could configure one. So this drives the real `generic_dayahead`, over
# the real columnar parser, from an OPTIONS DICT.
_T17_BODY = columnar_feed([v * 1000.0 for v in _T17_SPOT], start=_T17_UTC0)
_T17_E2E_TABLE = json.dumps(_T17_TABLE_JSON)
def _t17_load(**over):
return run(load_prices(
opts(price_provider="generic_dayahead", **over),
_T17_UTC0, _T17_UTC0 + timedelta(hours=24),
canned(_T17_BODY)))
_T17_E2E = derived(lambda: tuple(
r.match(series=lambda x: (_t17_cheapest(x.points),
len({round(pt.eur_per_kwh
- _T17_SPOT[brussels(pt.start).hour]
- MARKUP, 6) for pt in x.points})),
no_data=lambda x: x.reason)
for r in (_t17_load(price_taxes_eur_kwh=_T17_ENKELVOUDIG),
_t17_load(price_taxes_profile=_T17_E2E_TABLE))))
print(f" flat scalar option : cheapest hour {_T17_E2E[0][0]}, "
f"{_T17_E2E[0][1]} distinct tax delta(s)")
print(f" table option : cheapest hour {_T17_E2E[1][0]}, "
f"{_T17_E2E[1][1]} distinct tax delta(s)")
check("the OPTION is read, parsed and applied: the same shipped feed and the "
"same shipped plugin are cheapest at 13:00 under price_taxes_eur_kwh and "
"at 03:00 under price_taxes_profile",
isinstance(_T17_E2E, tuple) and _T17_E2E[0] == (13, 1)
and _T17_E2E[1] == (3, 2))
check("...and the scalar option is still the one that answers when the table "
"is absent, which is every install that exists today",
isinstance(_T17_E2E, tuple) and _T17_E2E[0][1] == 1)
# --------------------------------------------------------------------------- #
print()
print("17. TARIFF-16 - the INJECTION leg, which a scalar cannot hold")
#
# WHAT THIS SECTION IS DEFENDING. `export_price_eur_kwh()` models what a site
# is PAID for export as ONE number. The market publishes a LINE, and the cards
# say so in their own words:
#
# Eneco Zon & Wind Dynamisch 0,1 x BELPEX-H - 1,188 c EUR/kWh
# "Het injectietarief is dynamisch en kan per uur wijzigen"
# - identical on the January and the September 2026 cards
# ENGIE Dynamic (B x eSpot_15) - A
# Aspiravi (non-dynamic) 0,07 x Belpex - 2
#
# ⚠️ AND WHAT IT IS **NOT** DEFENDING, FIRST, BECAUSE OVERSTATING IT WOULD BE
# THE DEFECT TARIFF-07 WITHDREW THREE PLUGINS FOR. `arbitrage.py` does not read
# `export_price_eur_kwh` at all - its own comment says so, with the reason -
# so no trade this add-on takes is decided by this number and nothing here is a
# wrong-trade fix. 17h GRADES that claim rather than repeating it.
#
# ⚠️ EVERY COEFFICIENT BELOW IS SOMEBODY ELSE'S PUBLISHED NUMBER, transcribed
# from the cards and from nowhere in app/: section 2b's rule applied to
# arithmetic, which is 15a's move as well.
# Eneco Zon & Wind Dynamisch, the INJECTION line. ⚠️ NO `card` BLOCK, AND THAT
# IS THE CARD'S OWN SHAPE RATHER THAN AN OMISSION HERE: Eneco prints this
# formula with NO example price beside it. Inventing one would grade these
# coefficients against arithmetic done with these coefficients, which is
# MUTATION.md habit 4 in a fixture.
ENECO_INJECTION = {"index": "epex_be", "resolution": "hourly",
"factor": 0.100, "offset": -1.188,
"units": "c_eur_per_kwh", "vat_pct": 0}
# Aspiravi, non-dynamic: `0,07 x Belpex - 2`. A SECOND supplier, so that the
# pass-through bound below is graded against a range somebody published rather
# than against one number.
ASPIRAVI_INJECTION = {"index": "epex_be", "resolution": "hourly",
"factor": 0.07, "offset": -2.0,
"units": "c_eur_per_kwh", "vat_pct": 0}
print()
print("17a. the four properties a scalar cannot hold - the reproduction, as "
"checks")
# ⚠️ PRECONDITION FIRST (habit 1). Every claim below is a DIFFERENCE between
# the consumption leg and the injection leg, and a difference asserted against
# two fixtures that happen to be equal is decoration. The consumption side is
# ENECO_CARD, section 15's own fixture, held BY IDENTITY - not a second copy of
# Eneco's numbers typed here.
check(f"the two legs of the SAME supplier's card really are two: consumption "
f"factor {ENECO_CARD['factor']} against injection "
f"{ENECO_INJECTION['factor']}, vat_pct {ENECO_CARD['vat_pct']} against "
f"{ENECO_INJECTION['vat_pct']}, offset {ENECO_CARD['offset']} against "
f"{ENECO_INJECTION['offset']}",
ENECO_CARD["factor"] != ENECO_INJECTION["factor"]
and ENECO_CARD["vat_pct"] != ENECO_INJECTION["vat_pct"]
and ENECO_CARD["offset"] != ENECO_INJECTION["offset"])
check("...and the injection factor is NOT the consumption one with a sign "
"flip, which is the shape somebody would reach for first",
ENECO_INJECTION["factor"] != -ENECO_CARD["factor"]
and ASPIRAVI_INJECTION["factor"] != -ENECO_CARD["factor"])
_T16_SPEC = derived(lambda: parse_export_formula(json.dumps(ENECO_INJECTION)))
_T16_ASP = derived(lambda: parse_export_formula(
json.dumps(ASPIRAVI_INJECTION)))
check(f"PROPERTY 1 - its OWN factor: the injection leg parses to "
f"factor {derived(lambda: _T16_SPEC.factor)} while section 15's "
f"consumption card parses to "
f"{derived(lambda: parse_price_formula(json.dumps(ENECO_CARD)).factor)}"
f", from two INDEPENDENT options",
derived(lambda: _T16_SPEC.factor) == 0.100
and derived(lambda: parse_price_formula(
json.dumps(ENECO_CARD)).factor) == 0.102)
check(f"PROPERTY 2 - a NEGATIVE offset, which the markup option cannot hold "
f"at all: config.yaml declares `price_markup_eur_kwh: float(0,1)`, and "
f"the injection leg carries "
f"{derived(lambda: _T16_SPEC.offset)} {ENECO_INJECTION['units']} = "
f"{derived(lambda: _T16_SPEC.offset_eur_kwh):+.5f} EUR/kWh",
derived(lambda: _T16_SPEC.offset) == -1.188
and derived(lambda: _T16_SPEC.offset_eur_kwh) < 0.0)
check(f"PROPERTY 3 - vat_pct is PER LEG, so the 6% asymmetry is expressible: "
f"{derived(lambda: parse_price_formula(json.dumps(ENECO_CARD)).vat_pct)}"
f" on consumption against {derived(lambda: _T16_SPEC.vat_pct)} on "
f"injection - \"injectietarieven zijn vrijgesteld van btw\"",
derived(lambda: parse_price_formula(json.dumps(ENECO_CARD)).vat_pct)
== 6.0 and derived(lambda: _T16_SPEC.vat_pct) == 0.0)
# ⚠️ AND THE ASYMMETRY IS GRADED AS AN ARITHMETIC DIFFERENCE, NOT AS TWO FIELD
# VALUES. Two specs can carry 6 and 0 and still be multiplied by the same
# number if `in_card_units()` ever read VAT from anywhere but the spec.
_T16_VAT_PAIR = derived(lambda: (
in_card_units(parse_price_formula(json.dumps(ENECO_CARD)), 100.0)
/ (0.102 * 100.0 + 1.00),
in_card_units(_T16_SPEC, 100.0) / (0.100 * 100.0 - 1.188)))
print(f" VAT multiplier actually applied: consumption "
f"{derived(lambda: _T16_VAT_PAIR[0]):.4f}, injection "
f"{derived(lambda: _T16_VAT_PAIR[1]):.4f}")
check("...and the multiplier each leg actually APPLIES differs by exactly "
"6%: 1.06 on consumption, 1.00 on injection, measured through the "
"shipped arithmetic rather than read off the field",
derived(lambda: abs(_T16_VAT_PAIR[0] - 1.06) < 1e-9)
and derived(lambda: abs(_T16_VAT_PAIR[1] - 1.00) < 1e-9))
# PROPERTY 4 - it goes negative, and a PV household exports in exactly those
# hours. The break-even index is the card's own arithmetic: 1,188 / 0,1.
_T16_BREAKEVEN = 1.188 / 0.100
_T16_CURVE = derived(lambda: [
(idx, price_eur_kwh(_T16_SPEC, idx))
for idx in (-18.2, 0.0, _T16_BREAKEVEN, 61.96, 133.9905)])
for _i, _p in (derived(lambda: _T16_CURVE) or []):
print(f" BELPEX-H {_i:+9.4f} EUR/MWh -> injection {_p:+.6f} EUR/kWh")
check(f"PROPERTY 4 - the published line is NEGATIVE below "
f"{_T16_BREAKEVEN:.2f} EUR/MWh of BELPEX-H, zero AT it and positive "
f"above, through the shipped arithmetic - so 'you can be charged to "
f"export' is this module's answer and not a remark",
derived(lambda: [round(p, 8) for _i, p in _T16_CURVE])
== [round(p, 8) for p in (-0.03008, -0.01188, 0.0, 0.05008,
0.1221105)])
check("...and the SCALAR cannot hold any of it: one option, one number, the "
"same in every hour whatever the index did",
len({export_price_eur_kwh(opts())}) == 1
and export_price_eur_kwh(opts()) ==
PRICE_OPTION_DEFAULTS["export_price_eur_kwh"])
print()
print("17b. ONE parser, not two - the identity, pinned on the AST and on the "
"refusals")
# ⚠️ THE CLAIM IS AN IDENTITY ("it is THIS parser"), so MUTATION.md habit 10
# says pin it structurally AND grade its meaning behaviourally. Both are here:
# 17b1 the grammar family is DEFINED IN EXACTLY ONE MODULE, with the scope
# COMPUTED by parsing every file under app/ (habit 12 - a hand-written scope is
# the same defect as a hand-written fixture); 17b2 the two options give the
# SAME refusal sentences, each DERIVED from the other's rather than typed.
_T16_FAMILY = (
"FormulaSpec", "FormulaInvalid", "parse_price_formula",
"parse_export_formula", "in_card_units", "price_eur_kwh",
"card_discrepancy", "card_refusal", "_card", "_finite", "_month_date",
"to_resolution", "UNITS", "RESOLUTIONS", "INDEXES", "CARD_TOLERANCE",
"_LEGS", "_Leg", "implausible_injection", "implausible_all_in",
"EUR_KWH_PER_EUR_MWH", "PASSTHROUGH_LO", "PASSTHROUGH_HI",
"INJECTION_OFFSET_MAX_EUR_KWH", "_CARD_KEYS", "_TOP_KEYS",
"EXPORT_FORMULA_OPTION", "stale_card", "preview_lines", "CARD_STALE_DAYS",
"PLAUSIBLE_LO_EUR_KWH", "PLAUSIBLE_HI_EUR_KWH")
def _t16_defs(path):
"""Top-level names a module DEFINES - classes, functions, assignments.
⚠️ IT HANDLES `AnnAssign` AND test_static_tariff.py's 10a DOES NOT, which
matters here and not there: `UNITS: dict = {...}`, `RESOLUTIONS: tuple` and
`INDEXES: dict` are ANNOTATED assignments, so a scanner that only walks
`ast.Assign` would report them as defined in ZERO modules and the
"exactly one" line below would go red on the shipped tree - the failure
mode that gets a check deleted rather than read.
"""
tree = ast.parse(open(path, encoding="utf-8").read())
out = set()
for node in tree.body:
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef,
ast.ClassDef)):
out.add(node.name)
elif isinstance(node, ast.Assign):
out |= {t.id for t in node.targets if isinstance(t, ast.Name)}
elif isinstance(node, ast.AnnAssign) and isinstance(node.target,
ast.Name):
out.add(node.target.id)
return out
_T16_APP = sorted(glob.glob(os.path.join(HERE, "app", "*.py")))
_T16_WHERE = {name: sorted(os.path.basename(f) for f in _T16_APP
if name in _t16_defs(f))
for name in _T16_FAMILY}
_T16_HOME = ["custom_formula.py"]
_T16_STRAY = {n: w for n, w in _T16_WHERE.items() if w != _T16_HOME}
print(f" {len(_T16_FAMILY)} formula-grammar names over {len(_T16_APP)} "
f"modules under app/; not in custom_formula.py alone: {_T16_STRAY}")
check(f"every one of the {len(_T16_FAMILY)} formula-grammar names is defined "
f"in EXACTLY ONE of the {len(_T16_APP)} modules under app/ - a second "
f"copy of `_finite` or of the unit table takes this red wherever it is "
f"put, including in a module this check has never heard of",
all(len(w) == 1 for w in _T16_WHERE.values()))
check("...and that module is custom_formula.py in every case, so the grammar "
"did not move house either",
_T16_STRAY == {})
check(f"...and the pin is not vacuous: every name really is a top-level "
f"definition and the scan read more than a handful of modules "
f"({len(_T16_APP)})",
all(w for w in _T16_WHERE.values()) and len(_T16_APP) > 5)
_T16_BODY = ast.parse(open(os.path.join(HERE, "app", "custom_formula.py"),
encoding="utf-8").read())
_T16_WRAP = next((n for n in _T16_BODY.body
if isinstance(n, ast.FunctionDef)
and n.name == "parse_export_formula"), None)
_T16_CALLS = ([ast.unparse(n.func) for n in ast.walk(_T16_WRAP)
if isinstance(n, ast.Call)] if _T16_WRAP else [])
check(f"...and `parse_export_formula` is a WRAPPER and not a fork: its whole "
f"body calls {_T16_CALLS}, pinned on the AST rather than on the source "
f"text, because `\"parse_price_formula\" in src` is true of a copy that "
f"merely mentions it",
_T16_CALLS == ["parse_price_formula"])
print()
print("17b2. and the two options give the SAME refusals - derived from each "
"other, not typed twice")
# ⚠️ EVERY EXPECTED SENTENCE HERE IS THE OTHER OPTION'S OWN SENTENCE with the
# key substituted, which is TARIFF-17's 10b move. Nothing is transcribed, so a
# fork that reworded one refusal - or quietly stopped refusing something - is
# red on the row it broke and not on all of them.
def _t16_pair(blob):
"""The two legs' refusals on one stimulus."""
return (_reason(lambda: parse_price_formula(json.dumps(blob))),
_reason(lambda: parse_export_formula(json.dumps(blob))))
# Built FROM section 15's real card, so each row differs from an ACCEPTED
# baseline in exactly one field (habit 7). ⚠️ The baseline itself is checked
# first, on BOTH legs, or every refusal below has nowhere else to come from.
_T16_OK_BOTH = _t16_pair(ENECO_CARD)
check(f"the CONTROL: section 15's real Eneco card is accepted by BOTH options "
f"{_T16_OK_BOTH}. ⚠️ It is used here as a PARSER STIMULUS and is not a "
f"claim that a consumption card is an injection contract - what it "
f"establishes is that the rows below are refusals of the FIELD each "
f"names",
_T16_OK_BOTH == ("", ""))
_T16_SHARED = [
("a factor that is not a number", spoilt(factor="0.102 * spot")),
("a NaN factor", spoilt(factor=float("nan"))),
("a boolean factor", spoilt(factor=True)),
("an unknown index", spoilt(index="belpex_h")),
("an unknown resolution", spoilt(resolution="half_hourly")),
("missing units", spoilt(units="")),
("an unknown unit", spoilt(units="cent_per_kwh")),
("a vat_pct of 106", spoilt(vat_pct=106)),
("an unknown top-level field", dict(ENECO_CARD, expr="0.1*x")),
("an unknown card field", dict(ENECO_CARD,
card=dict(ENECO_CARD["card"], vat=6))),
("a card month that is not YYYY-MM", spoilt(card_month="September")),
("a card that is not an object", dict(ENECO_CARD, card=3)),
]
_T16_ROWS = []
for _what, _blob in _T16_SHARED:
_a, _b = _t16_pair(_blob)
_T16_ROWS.append((_what, _a, _b,
_a.replace("price_formula", EXPORT_FORMULA_OPTION)
== _b))
for _what, _a, _b, _same in _T16_ROWS:
print(f" {'same' if _same else 'DIFF'} {_what}: {_a[:56]!r}")
check(f"all {len(_T16_ROWS)} SHARED refusals are the consumption leg's OWN "
f"sentence with the key substituted - derived, not transcribed, so a "
f"second parser diverging on any one of them is red on that row alone",
all(_same for _w, _a, _b, _same in _T16_ROWS))
check("...and none of those rows is vacuous: every one really did refuse on "
"both legs, or 'the two empty strings match' would pass the line above",
all(_a and _b for _w, _a, _b, _s in _T16_ROWS))
# ⚠️ AND THE ROWS THAT DELIBERATELY DIFFER, NAMED WITH THEIR SHAPES RATHER THAN
# ROUNDED OFF (habit 13). Three sentences are per-LEG on purpose - `_LEGS` is
# exactly the set of things that are not shared - and a check that asserted
# "every refusal is identical" would have to be weakened the first time one of
# them was right to differ. So they are asserted to DIFFER, and to differ in
# the direction that points the operator at the right option.
_T16_DIFFER = [
("factor 0 -> where a FLAT price of THIS leg lives",
spoilt(factor=0.0), "static_profile", "export_price_eur_kwh"),
# UX-08 2c: a negative factor gets one sentence on both legs now ("put the
# minus sign on the offset"), so it left this table.
("a card miss -> what else to check on THIS leg",
spoilt(factor=0.120), "network charges are not in the offset",
"the VAT is 0 for injection"),
("invalid JSON -> WHEN this option is read at all, which is not the "
"same answer on the two legs: `price_formula` decides nothing unless "
"price_provider is 'custom_formula', and the injection leg is read "
"whatever the consumption provider is",
"{not json", "unless “Dynamic price source” is 'custom_formula'",
"only if your injection price follows the market"),
]
for _what, _blob, _cons_mark, _inj_mark in _T16_DIFFER:
_a, _b = ((_reason(lambda b=_blob: parse_price_formula(b)),
_reason(lambda b=_blob: parse_export_formula(b)))
if isinstance(_blob, str) else _t16_pair(_blob))
check(f"{_what}: the consumption refusal says {_cons_mark!r} and the "
f"injection one says {_inj_mark!r} - both refuse, and each names "
f"the option the operator should actually reach for",
_cons_mark in _a and _cons_mark not in _b
and _inj_mark.lower() in _b.lower()
and _inj_mark.lower() not in _a.lower())
print()
print("17c. a NEGATIVE injection price is ACCEPTED - and TARIFF-15's band is "
"MEASURED refusing it, rather than asserted to")
# ⚠️ THE ACCEPTANCE CRITERION, AND THE COUNTERFACTUAL THAT MAKES IT A CLAIM.
# `implausible_all_in()` floors the CONSUMPTION leg at +0.02 EUR/kWh of energy,
# which is right there and fatal here: Eneco's injection price is below zero
# for any BELPEX-H under 11,88 EUR/MWh, and a PV household exports in exactly
# those hours. So the same spec is put through BOTH bands and the two answers
# are printed side by side. Refusing a negative injection price would hide the
# hours in which exporting COSTS money.
_T16_NEG_CARD = derived(lambda: _cf.FormulaSpec(
index="epex_be", resolution="hourly", factor=0.100, offset=-1.188,
units="c_eur_per_kwh", vat_pct=0, card_month="2026-09",
card_index_eur_per_mwh=-18.2, card_expect_incl_vat=-3.008,
option=EXPORT_FORMULA_OPTION))
_T16_NEG_PRICE = derived(lambda: price_eur_kwh(_T16_NEG_CARD, -18.2))
_T16_OLD_BAND = derived(lambda: implausible_all_in(_T16_NEG_CARD, 0.11736))
_T16_NEW_BAND = derived(lambda: implausible_injection(_T16_NEG_CARD))
print(f" the same spec, at BELPEX-H -18.2 EUR/MWh -> "
f"{derived(lambda: _T16_NEG_PRICE):+.5f} EUR/kWh")
print(f" implausible_all_in() -> "
f"{'REFUSES' if _T16_OLD_BAND else 'accepts'}")
print(f" implausible_injection() -> "
f"{'REFUSES' if _T16_NEW_BAND else 'accepts'}")
check(f"the stimulus really is negative ({derived(lambda: _T16_NEG_PRICE)}), "
f"or the two lines below would be grading a positive number and would "
f"pass whatever either band did",
derived(lambda: _T16_NEG_PRICE) < 0.0)
check(f"TARIFF-15's consumption band REFUSES it, for its floor's reason - "
f"measured, so 'the scalar band would have ruled the market out' is an "
f"observation ({str(_T16_OLD_BAND)[:70]!r})",
isinstance(_T16_OLD_BAND, str)
and "outside the plausible band" in _T16_OLD_BAND
and f"{PLAUSIBLE_LO_EUR_KWH}" in _T16_OLD_BAND)
check("...and the INJECTION band accepts it, which is the acceptance "
"criterion: a negative injection price is the real market and is not "
"refused",
_T16_NEW_BAND is None)
check("...and so does the shipped PARSER, end to end, on the real published "
"line with no card block at all - `0,1 x BELPEX-H - 1,188` starts",
derived(lambda: _T16_SPEC.factor) == 0.100
and _reason(lambda: parse_export_formula(
json.dumps(ENECO_INJECTION))) == "")
check("...and Aspiravi's `0,07 x Belpex - 2` starts too, so the band admits a "
"RANGE of published contracts rather than one tuned pair",
_reason(lambda: parse_export_formula(
json.dumps(ASPIRAVI_INJECTION))) == "")
print()
print("17d. ...and an IMPLAUSIBLE one is still refused, by two rungs that are "
"graded on their REASONS and mutated TOGETHER")
# ⚠️ TEST-16's RULE. Two rungs can answer one stimulus - a whole card read 100x
# too large trips both - so each is given a stimulus the OTHER cannot reach,
# and each stimulus differs from the ACCEPTED baseline in EXACTLY ONE FIELD
# (habits 1 and 7). The precondition is its own check: the quantity the other
# rung grades is asserted to be INSIDE its band, so a row cannot pass because
# the wrong rung caught it.
def _t16_inj(**over):
"""Eneco's published injection line with one field replaced."""
b = dict(ENECO_INJECTION)
b.update(over)
return b
def _t16_refusal(blob):
return _reason(lambda: parse_export_formula(json.dumps(blob)))
check("the CONTROL: the published line is accepted, so every refusal below "
"has nowhere else to come from",
_t16_refusal(ENECO_INJECTION) == "")
_T16_ONLY_PT = _t16_inj(factor=ENECO_INJECTION["factor"] * 100.0)
_T16_ONLY_OFF = _t16_inj(offset=ENECO_INJECTION["offset"] * 100.0)
_T16_BOTH = _t16_inj(factor=ENECO_INJECTION["factor"] * 100.0,
offset=ENECO_INJECTION["offset"] * 100.0)
check("PRECONDITION: each stimulus differs from the accepted baseline in "
"EXACTLY ONE field - the pass-through one moves `factor` and nothing "
"else, the offset one moves `offset` and nothing else",
{k for k in _T16_ONLY_PT if _T16_ONLY_PT[k] != ENECO_INJECTION[k]}
== {"factor"}
and {k for k in _T16_ONLY_OFF if _T16_ONLY_OFF[k] != ENECO_INJECTION[k]}
== {"offset"})
_T16_PT_OFFSET = ENECO_INJECTION["offset"] * UNITS[ENECO_INJECTION["units"]]
_T16_OFF_PT = (ENECO_INJECTION["factor"] * UNITS[ENECO_INJECTION["units"]]
/ EUR_KWH_PER_EUR_MWH)
print(f" pass-through stimulus: offset stays {_T16_PT_OFFSET:+.5f} EUR/kWh "
f"(bound {INJECTION_OFFSET_MAX_EUR_KWH}) - the offset rung CANNOT fire")
print(f" offset stimulus : pass-through stays {_T16_OFF_PT:.4g}x "
f"(band {PASSTHROUGH_LO}-{PASSTHROUGH_HI}) - the pass-through rung "
f"CANNOT fire")
check("PRECONDITION: on each stimulus the OTHER rung's quantity is INSIDE its "
"band, so neither row can pass because the wrong rung answered - which "
"is the masking TEST-16 exists for",
abs(_T16_PT_OFFSET) <= INJECTION_OFFSET_MAX_EUR_KWH
and PASSTHROUGH_LO <= _T16_OFF_PT <= PASSTHROUGH_HI)
_T16_PT_WHY = _t16_refusal(_T16_ONLY_PT)
_T16_OFF_WHY = _t16_refusal(_T16_ONLY_OFF)
_T16_BOTH_WHY = _t16_refusal(_T16_BOTH)
check(f"a factor 100x out is refused BY THE PASS-THROUGH RUNG, naming the "
f"share of the wholesale price it would pay ({_T16_PT_WHY[:60]!r})",
"passes through" in _T16_PT_WHY
and f"{PASSTHROUGH_LO}-{PASSTHROUGH_HI}x" in _T16_PT_WHY
and "larger than" not in _T16_PT_WHY)
check(f"an offset 100x out is refused BY THE OFFSET RUNG, naming the EUR/kWh "
f"it works out at ({_T16_OFF_WHY[:60]!r})",
"larger than" in _T16_OFF_WHY
and "passes through" not in _T16_OFF_WHY)
check(f"...and the WHOLE card 100x out - which trips both - is refused, with "
f"the pass-through rung answering first ({_T16_BOTH_WHY[:44]!r})",
"passes through" in _T16_BOTH_WHY)
check("...and neither refusal mentions the SIGN or the LEVEL of a price, "
"which is why they can bound a leg that legitimately goes below zero",
all("below zero" not in w.split("⚠️")[0]
for w in (_T16_PT_WHY, _T16_OFF_WHY)))
# ⚠️ THE BOUNDS ARE GRADED FROM BOTH SIDES, or "within the band" and "within
# anything" would be the same check. The published contracts sit at 1.00x and
# 0.70x; the band is 0.10-10.
_T16_EDGES = derived(lambda: [
(f"{_pt}x", implausible_injection(_cf.FormulaSpec(
index="epex_be", resolution="hourly",
factor=_pt * EUR_KWH_PER_EUR_MWH / UNITS["c_eur_per_kwh"],
offset=-1.188, units="c_eur_per_kwh", vat_pct=0,
option=EXPORT_FORMULA_OPTION)) is None)
for _pt in (0.05, PASSTHROUGH_LO, 0.70, 1.00, PASSTHROUGH_HI, 20.0)])
print(f" pass-through accepted? {_T16_EDGES}")
check(f"the pass-through band is the number being enforced, not a shape: "
f"{PASSTHROUGH_LO}x and {PASSTHROUGH_HI}x pass and 0.05x / 20x do not, "
f"with both published contracts (0.70x, 1.00x) inside",
derived(lambda: [ok for _n, ok in _T16_EDGES])
== [False, True, True, True, True, False])
print()
print("17e. the card self-check applies WHERE THERE IS A CARD - and where "
"there is not, that is STATED")
# ⚠️ THE HONEST HALF OF THIS CARD. TARIFF-15 could demonstrate two tariffs end
# to end rather than three and refused to fabricate a third; the same fact
# lands here as an OPTIONAL `card` block. Eneco prints its injection formula
# with NO price beside it, so on that card there is nothing to reproduce -
# requiring a block would make the published line inexpressible, and inventing
# the estimate would grade the coefficients against arithmetic done with the
# same coefficients.
check("the consumption leg still REFUSES a formula with no card block, "
"unchanged, and says why the card is required rather than that a field "
"is missing",
"no \"card\" block" in _reason(lambda: parse_price_formula(json.dumps(
{k: v for k, v in ENECO_CARD.items() if k != "card"}))))
check("...and the INJECTION leg accepts one, because that is the shape of the "
"card it is read off",
_t16_refusal(ENECO_INJECTION) == ""
and derived(lambda: _T16_SPEC.card_graded) is False)
check("...and says so in a field callers can ask, rather than reporting a "
"discrepancy of zero - which is what 'the coefficients reproduce the "
"card' looks like, for the one state in which nothing was compared",
raises(lambda: card_discrepancy(_T16_SPEC), FormulaInvalid)
and "no card block" in _reason(lambda: card_discrepancy(_T16_SPEC)))
# ...and where a card IS supplied on this leg, the SAME rung grades it with the
# SAME tolerance. Graded against section 15's real printed 15.55 - the stimulus
# is the consumption card put through the injection option, which is what makes
# the expectation somebody else's published number rather than this file's.
_T16_WITH_CARD = _reason(lambda: parse_export_formula(json.dumps(ENECO_CARD)))
_T16_SPOILT_CARD = _reason(lambda: parse_export_formula(json.dumps(
spoilt(card_expect_incl_vat=15.05))))
check(f"a card block ON THIS LEG is graded by the same rung at the same "
f"tolerance: the real card passes and a transposed digit (15.05 for "
f"15.55) is refused, naming the option the operator typed "
f"({_T16_SPOILT_CARD[:64]!r})",
_T16_WITH_CARD == ""
and "the card says" in _T16_SPOILT_CARD
and _T16_SPOILT_CARD.startswith(EXPORT_FORMULA_OPTION))
check(f"...and half a tolerance out still passes, so {CARD_TOLERANCE} is "
f"enforced on this leg too rather than 'something'",
_reason(lambda: parse_export_formula(json.dumps(spoilt(
card_expect_incl_vat=round(in_card_units(parse_price_formula(
json.dumps(ENECO_CARD)), 133.9905) + CARD_TOLERANCE * 0.5,
6))))) == "")
# ⚠️ WHAT THE UNGRADED STATE COSTS, SAID WITH ITS SIZE (habit 13). Without a
# printed estimate the two coefficient bounds are the WHOLE of what is
# mechanised, and they are two orders of magnitude coarser than the card check.
_T16_SNEAK = derived(lambda: parse_export_formula(json.dumps(
_t16_inj(factor=0.070))))
check(f"and the gap is real and is not glossed: with no card, a factor of "
f"0.070 instead of 0.100 - Aspiravi's number on Eneco's contract, 30% "
f"of every export payment - passes both bounds, because 0.70x is a "
f"published pass-through. Nothing here can catch a wrong-contract pair; "
f"only a printed estimate can, and this card has none",
derived(lambda: _T16_SNEAK.factor) == 0.070)
print()
print("17f. the SCALAR path is unchanged - arithmetically, not merely in "
"intent")
# ⚠️ THE ACCEPTANCE CRITERION TARIFF-17 GRADED THE SAME WAY (16f). A site with
# a flat injection price is every site that exists today, and "we did not mean
# to change it" is not a measurement.
_T16_OLD_SCALAR = [ast.unparse(n) for n in ast.walk(first(
(n for n in ast.parse(open(os.path.join(HERE, "app", "price.py"),
encoding="utf-8").read()).body
if isinstance(n, ast.FunctionDef) and n.name == "export_price_eur_kwh"),
"def export_price_eur_kwh in price.py"))
if isinstance(n, ast.Return)]
check(f"`export_price_eur_kwh()`'s BODY is still the single expression it has "
f"always been, pinned on the AST rather than on the source text: "
f"{_T16_OLD_SCALAR}",
_T16_OLD_SCALAR ==
["return float(price_option(opts, 'export_price_eur_kwh'))"])
_T16_SITES = [opts(), opts(export_price_eur_kwh=0.0),
opts(export_price_eur_kwh=-0.02),
opts(export_price_eur_kwh=0.045),
opts(export_price_eur_kwh=""),
{}, {"export_price_eur_kwh": 0.07}]
_T16_IDX = (-18.2, 0.0, 61.96, 133.9905, 400.0)
_T16_SAME = derived(lambda: [
(export_price_eur_kwh(o),
float(price_option(o, "export_price_eur_kwh")),
{export_price_at(o, i) for i in _T16_IDX})
for o in _T16_SITES])
check(f"over {len(_T16_SITES)} option dicts, the reader equals the "
f"pre-TARIFF-16 expression to the last digit - `==`, not `near()`",
derived(lambda: all(a == b for a, b, _c in _T16_SAME)))
check(f"...and with the formula option unset - which is every install today - "
f"`export_price_at()` returns that SAME number at all "
f"{len(_T16_IDX)} index values, including a negative one: a flat "
f"injection price does not move with the market, which is what flat "
f"means",
derived(lambda: all(c == {a} for a, _b, c in _T16_SAME)))
check("...and the dicts are not all the same answer, or the two lines above "
"would be one comparison repeated",
derived(lambda: len({a for a, _b, _c in _T16_SAME})) > 2)
# ...and the formula, when set, is what answers - or everything above would be
# true of a tree that parses the option and never reads it (TARIFF-17's M4).
_T16_LIVE = derived(lambda: [
export_price_at(opts(**{EXPORT_FORMULA_OPTION:
json.dumps(ENECO_INJECTION)}), i)
for i in _T16_IDX])
print(f" with the formula set: {[round(v, 6) for v in _T16_LIVE]}")
check(f"with `{EXPORT_FORMULA_OPTION}` SET, the seam answers from the formula "
f"- five distinct prices where the scalar has one, and the negative "
f"index gives a negative price rather than a floored zero",
derived(lambda: len(set(_T16_LIVE))) == len(_T16_IDX)
and derived(lambda: _T16_LIVE[0]) < 0.0
and derived(lambda: round(_T16_LIVE[0], 9)) == -0.03008)
print()
print("17g. the new key is FRAMEWORK-OWNED, and a plugin that names it is "
"still refused")
check(f"`{EXPORT_FORMULA_OPTION}` is in FRAMEWORK_KEYS, beside the scalar it "
f"varies",
EXPORT_FORMULA_OPTION in _price.FRAMEWORK_KEYS
and "export_price_eur_kwh" in _price.FRAMEWORK_KEYS)
check("...and BANNED_IN_PLUGIN_CODE is DERIVED from that set, so it arrived "
"there without a second list being edited",
EXPORT_FORMULA_OPTION in BANNED_IN_PLUGIN_CODE
and set(_price.FRAMEWORK_KEYS) <= set(BANNED_IN_PLUGIN_CODE))
check("...so PluginOptions refuses to hand it over, naming the framework",
raises(lambda: PluginOptions(opts()).get(EXPORT_FORMULA_OPTION),
FrameworkOption)
and "framework" in _reason(
lambda: PluginOptions(opts()).get(EXPORT_FORMULA_OPTION)))
class _T16Rogue(PriceProvider):
"""A plugin that reaches for the INJECTION curve.
The shape of the defect: a CONSUMPTION plugin that could read what the site
is paid for export could fold it into the series it emits, and `assemble()`
has no way to see a component that arrived inside a RawPoint.
"""
id = "t16_rogue"
@classmethod
def capabilities(cls):
return ProviderCapabilities(product=PriceProduct.DAY_AHEAD,
sources=frozenset({Component.ENERGY}),
egress_hosts=("example.invalid",))
async def fetch(self, opts, since, until, transport):
leak = opts.get("price_export_formula") # the leak
return [RawPoint(since, 0.1 + float(bool(leak)), False)]
_T16_LEAK = derived(lambda: code_leaks(_T16Rogue))
check(f"code_leaks() finds it in a plugin's own code: {_T16_LEAK}",
_T16_LEAK == [EXPORT_FORMULA_OPTION])
check("...and register() refuses to install it, so this is an inability and "
"not a convention",
raises(lambda: register(_T16Rogue), FrameworkOption)
and EXPORT_FORMULA_OPTION in _reason(lambda: register(_T16Rogue)))
check("...and it is the LEAK rung that refused, not the duplicate-id or "
"retired-id one two lines above it in register() - those raise "
"ValueError, and `raises()` grades the exact class",
not raises(lambda: register(_T16Rogue), ValueError))
check("...and the docstring above, which says 'injection' and 'export' in "
"prose, is NOT what made it fire - docstrings are stripped before the "
"scan",
EXPORT_FORMULA_OPTION not in
"".join(_T16Rogue.__doc__ or "")
+ (_T16Rogue.capabilities.__doc__ or ""))
check("...and the plugin id really was not installed by the attempt",
"t16_rogue" not in PROVIDERS)
print()
print("17h. NOTHING IN THIS ADD-ON TRADES ON IT - graded, not attested")
# ⚠️ THIS IS THE CHECK BEHIND A SENTENCE, AND THE SENTENCE IS THE ONE MOST
# LIKELY TO ROT. DOCS.md and this module both say no strategy reads an
# injection price; the day somebody wires one in, that becomes false in prose
# nobody re-reads. So the SCOPE IS COMPUTED (habit 12): every module under app/
# is parsed and asked which of the three injection readers it calls.
_T16_READERS = ("export_price_eur_kwh", "export_price_at", "export_formula")
def _t16_calls(path):
"""The names called anywhere in a module, by simple or attribute name."""
out = set()
for node in ast.walk(ast.parse(open(path, encoding="utf-8").read())):
if isinstance(node, ast.Call):
f = node.func
out.add(f.id if isinstance(f, ast.Name)
else f.attr if isinstance(f, ast.Attribute)
else ast.unparse(f))
return out
_T16_CALLERS = {os.path.basename(f): sorted(set(_T16_READERS)
& _t16_calls(f))
for f in _T16_APP}
_T16_CALLERS = {m: c for m, c in _T16_CALLERS.items() if c}
print(f" modules under app/ that call an injection reader: {_T16_CALLERS}")
check(f"`arbitrage.py` calls NONE of {list(_T16_READERS)} - it prices the "
f"sell side as displaced IMPORT and compares two all-in CONSUMPTION "
f"prices, "
f"which is its own accounting-basis comment and is what makes this card "
f"a missing capability rather than a wrong trade",
"arbitrage.py" not in _T16_CALLERS)
check("...and no other module under app/ calls one either: the only caller is "
"price.py, where `export_price_at()` calls the other two. Wire one into "
"a strategy and this row goes red, which is the point - the DOCS "
"sentence is graded rather than attested",
set(_T16_CALLERS) == {"price.py"})
check("...and the scan is not vacuous: it found the readers where they DO "
"live, so 'nothing calls them' is a result and not an empty search",
derived(lambda: _T16_CALLERS["price.py"]) == sorted(_T16_READERS[:2])
or derived(lambda: set(_T16_CALLERS["price.py"])) <= set(_T16_READERS)
and len(derived(lambda: _T16_CALLERS["price.py"])) >= 2)
# --------------------------------------------------------------------------- #
# TARIFF-16's MUTATION LEDGER. Every row was RUN, not reasoned about, and
# the method is MUTATION.md's.
#
# ⚠️ NO ROW HERE IS AN EDIT-AND-REVERT (habit 18). The harness
# (`/home/homelab/.cache/t16-mut/mut.sh`) rsyncs a FRESH COPY of the tree per
# mutation into a `mktemp` directory, excluding `__pycache__`, mutates the
# copy, mounts it `:ro` into the checks image and deletes it. There is no
# revert to fail and no shared backup path; a `:ro` tree with no cache in it
# cannot read a stale `.pyc`, which is TEST-15 closed the way CI closes it.
# Each row's byte delta and the clean/mutant tree fingerprints were printed by
# the harness before the suites ran; the clean fingerprint is
# `71160af73582cde8` on every row.
#
# ⚠️ AND THAT DESIGN WAS EXERCISED RATHER THAN ADMIRED. Two copies of this
# sweep were running at one point - a scheduling mistake, exactly habit 18's
# second bullet - and because each holds its own `mktemp` tree neither could
# restore over the other's work. The orphan was found by `docker inspect`
# showing two different `run.XXXXXX` mounts and killed; no result below comes
# from a tree two mutations deep.
#
# ⚠️ SCOPE, STATED (habit 13). Rows marked FULL were run over all 29 files.
# The rest were run over the five files that can reach this card -
# test_price.py, test_validate.py, test_price_forecast.py,
# test_static_tariff.py, test_defaults_pin.py - so those rows say which suites
# went red and do NOT claim anything about the other 24.
#
# # what was changed delta red on
# --- ------------------------------------------------- ----- ---------------
# M1 the PASS-THROUGH rung of +10 B 17d (3),
# implausible_injection() made unreachable. test_validate
# (1)
# M2 the OFFSET rung made unreachable. +10 B 17d (1),
# test_validate
# (1)
# M3 ⚠️ TEST-16's rule: BOTH rungs that can answer +20 B 17d (4),
# the hundredfold stimulus, TOGETHER. Neither test_validate
# masks the other - each still has a stimulus the (2)
# other cannot reach, so the pair moves six rows.
# M4 parse_price_formula() stops applying the +10 B 17d (3),
# injection band at all. test_validate
# (2)
# M5 ⚠️ THE HEADLINE, AND THE ACCEPTANCE CRITERION. +174 B 17c, on the
# implausible_injection() given TARIFF-15's ONE check that
# floor - a plausibility band that refuses a names it.
# legitimately NEGATIVE injection price. FULL 29-FILE
# Eneco's real published line stops starting. SWEEP: red on
# test_price.py,
# GREEN in the
# other 28.
# M6 `card_required=True` on the injection leg, so -1 B 17a (1),
# Eneco's line - printed with no price beside it - test_validate
# becomes inexpressible. ⚠️ ONE BYTE. (7)
# M7 card_discrepancy() returns 0.0 on an UNGRADED +33 B 17e (1)
# leg instead of refusing to answer - i.e. reports
# "the coefficients reproduce the card" for the
# one state in which nothing was compared.
# M8 the card self-check skipped where a card IS +10 B 17e (1) AND
# present, on EITHER leg. TARIFF-15's
# own 15b - 11
# checks in
# test_price,
# which is what
# "one rung, two
# legs" means
# M9 `price_export_formula` removed from -27 B 17g (7 checks)
# FRAMEWORK_KEYS.
# M10 a SECOND formula grammar: `UNITS` and +117 B 17b (2). FULL
# `_finite` copied into price.py. 29-FILE SWEEP:
# red on
# test_price.py,
# GREEN in the
# other 28.
# M11 parse_export_formula() drops `option=`, so every -30 B 17a (1), 17b2,
# refusal on the injection leg names test_validate
# `price_formula`. (9)
# M12 export_price_at() parses the formula and never +8 B 17f (1). FULL
# uses it - the option is configurable and decides 29-FILE SWEEP:
# nothing (TARIFF-17's M4, one option over). red on
# test_price.py,
# GREEN in the
# other 28.
# M13 a negative injection price silently FLOORED at +10 B 17f (1)
# zero by export_price_at().
# M14 validate.py never reads the option. +7 B test_validate
# (7 checks)
# M15 the UNGRADED warning suppressed: the leg starts +10 B test_validate
# and nothing says which check did not run. (1)
# M16 ⚠️ ZERO BYTES, AND NO DECISION CHANGES. The two +0 B test_validate,
# legs TRANSPOSED in the VAT-asymmetry banner. the ordered-
# Nothing the add-on DOES moves; the only surface pair check
# that moves is the sentence, which now tells the ONLY (hab. 14)
# operator their VAT treatment exactly backwards.
# M17 the injection leg's `flat` clause pointed at -6 B 17b2 DIFF row
# `static_profile`, so a site is told to fix the for factor 0
# consumption option when its injection formula
# is wrong.
# M18 the scalar reader given an IN-CODE LITERAL +4 B 17f's AST pin,
# fallback (`opts.get("export_price_eur_kwh", section 11,
# 0.04)`) instead of routing through AND
# PRICE_OPTION_DEFAULTS - CFG-03's fourth place. test_defaults_
# pin (1)
# M19 the injection leg's JSON-error hint pointed at +75 B 17b2 DIFF row
# the CONSUMPTION leg's rule ("leave it empty for invalid
# unless price_provider is 'custom_formula'"), JSON
# which is false of the leg you are PAID on.
# M20 ⚠️ RECORDED FOR ITS SHAPE, NOT AS A WIN. -23 B test_price.py
# export_price_eur_kwh() rerouted through dies, and
# export_price_at(). Because the seam falls BACK names NOTHING
# to the scalar the two recurse, so test_price.py
# dies with a RecursionError and ZERO named
# checks. That is habit 5 in the small: a failure
# with no name attached reads like broken
# infrastructure. M18 is the same property
# mutated in a way a check can NAME.
#
# ⚠️ WHAT NO ROW ABOVE COULD SEE, with its shape (habit 13):
# * 17b's pin grades WHERE the grammar is defined, never WHAT it means -
# rewriting `implausible_injection` in place to accept anything passes it
# (17c and 17d grade the meaning), and it covers `app/` only: a second
# parser under `tools/` or in a check file is outside it;
# * 17h grades that no module under `app/` CALLS an injection reader. A
# strategy that reached the option through `price_option(opts, ...)`
# directly, or through a name this scan resolves differently, is outside
# it - the scan is keyed by called NAME and therefore over-approximates,
# which makes "nothing calls them" the strong answer and a hit the weak
# one;
# * nothing here grades the DOCS.md injection section, which is prose. Every
# number it quotes is one 17a, 17c, 17d or 17e prints on every run, so a
# reader can check them, but no assertion holds them;
# * and the deepest gap is the one 17e's last check MEASURES rather than
# mentions: with no printed estimate there is no check anywhere that can
# tell a right pair of injection coefficients from a wrong one. A `factor`
# of 0.070 where the contract says 0.100 - 30% of every export payment -
# passes both bounds, because 0.70x is a pass-through somebody publishes.
# --------------------------------------------------------------------------- #
# TARIFF-17's MUTATION LEDGER. Every row was RUN, not reasoned about, and the
# method is MUTATION.md's. ⚠️ NO ROW HERE IS AN EDIT-AND-REVERT: the harness
# (`/home/homelab/.cache/t17-mut/mut.sh`) rsyncs a FRESH COPY of the tree per
# mutation, excluding `__pycache__`, mutates the copy, mounts it `:ro` into the
# checks image and deletes it. There is no revert to fail and no shared backup
# path, which is habit 18 closed structurally rather than by discipline; and a
# `:ro` tree with no cache in it cannot read a stale `.pyc`, which is TEST-15
# closed the way CI closes it. Each row's byte delta and the mutant/clean
# sha256 pair were printed by the harness before the suites ran.
#
# # what was changed delta red on
# --- ------------------------------------------------- ----- ---------------
# M1 assemble() hoists the table lookup out of the +17 B 16c 16d 16e 16k
# loop (`taxes_at(taxes, brussels(raw[0].start))`) - THE headline
# -> the LEVEL bucket wearing a period table. mutation. FULL
# 29-FILE SWEEP:
# red on
# test_price.py,
# GREEN in the
# other 28.
# M2 taxes_at() returns `periods[0]`'s value instead +17 B 16c 16d 16e 16k
# of the point's own band.
# M3 "price_taxes_profile" removed from -27 B 16g (5 checks)
# FRAMEWORK_KEYS.
# M4 load_prices() parses the table and never applies +10 B 16k ONLY - the
# it (`if table is not None and False`). reason 16k is
# a separate
# check: every
# other TARIFF-17
# check stays
# green while no
# site can
# configure one.
# M5 validate.py never reads the option. -32 B test_validate
# (6 checks)
# M6 the two-writers refusal (table + non-default -1 B test_validate
# scalar) is made unreachable. (2 checks)
# M7 the cents/units rung's threshold 1.0 -> 1000.0. +3 B test_validate,
# the SIBELGA
# line ONLY - the
# ORES line goes
# on passing for
# the PARSER's
# reason, which
# is what grading
# the reason
# buys.
# M8 ⚠️ TEST-16's rule: BOTH rungs that can answer +3 B test_validate
# the cents stimulus, TOGETHER - the units rung (+ 3) (3 checks),
# AND price.py's MAX_PLAUSIBLE_EUR_KWH. test_price,
# Neither masks the other. test_static
# M9 a SECOND period grammar: `_check_tiling` and +86 B test_static 10a
# `MINUTES_PER_DAY` copied into price.py. (2 checks)
# M10 parse_taxes_profile() drops `option=`, so every -61 B test_static 10b,
# refusal names `static_profile`. test_price 16j.
# ⚠️ GREEN in
# test_validate -
# that file's
# own prefix
# supplies the
# key, so it
# cannot see
# this. Named
# rather than
# rounded off.
# M11 StaticProfile.at() matches on the UTC clock +15 B 16e, 16i and
# instead of Brussels wall time. test_static
# (5 checks)
# M12 _price() ignores its `means` argument. -26 B test_static 10b
# M13 assemble() adds the markup unconditionally, -42 B 16f and
# ignoring caps.adds. section 3
# (6 checks)
#
# ⚠️ WHAT NO ROW ABOVE COULD SEE, with its shape (habit 13):
# * passing `p.start` (UTC) to `taxes_at()` instead of `local` is an
# EQUIVALENT mutant, not a survivor: `StaticProfile.at()` calls
# `brussels()` on whatever it is given, so the two are the same instant by
# construction and no fixture can separate them;
# * 10a grades WHERE the grammar is defined, never WHAT it means - rewriting
# `_check_tiling` in place to accept a gap passes it (section 5 and 10b are
# what grade the meaning), and it covers `app/` only;
# * nothing here grades the DOCS.md bucket table, which is prose. The numbers
# it quotes are the ones 16b and 16c print on every run, so a reader can
# check them, but no assertion holds them.
print()
if fails:
print(f"{len(fails)} FAILED: " + "; ".join(fails))
sys.exit(1)
print("all checks passed")