Merge release/1.0 (FCST-03) into SAFETY-24 - CHANGELOG only
checks / unit (pull_request) Successful in 3m28s
checks / unit (pull_request) Successful in 3m28s
FCST-03 landed while this was in review, so both branches had prepended an Unreleased stanza. Only CHANGELOG.md conflicted, exactly as predicted; app/, config.yaml, validate.py and DOCS.md all auto-merged, and this card touches neither price_forecast.py nor static_tariff.py. Both stanzas kept verbatim, SAFETY-24's above FCST-03's - no text from either author edited. Full suite green on the merged tree: 14 files. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015VXjA3LWccN1rJsk3okyPC
This commit is contained in:
@@ -46,6 +46,69 @@ startup, in CFG-01/MAINT-04's manner. `maintenance_soc_floor` is deliberately
|
||||
spend limit, and the rename is recorded as a decision to revisit if a third
|
||||
floor ever appears.
|
||||
|
||||
**Provisional prices, and what may not rest on one (FCST-03).**
|
||||
Day-ahead prices arrive once a day, around 14–16 h CET. Until they do, §8.3 says
|
||||
the unknown hours use yesterday's profile and are flagged `provisional: true` —
|
||||
and **no irreversible action may be taken on a provisional price.**
|
||||
`app/price_forecast.py` does the fill, and the whole module is shaped by one
|
||||
defect: a carried-forward point and a published one are the same three fields
|
||||
and a bool, so the natural line `PricePoint(..., provisional=y.provisional)`
|
||||
copies yesterday's `False` onto today's guess and produces stale data presented
|
||||
as current. That is the meter-side SAFETY-18 failure one layer up — the reading
|
||||
arrives on time, looks fresh, and is wrong.
|
||||
|
||||
**So the flag is never passed through.** Carried points are built by a
|
||||
constructor that has no `provisional` parameter at all (the literal `True` lives
|
||||
in one place, and there is nothing for a caller to forget or copy); published
|
||||
points are passed through **by identity**, so there is no copy that could lose a
|
||||
flag; and an audit re-derives the expected flag for every emitted point from the
|
||||
set of hours the feed actually published and *raises* rather than repairing one.
|
||||
A published point may itself be provisional — `eneco_eddy`'s current-quarter
|
||||
imbalance print is — so the audit's rule for a published hour is "whatever the
|
||||
feed said", never "False".
|
||||
|
||||
**The fill keys on the Brussels wall clock, not on `t − 24 h`.** A 23-hour day
|
||||
has no 02:00 and a 25-hour day has two, so a fixed 24-hour subtraction shifts
|
||||
every carried hour by one across a transition and misprices a whole day with
|
||||
nothing to show for it. Re-running the fill after a late publication replaces
|
||||
the carried hours with the real print: the number and the flag move together
|
||||
because the series is rebuilt, never patched.
|
||||
|
||||
**Arbitrage now drops provisional hours instead of refusing the whole day.**
|
||||
The refusal that read "the plan is provisional" would have switched arbitrage off
|
||||
every morning on every dynamic site once carry-forward filling landed — a
|
||||
part-firm series is the *normal* state before the print. What §8.3 forbids is
|
||||
acting **on** a provisional price, so the provisional hours are dropped and the
|
||||
spread, the cheapest hour and the dearest hour are computed over the published
|
||||
ones only; fewer than two firm hours is still a refusal, which is where the
|
||||
§11.4 fallback (wholly provisional) lands. The permitting variant now refuses to
|
||||
be *constructed* around a provisional point at all. The cheap-window flag became
|
||||
per-hour for the same reason, so a site's maintenance charge floor does not go
|
||||
dark every morning.
|
||||
|
||||
**Real-time / imbalance products get measurably wider thresholds.** One widening
|
||||
path, driven by a set of active uncertainty sources: the product's own (§9.2 row
|
||||
2, derived from the declared `PriceProduct` and never from a plugin id) and
|
||||
§11.4's "no weather data" row, which the spec widens by a different trigger and
|
||||
which is the same mechanism. The combination rule is `max`, not a product — two
|
||||
independent doubts must not compound into a threshold nothing can clear. The
|
||||
caller supplies the base round-trip cost threshold, so OPT-04 still owns what
|
||||
the number *is*; this owns only how much wider it gets and why, and the allowed
|
||||
verdict reports the threshold it cleared.
|
||||
|
||||
**An irreversible action is a value, not a judgement call at each call site.**
|
||||
`Irreversible` carries the hours an action commits — the maintenance drain's
|
||||
window is derived from `maintenance.DRAIN_TIMEOUT_MIN` rather than typed twice —
|
||||
and the gate looks at exactly those hours, blocking on a provisional one, on an
|
||||
hour the series does not cover, and on no price data at all: three different
|
||||
reasons, none of them an answer. ⚠️ It is the entry point for a start being
|
||||
*justified by a price*; the calendar path main.py runs today is untouched,
|
||||
because a gate that blocked on `no_data` would stop the monthly cycle on every
|
||||
default install — §12's opening sentence.
|
||||
|
||||
**Zero new options.** No `config.yaml` key, no new defaults dict, no `o.get(k,
|
||||
X)` in main.py: the module reads no options at all, so CFG-03 did not widen.
|
||||
|
||||
**The grid-following loop is now capped against the month's billed capacity peak
|
||||
(SAFETY-07).** New `app/peakguard.py`. TARIFF-01 measured the quarter; this is the
|
||||
consumer it was built for. Every cycle the guard projects the open quarter,
|
||||
|
||||
Reference in New Issue
Block a user