TARIFF-17: a price component that varies by hour has somewhere to live
checks / unit (pull_request) Successful in 7m25s

`price.py` modelled the whole non-energy part of a Belgian bill as ONE flat
scalar. This adds `price_taxes_profile`, which carries the TAXES component as
a period table, and makes `assemble()` evaluate it at each point's own instant.

⚠️ THE FLAT SCALAR WAS NOT A WRONG NUMBER, AND THIS IS NOT A CORRECTION OF ONE.
The optimiser reads exactly two quantities off a series - the spread
`max - min` over the firm points, and which hour is cheapest - and an additive
constant identical in every hour cancels out of the first and leaves the second
where it was. It is provably inert. Measured, and printed by test_price.py 16b
on every run: with a flat taxes figure of 0.00, 0.0739, 0.1198, 0.1327 or 1.00
EUR/kWh the same 24-hour feed is cheapest at 13:00 in all five cases and its
spread is 0.1500 EUR/kWh in all five cases. What was missing is anywhere for an
HOUR-VARYING non-energy component to live. This is a missing capability, not a
bad constant.

Belgium has one. Fluvius distribution is a single rate - its tariff-card row
reads `Normaal | Uitsl. nacht`, where exclusive night is a separate physical
circuit and not a night register - so nothing changes for the reference site in
Flanders. The Walloon DSOs and Sibelga are on the same card with four columns,
`Enkelvoudig | Dag | Nacht | Uitsl. nacht`, and the gap is real money (c/kWh):
ORES 13,27 / 7,39; AIESH 15,17 / 8,21; Tecteo RESA 12,20 / 7,01; Sibelga
9,39 / 7,10; ORES IMPACT 2026 16,578 / 5,090.

⚠️ AND THE BANDS ARE ANTI-CORRELATED WITH WHAT THE BATTERY WANTS TO DO. The
cheap network hours are the cheap spot hours, so flattening them does not shift
the curve - it UNDERSTATES the spread and declines cycles that would have paid.
Same feed, same markup, the only difference being the taxes argument:

    flat 0.0000 / 0.0739 / 0.1198 / 0.1327 / 1.0000
                                       -> cheapest hour 13, spread 0.1500
    ORES dag 0.1327 / nacht 0.0739     -> cheapest hour 03, spread 0.1788

The regression check grades WHICH HOUR IS CHEAPEST, not the spread: an
implementation that reads the table once and adds one number to every point is
the LEVEL bucket wearing a period table - right magnitude, wrong shape - and a
spread assertion passes on it. Measured: hoisting the lookup out of
`assemble()`'s loop (+17 B) is red on six named checks in test_price.py and
GREEN in the other 28 files.

ONE GRAMMAR, NOT TWO. `static_tariff.py` already shipped the periods, the day
selectors, the tiling rule and the DST handling, so `parse_taxes_profile()` is
the same parser with the option name and the "what this number means" clause as
arguments. A second tiling rule would be a second set of 23- and 25-hour-day
bugs. test_static_tariff.py section 10 pins that two ways: by parsing every
module under `app/` and asserting the grammar is defined in exactly one of
them, and by deriving each of thirteen refusal sentences for the new option
from the old option's own sentence rather than transcribing them.

`Component.TAXES` semantics, `code_leaks()` and the double-counting guarantee
are untouched. The new key joins FRAMEWORK_KEYS, BANNED_IN_PLUGIN_CODE is
derived from that set, and a plugin that names it is refused at `register()`.
It reaches both plugins that take taxes - `custom_formula` ({ENERGY, MARKUP})
and `generic_dayahead` ({ENERGY}) - and not `manual_csv`, which is all-in: the
table is routed through `capabilities().adds`, exactly as the scalar is.

⚠️ A MALFORMED TABLE IS REFUSED AT STARTUP, NOT AT THE NEXT FETCH. It is read
on the ~30 s planning path, and a table that first fails at 03:00 is a site
that stops pricing at 03:00, which §11.4 turns into a quiet fallback
indistinguishable from a healthy fixed-tariff day. validate.py refuses three
things: a table that does not tile the day; a table set beside a NON-DEFAULT
`price_taxes_eur_kwh`, because the table wins and the scalar would then be a
number the operator typed that decides nothing; and a whole card typed in
CENTS, which is how these figures are printed. That last rung is not a
restatement of the parser's +-10 EUR/kWh bound - ORES's 13,27 trips the parser,
but Sibelga's entire card, 9,39 and 7,10, is under it and would otherwise have
started as a 9.39 EUR/kWh levy. Both rungs are graded by the REASON each gives,
and mutating them together (TEST-16) moves three rows.

NOTHING CHANGES FOR A SITE THAT DOES NOT SET IT. The option ships "", and the
scalar path is unchanged arithmetically rather than merely in intent:
test_price.py 16f reproduces the pre-TARIFF-17 expression and compares 12
capability/scalar combinations point for point with `==`.

13 mutations, every one red on a named check, ledger in test_price.py below
section 16k with byte deltas and the one equivalent mutant named. 29/29 green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU
This commit is contained in:
glenn schrooyen
2026-09-13 15:31:15 +02:00
co-authored by Claude Opus 5
parent c518007caf
commit 2fac0aaf06
12 changed files with 1485 additions and 54 deletions
+577 -1
View File
@@ -68,6 +68,7 @@ from app.price import (
brussels_offset, check_egress, dated_feed_url,
price_option,
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 (
@@ -80,7 +81,8 @@ 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, static_status
from app.static_tariff import (
TariffPlan, parse_taxes_profile, static_status)
from app.validate import validate_startup_config
fails = []
@@ -4465,6 +4467,580 @@ check("...and this is the same wording FCST-04's *Price history* row already "
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))
check(f"the two arms are the same feed: same caps, same {len(_T17_RAW)} "
f"RawPoints and the same markup {MARKUP}, by IDENTITY and not by value",
_T17_CAPS is _T17_CAPS and _T17_RAW is _T17_RAW)
_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)
# --------------------------------------------------------------------------- #
# 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))