TARIFF-02 review: close the three other doors to the markup, and stop overclaiming
checks / unit (pull_request) Successful in 2m34s

Review of PR #16 wrote a double-counting plugin four ways. PluginOptions
blocked one. The one that decided it touched nothing but
PRICE_OPTION_DEFAULTS - a public dict in the module every plugin must
already import for register() - so "the number is not in scope" was
simply false, and an absolute guarantee a reviewer trusts is worse than
a stated limit, because they stop checking.

register() now refuses, AT IMPORT, any plugin whose own code mentions a
framework-owned name: FRAMEWORK_KEYS plus PRICE_OPTION_DEFAULTS,
price_option and _o. code_leaks() parses the class and drops every
docstring node before searching, because every plugin is REQUIRED to
name its sourced components in prose - engie_dynamic says "markup" in
its docstring and a raw text scan false-positives on the documentation
the same file mandates. A check that fires on required documentation is
a check somebody deletes. test_price.py asserts that a raw scan really
would have false-positived, so the strip is proven load-bearing rather
than asserted.

It fails OPEN if inspect.getsource() cannot find the .py, which the
add-on image always can. Refusing to start a battery controller because
a static analysis could not run would trade a real outage for a
hypothetical accounting error - so instead the checks assert source was
found for all five shipped plugins, and a build that lost it is a red
check rather than a quietly disarmed guard.

All three reachable attacks are written out as plugins in the suite and
each must be refused at registration. The cost is measured, not
asserted: honest wholesale 0.188, every attack 0.219 - one markup high,
every hour, invisible in every log this add-on writes.

The guarantee is now stated at its true size everywhere it appears: the
framework adds the complement exactly once, GIVEN A CORRECT sources
DECLARATION. A plugin that declares {ENERGY} while its feed is all-in
still doubles while touching no framework name, and no code can catch
that - the declaration is an unverifiable claim about somebody else's
tariff. Measured in the suite (0.276) so the limit is a number rather
than a caveat. The docstring/declaration check verifies that a plugin's
two claims AGREE, not that either is true; that now says so.

The three surviving mutations:

M3 - _NoData's reason taking a default. This is the identical survivor
left open on RIG-01's _Unread.cause: the shape was borrowed from
sim/riglock.py including its blind spot, which is what borrowing a shape
does if nobody re-runs the mutations against the copy. Closed by
SIGNATURE - no constructor or handler of an outcome type may carry a
default - so the next one is red without anyone writing a new case.

M5 - the ENERGY-required guard was untested. Its consequence is that
adds becomes {ENERGY, TAXES} and the emitted price silently omits the
energy it is pricing.

M7 - deleting the TLS refusal went green AND the check named for it
still passed, because the old parser stripped a fixed len("https://")
and mangled http://api.energy-charts.info/p into the host
pi.energy-charts.info, which then failed the ALLOW-LIST. An assertion
that does not test what it names, inside a security check. Fixed on both
sides: the scheme is parsed as a scheme, so an http URL now resolves to
its real and declared host and only the TLS rule can refuse it; and the
check asserts the REASON. Fixing it surfaced M7b - the no-scheme guard
was survivable the same way - found by mutating my own fix, and closed
the same way.

Also carried forward: rule 7's refusal is correct forever but its stated
reason ("the provider decides nothing") expires the moment TARIFF-03 or
OPT-04 lands. test_price.py section 12 now greps app/ by AST for a
load_prices consumer and goes red when one appears, telling whoever
lands it to update the message in the same commit. The first draft of
that tripwire used a substring and matched the comment in validate.py
explaining the tripwire - the same false-positive class as the plugin
docstrings, hit twice in one ticket, and fixed the same way.

Documented rather than coded around: vars(read).get("points", []) and
bool(read) both still get past the absence type, and neither is worth
code - defining __bool__ would turn a line that reads like a guard into
one that IS a guard by accident, which is how .get(k, default) got here
in the first place.

Stated plainly in DOCS/CHANGELOG/PR: every fixture is synthetic, so the
five feed URLs and JSON shapes are verified against a belief about what
those suppliers publish rather than against the feeds. A wrong shape
degrades to no-price-data, not to a wrong price.

Review's own DST verification recorded: 149,040 hourly instants,
2020-2036, zero mismatches against zoneinfo. The residual is political,
not arithmetic, and the docstring now says so.

24 mutations driven through the suite, 0 survivors. Whole add-on suite
green. No deleted lines outside this ticket's own files; main.py still
untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015VXjA3LWccN1rJsk3okyPC
This commit is contained in:
2026-08-30 19:35:36 +02:00
co-authored by Claude Opus 5
parent 852fc7ced6
commit e102cde841
5 changed files with 531 additions and 37 deletions
+31 -9
View File
@@ -10,15 +10,30 @@
consumes it yet**: this is the seam, landed first, so that the arbitrage work
that follows never learns a supplier's name.
**Double-counting the markup is unreachable rather than discouraged.** A plugin
does not return a price; it returns the figure its source published and
declares which of {energy, markup, taxes} that figure contains. The framework
adds the complement, once. A plugin is never handed `price_markup_eur_kwh` at
all - `PluginOptions` raises on it - so the plugin that adds the margin a second
time cannot be written. Engie and TotalEnergies publish a retail figure with
their margin already inside; adding the configured 3 c/kWh on top would have
made every future arbitrage decision wrong by that amount with nothing
reporting it.
**Double-counting the markup is refused rather than discouraged, given a
correct declaration.** A plugin does not return a price; it returns the figure
its source published and declares which of {energy, markup, taxes} that figure
contains. The framework adds the complement, once. Engie and TotalEnergies
publish a retail figure with their margin already inside; adding the configured
3 c/kWh on top would make every future arbitrage decision wrong by that amount
with nothing reporting it.
Two mechanisms, because one was not enough. `PluginOptions` raises on the
framework-owned keys - and review of PR #16 then 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 already imports. So `register()` now also
**refuses at import any plugin whose own code mentions a framework key**
(`code_leaks()`, docstrings stripped via `ast` so a plugin's mandated
`SOURCES:` line does not false-positive). Honest wholesale baseline 0.188
EUR/kWh; every attack landed 0.219.
**The guarantee's precise shape**, stated because an overclaim is worse than no
claim: the framework adds the complement exactly once *given a correct `sources`
declaration*. A plugin that declares `{energy}` while its feed is really all-in
still comes out one markup high while touching no framework name, and no code
can catch that - the declaration is the plugin's unverifiable claim about
somebody else's tariff. What is mechanised is reachability; what stays human is
truthfulness.
**The product type is declared, not inferred from the plugin's name.**
`eneco_eddy` is a real-time/imbalance product and gets widened arbitrage
@@ -38,6 +53,13 @@ pointless under the first and forbidden under the second - and an implementation
that returns `[]` on a failed fetch cannot tell them apart. Every site-level
failure returns `no_data`; nothing raises into the control tick.
**What is not verified, and cannot be by an offline suite:** every fixture is
synthetic, so the five feed URLs and JSON shapes are checked against a belief
about what those suppliers publish, not against the feeds. The parse layer is
exercised; the contract with each supplier is not. Expect the first live fetch
of each to need a shape fix, and note that a wrong shape degrades to
no-price-data rather than to a wrong price.
**`manual_csv` is the documented offline fallback** and needs no network, no
supplier account and no HTTP stack. See DOCS.md for the format. Its `start`
column must carry a UTC offset - a bare local timestamp is refused, because one