Four defects of one shape - the add-on's report of its own tariff plan was
wrong, always in the direction that flattered it - and one check, which is
the deliverable.
1. `TariffPlan.words` counted POINTS and called them HOURS. The live add-on
logged `dynamic_dayahead from generic_dayahead, 132 h` for 132
quarter-hourly points = 33 hours. Belgium moved to a 15-minute MTU on
2025-10-01, so this was wrong every time it was printed, and NOT by a
constant factor: a 40-hour soak measured 1.79x .. 5.33x, because §8.3's
carried points are hourly while the firm ones are quarter-hourly.
`words` now reports both quantities, plus the firm/carried split and the
spacing, in FCST-04's *Price history* wording so the two rows can be
compared.
2. §8.3's firm-pair refusal and the flat-tariff refusal did the same, and
they reach an operator through `self.arb.explain()`. This is exactly the
point<->hour conflation TARIFF-14 removed from `at()`, left standing in
the prose. `price_forecast.py`'s §8.3 carry-forward log line carried the
third instance - a carried-HOUR count over a POINT count - and DOCS
quotes it verbatim, so it is corrected here too.
3. The plan reached no HTTP surface at all. `rows["Tariff"]` was
`static_status()`'s 30 s-tick sentence and never saw the provider plan -
it read 'no static profile' at all 802 soak samples on a site whose feed
was answering. It now carries the plan's shape. ⚠️ The two sources are
kept distinguishable: the plan half is stamped with its own age, measured
from `tariff_at`, because it can be up to TARIFF_MAX_AGE_S (5400 s) old.
FCST-04 pins that row to DOCS as a pair; DOCS moves in this commit.
4. `main.py`'s "if and only if" was false. `cycle()` returns at the input
hold before `self.arb = None`, so an armed site read
`off (arbitrage_enabled)` - naming an option that is on - and, after one
completed cycle, showed a decision up to 15 s stale as a current one. The
row now reads the OPTION and separates three facts; `cycle_at` and
`arb_at` make "this cycle reached no decision" an observation.
THE DELIVERABLE IS THE CHECK. All four survived 29 green suites because
nothing graded the strings a human is shown. `test_price.py` section 12
renders through the shipped `status_payload()`, parses each number out of
its OWN SLOT and compares it against a figure computed from the fixture's
construction parameters - not from the property the row was built out of
(habit 4), and not as a substring of a sentence the same file wrote (habit
14). The fixture set is four plan shapes and the check computes and prints
the field pairs no fixture separates. The *Arbitrage* row's states are
driven through real cycles with a sensor answering None and graded by
INEQUALITY.
No behaviour changed: no threshold, refusal rung, claim or setpoint moved.
MUTATIONS: 16, no survivors, one at +0 B (the risky delta) and red. Two were
found by the battery INSIDE THIS CARD'S OWN CHECK: a -190 B survivor whose
fixture varied two things at once (habit 4), and a mutation whose red was a
TRACEBACK in an f-string operand and therefore named nothing (FCST-06). Both
closed; the table is on the PR.
All 29 test files green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU