CHANGELOG.md was the entire collision - SAFETY-07 lands in peakguard.py, main.py
and the arbiter, none of which this branch touches. Both entries kept.
X1. `_firm_or_die` guarding only `cheapest` survived, because the check passed
the SAME provisional point as both cheapest and dearest: a guard on either role
alone raises, so the stimulus could not tell "the buy hour is guarded" from
"both are". Now one call per role with a firm point in the other seat, plus the
positive arm so the two refusals are discriminating.
X6. `firm_points()` returning the carried hours too survived, because its two
consumers - the §11.4 refusal and cheap_now - guard provenance independently and
both keep answering correctly. The accessor is now graded directly: exactly the
published points, by identity and in order.
Both are the degeneracy rule this file states at the top, in a new dress - a
stimulus that cannot distinguish the two things it grades.
⚠️ AND THE REASON THE M9 MUTATION "SURVIVED" IN MY EARLIER REPORT: a stale
__pycache__. Reverting `self.provisional` to `here.provisional` changes neither
the file size nor, at one-second resolution, the mtime, so CPython kept the
MUTATED bytecode and a green run afterwards proved nothing about that line. Every
mutation here now clears __pycache__ first; re-run that way, M9 is caught. The
committed source was always correct - the checks were not being run against it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015VXjA3LWccN1rJsk3okyPC
§8.3 fills the hours a day-ahead feed has not published yet with yesterday's
profile and flags them provisional, and forbids any irreversible action on such
a price. The whole risk of this ticket is one line - a carried point and a
published point are three fields and a bool, so `provisional=y.provisional`
copies yesterday's False onto today's guess and produces stale data presented as
current. That is SAFETY-18's frozen-but-fresh meter one layer up.
So the flag is never passed through. Carried points come from a constructor with
no provisional parameter; published points pass through BY IDENTITY, so no copy
exists that could lose one; an audit re-derives the expected flag for every
emitted point and raises rather than repairing; and the check walks the module's
own AST so the pass-through idiom cannot come back looking ordinary. A published
point may itself be provisional (eneco_eddy's imbalance print is), so the rule
for a published hour is "whatever the feed said", never False.
The fill keys on the Brussels wall clock rather than t-24h, so the 23- and
25-hour days are right; a late publication rebuilds the series, moving the
number and the flag together.
AC2 and AC3 landed on TARIFF-03's existing verdict surface rather than beside
it. Refusal 3 became "fewer than two FIRM hours" - refusing on "any point is
provisional" would have switched arbitrage off every morning on every dynamic
site - and refusal 5 is new: the spread must clear a threshold widened by the
widest active uncertainty source (max, never a product). The permitting variant
now refuses to be constructed around a provisional point at all, and the
cheap-window flag became per-hour for the same reason.
The irreversible action is a value carrying the hours it commits, derived from
maintenance.py's own DRAIN_TIMEOUT_MIN; the gate blocks a maintenance drain
whose window reaches into a carried hour, and the phase machine stays idle. It
is the price-justified entry point only - a gate that blocked on no_data would
stop the monthly cycle on every default install.
Zero new config options; TEST-14's signature scan ported to cover both Gate's
and Arbitrage's constructors, closing _Refused.reason's open hole.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015VXjA3LWccN1rJsk3okyPC