price.py modelled the injection price - what a site is paid for an
exported kWh - as one scalar. 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
price_export_formula carries that line, with its own coefficients, in price_formula's grammar.
⚠️ This is NOT a wrong-trade fix, and that matters for how it is read
arbitrage.py does not read export_price_eur_kwh at all - its own comment
says so, citing §9.3's near-zero Belgian injection price as the reason the
economically sound trade on this market is charge cheap to displace expensive
import. Both hours it compares are all-in consumption prices. No trade
this add-on takes today is decided by an injection price, and nothing in this
PR changes one.
What it changes is what an operator is told, and what a future export-aware
strategy rests on: OPT-06 and any export logic would otherwise inherit a scalar
that cannot represent the market, and the 6% VAT asymmetry between the two legs
would be invisible in the arithmetic. test_price.py 17h parses every module
under app/ and goes red the day something calls an injection reader, so that
sentence is graded rather than attested.
The four properties a scalar cannot hold
Its own factor. Eneco 0,102 consumption against 0,100 injection;
Aspiravi 0,07 against ~0,10. Not the consumption B with a sign flip.
A negative offset - - 1,188, - 2. price_markup_eur_kwh: float(0,1)
cannot express a negative markup at all.
VAT-exempt - "Injectietarieven zijn vrijgesteld van btw" - against 6%
on consumption. vat_pct is per leg, so the asymmetry is now expressible.
It goes negative with no floor, below 11,88 EUR/MWh of BELPEX-H - the
midday hours a PV household exports in. You can be charged to export.
⚠️ A negative injection price is ACCEPTED, and that needed a different band
implausible_all_in() floors the consumption leg at +0.02 EUR/kWh of energy.
Applied to injection that refuses the real market, in the hours it matters most. Measured, not assumed:test_price.py 17c puts ONE spec through BOTH bands
and prints that the consumption band refuses -0.03008 EUR/kWh while the
injection band accepts it.
The floor was not decoration, though - it is what catches a card read a hundred
times too small, the one unit error the card self-check cannot see because it
cancels out of a comparison between the two sides. So the bound moves off the
price and onto the coefficients, where it is index-free and sign-free:
pass-through - factor as a share of the wholesale price. Eneco's
injection leg is 1.00x, Aspiravi's 0.70x; the band is 0.10-10x.
offset magnitude - |offset| <= 0.20 EUR/kWh against published values of 0.01188 and 0.02.
Two rungs that can both answer a hundredfold error, so each is graded on its reason, each gets a stimulus the other cannot reach (preconditions asserted
as their own checks), and they are mutated together - TEST-16.
⚠️ The card self-check where there is a card, and a STATED gap where there is not
price_formula can require a card block because a Belgian card prints the
consumption formula, the reference index and the resulting price. Eneco's injection formula is printed with no price beside it. Requiring a
block would make the market's own published line inexpressible; inventing the
estimate would grade the coefficients against arithmetic done with the same
coefficients. So the block is optional on this leg and unchanged on the
other, the self-check runs at the same tolerance wherever a printed estimate
exists, and validate.py says at startup - "its coefficients are UNGRADED" -
when it does not, naming the two bounds that ran instead. This is TARIFF-15
demonstrating two tariffs end to end rather than three, one card later.
test_price.py 17e also states the size of that gap: with no printed estimate,
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 really
publishes.
ONE parser, not two
parse_export_formula() is parse_price_formula() with its option name as an
argument - TARIFF-17's move on parse_static_profile(), exactly. Everything
that differs between the legs is a string in one _LEGS table. Pinned two ways:
all 32 grammar names are defined in exactly one module under app/ (scope
computed by parsing every file, not by a list somebody typed), and twelve
refusal sentences for the new option are derived from the old option's own
sentence rather than transcribed. The three sentences that deliberately differ
are named, with what differs, and asserted to differ.
The scalar path is unchanged - arithmetically
export_price_eur_kwh()'s body is byte-identical, pinned on the AST rather
than on the source text. The option ships "", export_price_at() falls
through to the scalar for every index value, and 17f compares seven option dicts
x five index values against the pre-TARIFF-16 expression with ==.
Also corrected here
config.yaml's comment beside export_price_eur_kwh still read "no shipped
supplier publishes a dynamic injection curve". TARIFF-07 struck that claim from app/price.py and DOCS.md and did not reach config.yaml, where it has been
shipping since.
Checks
price_export_formula joins FRAMEWORK_KEYS; BANNED_IN_PLUGIN_CODE is
derived from that set, so code_leaks() refuses a plugin that names it with
nothing else edited.
29/29 green.
20 mutations, every one red on a named check - ledger in test_price.py
below section 17, with byte deltas. Two are worth naming:
M16, zero bytes: the two legs transposed in the VAT-asymmetry banner. No
decision the add-on takes moves - the only surface that moves is the sentence,
which now tells the operator their VAT treatment exactly backwards. Red on the
ordered-pair check and on nothing else (MUTATION.md habit 14).
M5, the headline: implausible_injection() given TARIFF-15's floor.
Eneco's real published line stops starting. Full 29-file sweep: red on test_price.py, green in the other 28. M10 and M12 were run the same way.
Each row gets a fresh copy of the tree under a mktemp path, mounted :ro - no
revert to fail and no shared backup path, which is MUTATION.md habit 18 closed
structurally rather than by discipline. That design was exercised rather than
admired: two copies of the sweep ran concurrently at one point, and because each
holds its own tree neither could restore over the other's work.
⚠️What no row could see, with its shape - and the deepest one is measured
rather than mentioned: with no printed estimate there is no check anywhere that
can tell a right pair of injection coefficients from a wrong one. test_price.py
17e's last check demonstrates it.
`price.py` modelled the injection price - what a site is **paid** for an
exported kWh - as one scalar. 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
```
`price_export_formula` carries that line, with **its own** coefficients, in
`price_formula`'s grammar.
## ⚠️ This is NOT a wrong-trade fix, and that matters for how it is read
`arbitrage.py` does not read `export_price_eur_kwh` at all - its own comment
says so, citing §9.3's near-zero Belgian injection price as the reason the
economically sound trade on this market is *charge cheap to displace expensive
import*. Both hours it compares are all-in **consumption** prices. **No trade
this add-on takes today is decided by an injection price, and nothing in this
PR changes one.**
What it changes is what an operator is told, and what a future export-aware
strategy rests on: OPT-06 and any export logic would otherwise inherit a scalar
that cannot represent the market, and the 6% VAT asymmetry between the two legs
would be invisible in the arithmetic. `test_price.py` 17h parses every module
under `app/` and goes red the day something calls an injection reader, so that
sentence is graded rather than attested.
## The four properties a scalar cannot hold
1. **Its own factor.** Eneco `0,102` consumption against `0,100` injection;
Aspiravi `0,07` against ~`0,10`. Not the consumption `B` with a sign flip.
2. **A negative offset** - `- 1,188`, `- 2`. `price_markup_eur_kwh: float(0,1)`
cannot express a negative markup at all.
3. **VAT-exempt** - *"Injectietarieven zijn vrijgesteld van btw"* - against 6%
on consumption. `vat_pct` is per leg, so the asymmetry is now expressible.
4. **It goes negative with no floor**, below `11,88 EUR/MWh` of BELPEX-H - the
midday hours a PV household exports in. You can be charged to export.
## ⚠️ A negative injection price is ACCEPTED, and that needed a different band
`implausible_all_in()` floors the consumption leg at `+0.02 EUR/kWh` of energy.
Applied to injection that refuses the real market, in the hours it matters most.
**Measured, not assumed:** `test_price.py` 17c puts ONE spec through BOTH bands
and prints that the consumption band refuses `-0.03008 EUR/kWh` while the
injection band accepts it.
The floor was not decoration, though - it is what catches a card read a hundred
times too small, the one unit error the card self-check cannot see because it
cancels out of a comparison between the two sides. So the bound moves **off the
price and onto the coefficients**, where it is index-free and sign-free:
* **pass-through** - `factor` as a share of the wholesale price. Eneco's
injection leg is `1.00x`, Aspiravi's `0.70x`; the band is `0.10-10x`.
* **offset magnitude** - `|offset| <= 0.20 EUR/kWh` against published values of
`0.01188` and `0.02`.
Two rungs that can both answer a hundredfold error, so each is graded on its
**reason**, each gets a stimulus the other cannot reach (preconditions asserted
as their own checks), and they are mutated **together** - TEST-16.
## ⚠️ The card self-check where there is a card, and a STATED gap where there is not
`price_formula` can require a `card` block because a Belgian card prints the
consumption formula, the reference index **and** the resulting price.
**Eneco's injection formula is printed with no price beside it.** Requiring a
block would make the market's own published line inexpressible; inventing the
estimate would grade the coefficients against arithmetic done with the same
coefficients. So the block is **optional on this leg and unchanged on the
other**, the self-check runs at the same tolerance wherever a printed estimate
exists, and validate.py says at startup - *"its coefficients are UNGRADED"* -
when it does not, naming the two bounds that ran instead. This is TARIFF-15
demonstrating two tariffs end to end rather than three, one card later.
`test_price.py` 17e also states the size of that gap: with no printed estimate,
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 really
publishes.
## ONE parser, not two
`parse_export_formula()` is `parse_price_formula()` with its **option name as an
argument** - TARIFF-17's move on `parse_static_profile()`, exactly. Everything
that differs between the legs is a string in one `_LEGS` table. Pinned two ways:
all **32** grammar names are defined in exactly one module under `app/` (scope
computed by parsing every file, not by a list somebody typed), and twelve
refusal sentences for the new option are **derived** from the old option's own
sentence rather than transcribed. The three sentences that deliberately differ
are named, with what differs, and asserted to differ.
## The scalar path is unchanged - arithmetically
`export_price_eur_kwh()`'s body is byte-identical, pinned on the **AST** rather
than on the source text. The option ships `""`, `export_price_at()` falls
through to the scalar for every index value, and 17f compares seven option dicts
x five index values against the pre-TARIFF-16 expression with `==`.
## Also corrected here
`config.yaml`'s comment beside `export_price_eur_kwh` still read *"no shipped
supplier publishes a dynamic injection curve"*. TARIFF-07 struck that claim from
`app/price.py` and `DOCS.md` and did not reach `config.yaml`, where it has been
shipping since.
## Checks
`price_export_formula` joins `FRAMEWORK_KEYS`; `BANNED_IN_PLUGIN_CODE` is
derived from that set, so `code_leaks()` refuses a plugin that names it with
nothing else edited.
**29/29 green.**
**20 mutations, every one red on a named check** - ledger in `test_price.py`
below section 17, with byte deltas. Two are worth naming:
* **M16, zero bytes**: the two legs transposed in the VAT-asymmetry banner. No
decision the add-on takes moves - the only surface that moves is the sentence,
which now tells the operator their VAT treatment exactly backwards. Red on the
ordered-pair check and on nothing else (MUTATION.md habit 14).
* **M5, the headline**: `implausible_injection()` given TARIFF-15's floor.
Eneco's real published line stops starting. **Full 29-file sweep: red on
`test_price.py`, green in the other 28.** M10 and M12 were run the same way.
Each row gets a fresh copy of the tree under a `mktemp` path, mounted `:ro` - no
revert to fail and no shared backup path, which is MUTATION.md habit 18 closed
structurally rather than by discipline. That design was exercised rather than
admired: two copies of the sweep ran concurrently at one point, and because each
holds its own tree neither could restore over the other's work.
⚠️ **What no row could see, with its shape** - and the deepest one is measured
rather than mentioned: with no printed estimate there is no check anywhere that
can tell a right pair of injection coefficients from a wrong one. `test_price.py`
17e's last check demonstrates it.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU
`price.py` modelled what a site is PAID for export as ONE scalar. 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 `0,07 x Belpex - 2`.
`price_export_formula` carries that line with its OWN coefficients.
⚠️ THIS IS NOT A WRONG-TRADE FIX, AND SAYING SO IS PART OF SHIPPING IT.
`arbitrage.py` does not read `export_price_eur_kwh` at all - its own comment
says so, citing §9.3's near-zero Belgian injection price as the reason the
economically sound trade on this market is charge-cheap-to-displace-expensive-
IMPORT. Both hours it compares are all-in CONSUMPTION prices. No trade this
add-on takes today is decided by an injection price and nothing here changes
one. What it changes is what an operator is told, and what a future
export-aware strategy rests on: OPT-06 and any export logic would otherwise
inherit a scalar that cannot represent the market, and the 6% VAT asymmetry
would be invisible in the arithmetic. test_price.py 17h parses every module
under app/ and goes red the day something calls an injection reader, so that
sentence is graded rather than attested.
FOUR PROPERTIES A SCALAR CANNOT HOLD. Its own factor (Eneco 0,102 consumption
against 0,100 injection; Aspiravi 0,07 against ~0,10 - not the consumption B
with a sign flip). A negative offset, which `price_markup_eur_kwh: float(0,1)`
cannot express at all. VAT-exemption - "injectietarieven zijn vrijgesteld van
btw" - against 6% on consumption, representable only because `vat_pct` is PER
SPEC. And a price that goes below zero with no floor, under 11,88 EUR/MWh of
BELPEX-H, which is the middle of a sunny day.
⚠️ A NEGATIVE INJECTION PRICE IS ACCEPTED, AND THAT NEEDED A DIFFERENT BAND
RATHER THAN A LOWER FLOOR. `implausible_all_in()` floors the consumption leg at
+0.02 EUR/kWh of energy; applied to injection that refuses the real market in
the hours it matters most. Measured rather than argued: 17c puts ONE spec
through BOTH bands and prints that the consumption band refuses -0.03008
EUR/kWh while the injection band accepts it. The floor was not decoration - it
is what catches a card read a hundred times too small, the one unit error the
card self-check cannot see because it cancels out of a comparison between the
two sides - so the bound moves OFF the price and ONTO the coefficients, where
it is index-free and sign-free: a pass-through band (Eneco's injection leg is
1.00x, Aspiravi's 0.70x; the band is 0.10-10x) and an offset magnitude bound
(0.20 EUR/kWh against published 0.01188 and 0.02). Two rungs that can both
answer a hundredfold error, so each is graded on its REASON, each gets a
stimulus the other cannot reach with the precondition asserted as its own
check, and they are mutated TOGETHER (TEST-16).
⚠️ THE CARD SELF-CHECK APPLIES WHERE THERE IS A CARD, AND WHERE THERE IS NOT
THAT IS STATED RATHER THAN WORKED AROUND. `price_formula` can require a `card`
block because a Belgian card prints the consumption formula, the reference
index AND the resulting price. Eneco's INJECTION formula is printed with no
price beside it. Requiring a block would make the published line inexpressible;
inventing the estimate would grade the coefficients against arithmetic done
with the same coefficients. So the block is OPTIONAL on this leg and unchanged
on the other, the self-check runs at the same tolerance wherever a printed
estimate exists, and validate.py says at startup - "its coefficients are
UNGRADED" - when it does not, naming the two bounds that ran instead. This is
TARIFF-15 demonstrating two tariffs end to end rather than three, one card
later. 17e also MEASURES the size of that gap: 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 really publishes.
ONE PARSER, NOT TWO. `parse_export_formula()` is `parse_price_formula()` with
its OPTION NAME as an argument - TARIFF-17's move on `parse_static_profile()`,
exactly - and everything that differs between the legs is a string in one
`_LEGS` table. A second copy would be a second unit table, a second NaN rule, a
second unknown-key rule and a second `to_resolution()`, diverging first on the
23- and 25-hour days. test_price.py 17b pins it two ways: all 32 grammar names
are defined in exactly ONE module under app/, with the scope COMPUTED by
parsing every file rather than listed; and twelve refusal sentences for the new
option are DERIVED from the old option's own sentence rather than transcribed.
The four sentences that deliberately differ are named, with what differs, and
asserted to differ.
NOTHING CHANGES FOR A SITE THAT DOES NOT SET IT. The option ships "";
`export_price_eur_kwh()`'s body is byte-identical, pinned on the AST rather
than on the source text; `export_price_at()` falls through to it for every
index value. 17f compares seven option dicts x five index values against the
pre-TARIFF-16 expression with `==`.
`price_export_formula` joins FRAMEWORK_KEYS, BANNED_IN_PLUGIN_CODE is derived
from that set, and `code_leaks()` refuses a plugin that names it with nothing
else edited.
ALSO CORRECTED HERE: config.yaml's comment beside `export_price_eur_kwh` still
read "no shipped supplier publishes a dynamic injection curve". TARIFF-07
struck that claim from app/price.py and DOCS.md and did not reach config.yaml,
where it has been shipping since.
20 mutations, every one red on a named check, ledger in test_price.py below
section 17 - including a ZERO-byte one (the two legs transposed in the
VAT-asymmetry banner: no decision moves, only the sentence) and a ONE-byte one
(`card_required=True` on the injection leg). Three run over all 29 files and
red on test_price.py alone. 29/29 green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU
admin
merged commit 478ba93710 into release/1.02026-09-13 18:51:26 +02:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
price.pymodelled the injection price - what a site is paid for anexported kWh - as one scalar. The market publishes a line, and the cards say so
in their own words:
price_export_formulacarries that line, with its own coefficients, inprice_formula's grammar.⚠️ This is NOT a wrong-trade fix, and that matters for how it is read
arbitrage.pydoes not readexport_price_eur_kwhat all - its own commentsays so, citing §9.3's near-zero Belgian injection price as the reason the
economically sound trade on this market is charge cheap to displace expensive
import. Both hours it compares are all-in consumption prices. No trade
this add-on takes today is decided by an injection price, and nothing in this
PR changes one.
What it changes is what an operator is told, and what a future export-aware
strategy rests on: OPT-06 and any export logic would otherwise inherit a scalar
that cannot represent the market, and the 6% VAT asymmetry between the two legs
would be invisible in the arithmetic.
test_price.py17h parses every moduleunder
app/and goes red the day something calls an injection reader, so thatsentence is graded rather than attested.
The four properties a scalar cannot hold
0,102consumption against0,100injection;Aspiravi
0,07against ~0,10. Not the consumptionBwith a sign flip.- 1,188,- 2.price_markup_eur_kwh: float(0,1)cannot express a negative markup at all.
on consumption.
vat_pctis per leg, so the asymmetry is now expressible.11,88 EUR/MWhof BELPEX-H - themidday hours a PV household exports in. You can be charged to export.
⚠️ A negative injection price is ACCEPTED, and that needed a different band
implausible_all_in()floors the consumption leg at+0.02 EUR/kWhof energy.Applied to injection that refuses the real market, in the hours it matters most.
Measured, not assumed:
test_price.py17c puts ONE spec through BOTH bandsand prints that the consumption band refuses
-0.03008 EUR/kWhwhile theinjection band accepts it.
The floor was not decoration, though - it is what catches a card read a hundred
times too small, the one unit error the card self-check cannot see because it
cancels out of a comparison between the two sides. So the bound moves off the
price and onto the coefficients, where it is index-free and sign-free:
factoras a share of the wholesale price. Eneco'sinjection leg is
1.00x, Aspiravi's0.70x; the band is0.10-10x.|offset| <= 0.20 EUR/kWhagainst published values of0.01188and0.02.Two rungs that can both answer a hundredfold error, so each is graded on its
reason, each gets a stimulus the other cannot reach (preconditions asserted
as their own checks), and they are mutated together - TEST-16.
⚠️ The card self-check where there is a card, and a STATED gap where there is not
price_formulacan require acardblock because a Belgian card prints theconsumption formula, the reference index and the resulting price.
Eneco's injection formula is printed with no price beside it. Requiring a
block would make the market's own published line inexpressible; inventing the
estimate would grade the coefficients against arithmetic done with the same
coefficients. So the block is optional on this leg and unchanged on the
other, the self-check runs at the same tolerance wherever a printed estimate
exists, and validate.py says at startup - "its coefficients are UNGRADED" -
when it does not, naming the two bounds that ran instead. This is TARIFF-15
demonstrating two tariffs end to end rather than three, one card later.
test_price.py17e also states the size of that gap: with no printed estimate,a
factorof0.070where the contract says0.100- 30% of every exportpayment - passes both bounds, because
0.70xis a pass-through somebody reallypublishes.
ONE parser, not two
parse_export_formula()isparse_price_formula()with its option name as anargument - TARIFF-17's move on
parse_static_profile(), exactly. Everythingthat differs between the legs is a string in one
_LEGStable. Pinned two ways:all 32 grammar names are defined in exactly one module under
app/(scopecomputed by parsing every file, not by a list somebody typed), and twelve
refusal sentences for the new option are derived from the old option's own
sentence rather than transcribed. The three sentences that deliberately differ
are named, with what differs, and asserted to differ.
The scalar path is unchanged - arithmetically
export_price_eur_kwh()'s body is byte-identical, pinned on the AST ratherthan on the source text. The option ships
"",export_price_at()fallsthrough to the scalar for every index value, and 17f compares seven option dicts
x five index values against the pre-TARIFF-16 expression with
==.Also corrected here
config.yaml's comment besideexport_price_eur_kwhstill read "no shippedsupplier publishes a dynamic injection curve". TARIFF-07 struck that claim from
app/price.pyandDOCS.mdand did not reachconfig.yaml, where it has beenshipping since.
Checks
price_export_formulajoinsFRAMEWORK_KEYS;BANNED_IN_PLUGIN_CODEisderived from that set, so
code_leaks()refuses a plugin that names it withnothing else edited.
29/29 green.
20 mutations, every one red on a named check - ledger in
test_price.pybelow section 17, with byte deltas. Two are worth naming:
decision the add-on takes moves - the only surface that moves is the sentence,
which now tells the operator their VAT treatment exactly backwards. Red on the
ordered-pair check and on nothing else (MUTATION.md habit 14).
implausible_injection()given TARIFF-15's floor.Eneco's real published line stops starting. Full 29-file sweep: red on
test_price.py, green in the other 28. M10 and M12 were run the same way.Each row gets a fresh copy of the tree under a
mktemppath, mounted:ro- norevert to fail and no shared backup path, which is MUTATION.md habit 18 closed
structurally rather than by discipline. That design was exercised rather than
admired: two copies of the sweep ran concurrently at one point, and because each
holds its own tree neither could restore over the other's work.
⚠️ What no row could see, with its shape - and the deepest one is measured
rather than mentioned: with no printed estimate there is no check anywhere that
can tell a right pair of injection coefficients from a wrong one.
test_price.py17e's last check demonstrates it.
🤖 Generated with Claude Code
https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU