PKG-14: cut 0.10.0, and say what an upgrade from 0.7.0 crosses
checks / unit (pull_request) Successful in 7m51s
checks / unit (pull_request) Successful in 7m51s
`config.yaml` 0.9.0 -> 0.10.0, and the top `## Unreleased` block is closed
into `## 0.10.0` with a preamble that names the backward-incompatible
changes and who they reach.
MINOR, and NOT because eighteen tickets landed. A stored options blob the
previous version started on is REFUSED on this one:
* TARIFF-07 retires three `price_provider` ids; a site set to one is
refused BY NAME at startup. Measured on the reference blob, with a
control on an accepted id so the refusals are about the ID.
* TARIFF-18 moves `price_taxes_eur_kwh` 0.06 -> 0.11736. A site that
STORED 0.06 is untouched, and - the non-obvious half - a taxes period
table beside that stored 0.06 does NOT become a startup refusal on
upgrade, because `SUPERSEDED_TAXES_DEFAULTS_EUR_KWH` reads it as
untouched. Measured, with the counterfactual that the rung is not
weakened (ORES's 0.1198 is still refused).
* SAFETY-10 adds `shadow_mode`, shipped `auto`. An existing CONTROLLING
install does not change by one watt and a FRESH one comes up observing.
Measured cross-revision, 8f82dd4 -> e6da478, same stimulus through the
shipped `Controller.cycle()`: byte-identical write streams, 13/13/25
writes. Positive control: the same driver at `shadow_mode: on` gets 13
writes to 5000 W on the older tree and one 0 W write on the newer.
And the one a reader of the newest section alone would miss: `ha addons
update` does not walk the versions, so a 0.7.0 install meets 0.8.0's and
0.9.0's rules at the same moment as 0.10.0's. Of those, SAFETY-39's rule 10
refuses 0.7.0's OWN shipped default `slew_w: 1000` against a 555.6 W
ceiling on a 5000 W rail. Measured red on this tree; the reference
install's `slew_w: 400` is measured green.
SCOPE, and it is drawn honestly rather than tidily. The top block's 18
entries all merged after the 0.9.0 bump `86bdf49`, so the block is closed
as-is and its `_this branch_` placeholders are resolved to merge shas.
⚠️ 0.10.0 SHIPS THREE MORE RUNTIME TICKETS THAT ARE NOT IN THAT TABLE -
SAFETY-43, TEST-21 and SAFETY-44 - and they are NAMED in the preamble
rather than moved, because re-filing this file's misplaced entries is
PKG-08. SAFETY-44 (`app/main.py`, `app/store.py`) is in the changelog
nowhere at all; that is said in both places. The second `## Unreleased`
block below `## 0.9.0` is ANNOTATED, not rewritten or emptied - UX-02 in it
shipped in 0.9.0, the other three in 0.10.0. No released section is
rewritten.
`test_version_gate.py` gains part 11: the bump and the closed section are
one act. PKG-09 refuses a deploy whose version did not move; nothing graded
the mirror failure, in which the version moves and the block is never
closed. Two sides from two files, a slicer with a synthetic positive
control run on every invocation, the retired ids read off `app/price.py` by
AST rather than transcribed, and the count of still-misfiled blocks frozen
so it can only shrink.
Mutations, all purged, tree bind-mounted :ro, backups at mktemp paths and
the revert fingerprinted:
M1 `## 0.10.0` -> `## Unreleased` +4 B RED 2
M2 version 0.10.0 -> 0.10.1 +0 B RED 1 (ZERO-BYTE)
M3 an id stripped from the NEWEST +10 B RED 1
section only, left below
M4 a 4th retired provider in price.py +62 B RED 1 here, 4 in
test_price.py
M5 `## 0.9.0` heading lost -3 B RED 1
M6 a 2nd `## Unreleased` left standing +27 B RED 1
M7 the slicer never closes -32 B RED 3
M8 SUPERSEDED_TAXES... emptied -5 B RED 2 on the evidence
instrument, plus
test_validate/test_price
⚠️ M5 WAS A SURVIVOR ON THE FIRST DRAFT. The slicer control was a sentinel
(`## 0.8.0`) that only a slicer running to EOF could reach, and deleting one
heading never gets there - ARCH-02's fifteenth instance, in a fresh costume.
11a now asserts the invariant instead: exactly one contents table per
section, which is what a lost heading breaks.
29/29 test files green. `tools/predeploy_check.py --deployed-version 0.7.0
--options <live blob>` CLEARED, exit 0, before and after.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU
This commit is contained in:
+198
-30
@@ -1,35 +1,185 @@
|
||||
# Changelog
|
||||
|
||||
## Unreleased
|
||||
## 0.10.0
|
||||
|
||||
⚠️ **THIS HAS NO VERSION AND CANNOT REACH AN INSTALL UNTIL IT GETS ONE.**
|
||||
PKG-06's and PKG-07's rule: the Supervisor keys the built image by `version:`,
|
||||
so `ha apps update` is a silent no-op on an unbumped change. This branch
|
||||
deliberately does **not** pick the number, because the right one depends on a
|
||||
fact the branch cannot observe - whether 0.9.0's image has already been built
|
||||
and pulled. If it has not, this belongs inside 0.9.0's section before it ships;
|
||||
if it has, it needs 0.10.0. Whoever deploys decides.
|
||||
⚠️ **A CONFIG THAT IS LEGAL ON 0.9.0 IS REFUSED ON 0.10.0, AND THAT - NOT THE
|
||||
NUMBER OF TICKETS - IS WHY THIS IS A MINOR BUMP RATHER THAN A PATCH.** Same
|
||||
shape as 0.8.0's `slew_w` rule and the same test: is there a stored options
|
||||
blob the previous version started on and this one will not? There is. A refusal
|
||||
is `SystemExit(78)` (`EX_CONFIG`) - the add-on does not start, ⚠️ **and the
|
||||
ingress panel goes with it**, because the add-on serving it is not running, so
|
||||
recovery means the Supervisor UI.
|
||||
|
||||
Measured on the reference install's own exported blob (70 keys), merged over
|
||||
this version's 75 defaults the way the Supervisor merges it, inside the CI
|
||||
image. The three below are what a reader who checks only one thing should
|
||||
check.
|
||||
|
||||
* ⚠️ **[[TARIFF-07]] - THREE `price_provider` IDS ARE RETIRED, AND A SITE SET
|
||||
TO ONE OF THEM IS REFUSED BY NAME AT STARTUP.** `engie_dynamic`,
|
||||
`totalenergies_dynamic` and `eneco_eddy` are gone from `PROVIDERS`. Each was
|
||||
accepted, silently, at every startup on 0.9.0 and earlier; on 0.10.0 each is
|
||||
a refusal that names the id, the endpoint, what that endpoint actually
|
||||
answers - a 404 from a gateway with no route, a TLS handshake against a
|
||||
hostname the certificate does not cover, a 404 with an empty body - which
|
||||
tariff the site has really been running since the id was entered, and what to
|
||||
set instead. **The refusal IS the migration.** Every fetch any of the three
|
||||
ever made ended in `SourceUnavailable`, and §11.4 turns that into a SILENT
|
||||
fall back to the static profile, so there is no working behaviour to preserve
|
||||
and nothing on the status page ever differed from a provider that simply
|
||||
never found a cheap hour. ⚠️ **All three ids stay in `config.yaml`'s
|
||||
`price_provider` enum on purpose**: drop one and the Supervisor rejects the
|
||||
options file *before the add-on runs*, the operator gets an opaque `expected
|
||||
one of ...`, and `validate.py`'s message is never seen - on exactly the sites
|
||||
that need it. `PROVIDERS` is three; the enum is seven.
|
||||
⚠️ **The reference install is on `price_provider: off` and is unaffected.**
|
||||
|
||||
* **[[TARIFF-18]] - `price_taxes_eur_kwh` moves from `0.06` to `0.11736`, and
|
||||
the non-obvious half is the refusal that deliberately does NOT happen.** A
|
||||
site that *stored* `0.06` is unaffected in both directions. It keeps `0.06`,
|
||||
because `App.options` is `deepmerge(config.yaml defaults, stored options)`
|
||||
and a moved default reaches only keys the blob does not carry - so the new
|
||||
figure arrives at a **fresh** install, and at an existing one only when its
|
||||
options are re-saved. ⚠️ **AND IT DOES NOT BECOME A STARTUP REFUSAL EITHER,
|
||||
WHICH TOOK A MECHANISM.** [[TARIFF-17]] made a `price_taxes_profile` period
|
||||
table beside a **non-default** `price_taxes_eur_kwh` a refusal, comparing
|
||||
against the shipped default precisely because the Supervisor writes the key
|
||||
whether the operator typed it or not. Moving the default turns every stored
|
||||
`0.06` into "non-default" **with nobody editing anything**, so a working
|
||||
Walloon or Brussels site with a period table would have stopped starting on
|
||||
this upgrade, over an inert number the table overrides anyway.
|
||||
`price.py` carries `SUPERSEDED_TAXES_DEFAULTS_EUR_KWH = (0.06,)` and
|
||||
`validate.py` reads a superseded default as *untouched*. **The rung is not
|
||||
weakened**: the same table beside a figure that is neither the shipped nor a
|
||||
superseded default - ORES's `0.1198` - is still refused, by name, and the
|
||||
refusal now says which two figures it will accept.
|
||||
|
||||
* **[[SAFETY-10]] - a new `shadow_mode` option, shipped `auto`, and BOTH halves
|
||||
of what that does matter.** ⚠️ **AN EXISTING INSTALL THAT IS CONTROLLING DOES
|
||||
NOT CHANGE BY ONE WATT.** `auto` is not a constant the Supervisor merges; it
|
||||
is a value the add-on **resolves**, and it resolves to `not control_started`
|
||||
- `shadow.engaged()` reads `Controller.auto`, the persisted start/stop state.
|
||||
An install with `auto_start: true`, or whose operator has pressed *Start /
|
||||
stop control*, is started, so `auto` resolves to live. An install that is
|
||||
**stopped** keeps its actuator at 0 W exactly as before, from the same writer
|
||||
on the same heartbeat; what changes is what the status page says and that the
|
||||
decision path now runs and is recorded. ⚠️ **A FRESH INSTALL COMES UP
|
||||
OBSERVING**: it ships `auto_start: false`, has no stored `auto` key, and
|
||||
therefore runs its whole decision path in shadow before it is ever allowed to
|
||||
command anything. That is the "default-on" the card asked for, delivered
|
||||
where it is safe and withheld where it is not - there is no mechanism in the
|
||||
add-on options system by which one key can default differently for a new
|
||||
install and an existing one, so a literal `shadow_mode: true` default would
|
||||
have stopped the battery at the reference site on the next update.
|
||||
**Measured cross-revision** - the same stimulus through the shipped
|
||||
`Controller.cycle()` on `8f82dd4` (the merge before SAFETY-10) and on
|
||||
`e6da478` (SAFETY-10 itself), start button ON: the write streams are
|
||||
**byte-identical**, 13 / 13 / 25 writes on a 6000 W import, a 4000 W export
|
||||
and a 300 W trickle, same values in the same order. An install that is
|
||||
**stopped** is byte-identical across the same pair too, at one 0 W write.
|
||||
⚠️ The positive control is what makes that readable: the same driver with
|
||||
`shadow_mode: on` and the button ON gets 13 writes up to 5000 W on the older
|
||||
tree and **one 0 W write** on the newer, so the driver does reach shadow mode
|
||||
and an identical pair is a result rather than a silence. The shadow case is
|
||||
also graded in `test_clamp.py` part 8 on what reached `set_number()`, against
|
||||
a precondition run that puts 30 non-zero writes on the wire. ⚠️ An
|
||||
unrecognised `shadow_mode` fails toward inaction
|
||||
**and** is refused by name (rule 11), so the reinterpretation is never
|
||||
silent; no blob written before 0.10.0 can carry one.
|
||||
|
||||
⚠️ **AND IF YOU ARE UPGRADING FROM 0.7.0 - WHICH THE REFERENCE INSTALL IS -
|
||||
THE REFUSAL THAT CAN STOP YOU IS IN 0.8.0'S SECTION, NOT IN THIS ONE.** `ha
|
||||
addons update` does not walk the versions; it installs 0.10.0 in one step, so
|
||||
**every validator rule added in 0.8.0 and 0.9.0 lands at the same moment as
|
||||
0.10.0's own**, and a config that has not been looked at since 0.7.0 meets all
|
||||
of them at once. Of those, exactly one can refuse a config nobody has edited:
|
||||
|
||||
> [[SAFETY-39]]'s **rule 10**, added in 0.8.0, refuses an explicit `slew_w`
|
||||
> above `SLEW_RAIL_FRAC x` the site rail, where `SLEW_RAIL_FRAC` is
|
||||
> `LOOP_TICK_S / PLANT_SETTLE_S` = 2/18. On a 5000 W rail that ceiling is
|
||||
> **555.6 W** - and ⚠️ **0.7.0's own shipped default was `slew_w: 1000`**, so
|
||||
> an install that never touched the key and simply took the default does not
|
||||
> start on 0.10.0 until the value is lowered.
|
||||
|
||||
Measured on this tree: the reference blob with `slew_w: 1000` is refused, by
|
||||
name, and the same blob at its actual `slew_w: 400` is accepted. 0.9.0's own
|
||||
additions cannot reach a pre-0.9.0 config at all - [[OPT-09]]'s two refusals
|
||||
fire only on a roster entry that USES `capacity_kwh`, a key that did not exist
|
||||
- so 0.8.0's is the whole of the crossing. **Read the live options and check
|
||||
`slew_w` against the ceiling and `price_provider` against the withdrawn list
|
||||
BEFORE `ha addons update`**, with `tools/predeploy_check.py`, which does both.
|
||||
|
||||
✅ **RUN AGAINST THIS RELEASE: CLEARED, exit 0.**
|
||||
|
||||
docker run --rm -v <repo>:/w:ro -v <dir>:/opt:ro goodwe-addon-checks:1 \
|
||||
python3 /w/goodwe_controller/tools/predeploy_check.py \
|
||||
--deployed-version 0.7.0 --options /opt/live-options.json
|
||||
|
||||
deployed on the box : 0.7.0
|
||||
shipped in this tree: 0.10.0
|
||||
live options pass this version's validator
|
||||
CLEARED for deploy.
|
||||
|
||||
**The option surface: 71 keys -> 75, `options:` and `schema:` in step.** Four
|
||||
new keys, and none of them can refuse a blob written before it existed:
|
||||
`price_formula`, `price_export_formula` and `price_taxes_profile` are `str?` -
|
||||
**note the trailing `?`, they are OPTIONAL** - defaulting to `""`;
|
||||
`shadow_mode` is `list(auto|on|off)` with no `?`, which is the shape that
|
||||
becomes exit 78, and it is safe for the same reason 0.9.0's `delivery_hold_s`
|
||||
was: it carries an `options:` default, so the Supervisor's merge supplies it to
|
||||
every stored blob that lacks it. Nothing was removed. One default moved
|
||||
(`price_taxes_eur_kwh`, above) and one enum widened (`price_provider` gains
|
||||
`custom_formula`).
|
||||
|
||||
⚠️ **WHY 0.10.0 AND NOT 0.9.0, SINCE 0.9.0'S IMAGE NEVER REACHED THE BOX.**
|
||||
The block this section closes used to say the choice depended on whether
|
||||
0.9.0 had been built and pulled, and that if it had not, this work belonged
|
||||
*inside* 0.9.0's section. That was written before 0.9.0's section was cut. It
|
||||
has been cut since `86bdf49`, on 2026-09-07, and **28 runtime commits** have
|
||||
landed behind it; folding them in would make the string `0.9.0` name two
|
||||
different trees, and the Supervisor keys the built image on exactly that
|
||||
string. Reusing it is the PKG-06/PKG-07 no-op in its worst form - a cached
|
||||
image the Supervisor is entitled to serve while reporting success. The version
|
||||
a tree has already shipped under is not available to be re-cut.
|
||||
|
||||
⚠️ **AND THIS SECTION IS NOT THE WHOLE OF WHAT 0.10.0 SHIPS. THREE TICKETS
|
||||
WITH RUNTIME CHANGES LANDED AFTER `86bdf49` AND ARE NOT IN THE TABLE BELOW.**
|
||||
Saying so is cheaper than moving them, and moving them is [[PKG-08]]'s card,
|
||||
not this one:
|
||||
|
||||
| ticket | runtime files | where it is written down |
|
||||
|---|---|---|
|
||||
| [[SAFETY-43]] | `app/delivery.py`, `app/packshare.py`, `config.yaml` | the `## Unreleased` block **below `## 0.9.0`**, which is misfiled |
|
||||
| [[TEST-21]] | `app/delivery.py`, `config.yaml`, `translations/` | the same block |
|
||||
| [[SAFETY-44]] | `app/main.py`, `app/store.py` | ⚠️ **nowhere in this file** |
|
||||
|
||||
Five more landed that touch no runtime file and so cannot change what the
|
||||
image does - PKG-13, UX-04, SAFETY-31, ARCH-02, INSTR-01. The count that
|
||||
matters for the no-op check is `tools/predeploy_check.py`'s own: **28 runtime
|
||||
commits since `86bdf49`**, of which the table below accounts for 21.
|
||||
|
||||
Contents of this release, in merge order since 0.9.0 (`86bdf49`):
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| _this branch_ | FCST-04 - the carry-forward fill reaches the plan, and the day it carries survives a restart |
|
||||
| _this branch_ | FCST-05 - one byte stops stopping the add-on, and the write path gets the read path's standard |
|
||||
| _this branch_ | FCST-06 - "cannot persist" stops meaning "has no /data" and starts meaning what it says |
|
||||
| _this branch_ | TARIFF-08 - the day-ahead feed is read in the shape it actually publishes, and the fixtures stop agreeing with the parser |
|
||||
| _this branch_ | TARIFF-09 - the day-ahead feed is asked for the horizon it was given, so tomorrow arrives firm instead of carried |
|
||||
| _this branch_ | TARIFF-10 - configuring a price provider is what fetches prices; trading stays its own switch |
|
||||
| _this branch_ | TARIFF-12 - a supplier outage stops deleting the plan on its first failed fetch, and TARIFF_MAX_AGE_S starts governing |
|
||||
| _this branch_ | TARIFF-14 - the price a trade is decided against is the price of the interval being traded |
|
||||
| _this branch_ | TARIFF-07 - three plugins that never fetched a price are withdrawn, and a site configured on one is told so **⚠️ BACKWARD-INCOMPATIBLE** |
|
||||
| _this branch_ | TARIFF-15 - `custom_formula`: your own tariff card's two coefficients, graded against the card's own printed price |
|
||||
| _this branch_ | TARIFF-13 - the capacity-tariff row's "quarter(s) banked" stops resetting to 0 at every restart, and a tied peak's quarter stops moving |
|
||||
| _this branch_ | TARIFF-11 - the add-on's account of the tariff plan stops flattering it, and the status page is graded as a surface |
|
||||
| _this branch_ | TARIFF-17 - a price component that varies by hour has somewhere to live, so a Walloon day/night network tariff can change which hour is cheapest |
|
||||
| _this branch_ | TARIFF-18 - `price_taxes_eur_kwh` stops shipping at half the real Flemish figure **⚠️ CHANGED DEFAULT** |
|
||||
| _this branch_ | TARIFF-16 - the injection leg stops being one number: an injection formula with its own coefficients, its own VAT and no floor under zero |
|
||||
| _this branch_ | SAFETY-45 - a price-driven charge is SIZED by the capacity-tariff guard instead of being switched on and off by it **⚠️ CONTROL STABILITY** |
|
||||
| _this branch_ | SAFETY-10 - shadow mode: the whole decision path runs and nothing reaches the inverter **⚠️ NEW OPTION, DEFAULT-ON FOR A NEW INSTALL** |
|
||||
| _this branch_ | SAFETY-47 - what a shadow run can show you, and the watts on its status row **⚠️ CORRECTS THREE CLAIMS SAFETY-10 SHIPPED** |
|
||||
| `c2787da` | FCST-04 - the carry-forward fill reaches the plan, and the day it carries survives a restart |
|
||||
| `5059e28` | FCST-05 - one byte stops stopping the add-on, and the write path gets the read path's standard |
|
||||
| `958f5cd` | FCST-06 - "cannot persist" stops meaning "has no /data" and starts meaning what it says |
|
||||
| `4257e38` | TARIFF-08 - the day-ahead feed is read in the shape it actually publishes, and the fixtures stop agreeing with the parser |
|
||||
| `88c59a9` | TARIFF-09 - the day-ahead feed is asked for the horizon it was given, so tomorrow arrives firm instead of carried |
|
||||
| `559f0eb` | TARIFF-10 - configuring a price provider is what fetches prices; trading stays its own switch |
|
||||
| `a6e64fb` | TARIFF-12 - a supplier outage stops deleting the plan on its first failed fetch, and TARIFF_MAX_AGE_S starts governing |
|
||||
| `8d09b30` | TARIFF-14 - the price a trade is decided against is the price of the interval being traded |
|
||||
| `aade2c7` | TARIFF-07 - three plugins that never fetched a price are withdrawn, and a site configured on one is told so **⚠️ BACKWARD-INCOMPATIBLE** |
|
||||
| `068b83f` | TARIFF-15 - `custom_formula`: your own tariff card's two coefficients, graded against the card's own printed price |
|
||||
| `b4a3801` | TARIFF-13 - the capacity-tariff row's "quarter(s) banked" stops resetting to 0 at every restart, and a tied peak's quarter stops moving |
|
||||
| `c518007` | TARIFF-11 - the add-on's account of the tariff plan stops flattering it, and the status page is graded as a surface |
|
||||
| `7796f16` | TARIFF-17 - a price component that varies by hour has somewhere to live, so a Walloon day/night network tariff can change which hour is cheapest |
|
||||
| `4dcd9b0` | TARIFF-18 - `price_taxes_eur_kwh` stops shipping at half the real Flemish figure **⚠️ CHANGED DEFAULT** |
|
||||
| `478ba93` | TARIFF-16 - the injection leg stops being one number: an injection formula with its own coefficients, its own VAT and no floor under zero |
|
||||
| `8f82dd4` | SAFETY-45 - a price-driven charge is SIZED by the capacity-tariff guard instead of being switched on and off by it **⚠️ CONTROL STABILITY** |
|
||||
| `e6da478` | SAFETY-10 - shadow mode: the whole decision path runs and nothing reaches the inverter **⚠️ NEW OPTION, DEFAULT-ON FOR A NEW INSTALL** |
|
||||
| `587495e` | SAFETY-47 - what a shadow run can show you, and the watts on its status row **⚠️ CORRECTS THREE CLAIMS SAFETY-10 SHIPPED** |
|
||||
|
||||
### SAFETY-47 — what a shadow run can show you, and the watts on its status row
|
||||
|
||||
@@ -1635,12 +1785,30 @@ and pulled. If it has not, this belongs inside 0.8.0's section before it ships;
|
||||
if it has, it needs 0.8.1. Whoever deploys decides, and PKG-07 is the record of
|
||||
what happens when nobody does.
|
||||
|
||||
⚠️ **ANNOTATED BY PKG-14, 2026-09-16 - EVERY ROW BELOW HAS SHIPPED, AND THE
|
||||
PARAGRAPH ABOVE IS NO LONGER TRUE OF ANY OF THEM.** It is annotated rather
|
||||
than rewritten, and nothing is moved out of it, because re-filing this file's
|
||||
misplaced entries is [[PKG-08]]'s card and guessing a boundary is what that
|
||||
card exists to stop. What is measurable, and what PKG-14 measured:
|
||||
|
||||
* **UX-02 shipped in 0.9.0.** Its merge `3d29993` is an ancestor of the 0.9.0
|
||||
bump `86bdf49` and it is already listed in `## 0.9.0`'s own contents table,
|
||||
so this row is a duplicate of a released one.
|
||||
* **UX-04, TEST-21 and SAFETY-43 shipped in 0.10.0.** All three merged after
|
||||
`86bdf49`. TEST-21 (`app/delivery.py`, `config.yaml`, `translations/`) and
|
||||
SAFETY-43 (`app/delivery.py`, `app/packshare.py`, `config.yaml`) are runtime
|
||||
changes and are named in `## 0.10.0`'s preamble as shipping there and being
|
||||
written down here; UX-04 touches no runtime file.
|
||||
* ⚠️ **[[SAFETY-44]] also shipped in 0.10.0 and is nowhere in this file** -
|
||||
`app/main.py` and `app/store.py`, three commits. It is named in
|
||||
`## 0.10.0`'s preamble for the same reason.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| _this branch_ | UX-02 - a configuration page in the ingress panel, where the validator's refusals can be seen |
|
||||
| _this branch_ | UX-04 - UX-01's three unmet acceptance criteria: one withdrawn, one built, one re-attributed |
|
||||
| _this branch_ | TEST-21 - `delivery_hold_s`'s floor re-derived from a harness that is now committed, and the recorded 30.0 s did not reproduce |
|
||||
| _this branch_ | SAFETY-43 - delinquency measured against the ask as well as against a flat 500 W, closing OPT-09's disclosed detection regression |
|
||||
| `3d29993` | UX-02 - a configuration page in the ingress panel, where the validator's refusals can be seen **⚠️ SHIPPED IN 0.9.0, see that section** |
|
||||
| `bb46916` | UX-04 - UX-01's three unmet acceptance criteria: one withdrawn, one built, one re-attributed **⚠️ SHIPPED IN 0.10.0** |
|
||||
| `6719bd8` | TEST-21 - `delivery_hold_s`'s floor re-derived from a harness that is now committed, and the recorded 30.0 s did not reproduce **⚠️ SHIPPED IN 0.10.0** |
|
||||
| `37dd0f0` | SAFETY-43 - delinquency measured against the ask as well as against a flat 500 W, closing OPT-09's disclosed detection regression **⚠️ SHIPPED IN 0.10.0** |
|
||||
|
||||
⚠️ **SAFETY-43 CLOSES THE DETECTION REGRESSION OPT-09 DISCLOSED, AND IT IS A
|
||||
TWO-INVERTER FIX. N=1 IS BYTE-IDENTICAL.** `delivery.delinquent()` named a unit
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
name: GoodWe RS485 Controller
|
||||
version: "0.9.0"
|
||||
version: "0.10.0"
|
||||
slug: goodwe_controller
|
||||
description: >-
|
||||
Drives a GoodWe ES/BP battery inverter over RS485 by emulating its smart
|
||||
|
||||
@@ -272,6 +272,136 @@ check("REFUSED is exactly 1, not merely non-zero",
|
||||
print("\nthe shipped tree's own version is readable")
|
||||
check("shipped_version() finds a version", bool(shipped_version(HERE)))
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# PKG-14 - THE OTHER HALF OF THE NO-OP, AND NOTHING GRADED IT.
|
||||
#
|
||||
# PKG-09 refuses a deploy whose `version:` did not move. It cannot see the
|
||||
# mirror failure, which this repository has now produced twice: the version
|
||||
# DOES move and the changelog's `## Unreleased` block is never closed, so the
|
||||
# operator who opens the file to find out what they are about to install reads
|
||||
# "THIS HAS NO VERSION AND CANNOT REACH AN INSTALL UNTIL IT GETS ONE" about a
|
||||
# tree that is being installed. `## 0.8.0` records one such block that
|
||||
# accumulated across three releases; `## 0.9.0` was cut with a second one left
|
||||
# standing underneath it.
|
||||
#
|
||||
# ⚠️ THE TWO SIDES COME FROM DIFFERENT FILES ON PURPOSE (MUTATION.md habit 4).
|
||||
# config.yaml's `version:` and CHANGELOG.md's newest heading are written by
|
||||
# different hands at different times, and the whole defect is that they drift.
|
||||
# A check that read the number once and compared it with itself would be the
|
||||
# DEV-02 fixture again: a number against itself.
|
||||
#
|
||||
# ⚠️ AND IT IS NOT A SUBSTRING SCAN OVER THE WHOLE FILE. Every id this section
|
||||
# looks for occurs dozens of times lower down, so a scan of the file would pass
|
||||
# on any tree whatever the newest section said - CFG-04's defect, which asserted
|
||||
# against the TEXT of the thing under test. The section is SLICED first, and
|
||||
# part 11a grades the slicer against a sentinel that exists only below it.
|
||||
print("\n11. PKG-14 - the bump and the closed section are one act")
|
||||
|
||||
_CHANGELOG = os.path.join(HERE, "CHANGELOG.md")
|
||||
with open(_CHANGELOG, encoding="utf-8") as fh:
|
||||
_cl_lines = fh.read().splitlines()
|
||||
|
||||
_heads = [(i, l[3:].strip()) for i, l in enumerate(_cl_lines)
|
||||
if l.startswith("## ")]
|
||||
check(f"precondition: the file has {len(_heads)} `## ` headings, so the "
|
||||
f"quantifiers below are not over an empty set", len(_heads) >= 8)
|
||||
|
||||
# 11a. THE SLICE, AND TWO CONTROLS ON THE SLICER.
|
||||
# ARCH-02's fifteenth instance: a splitter that never closes attributes
|
||||
# everything to the last thing it opened, and every per-section check
|
||||
# downstream silently becomes a total over the whole file. Both controls below
|
||||
# exist because 11b/11c would go on passing for a tree in which nothing they
|
||||
# name is true.
|
||||
|
||||
|
||||
def slice_newest(lines):
|
||||
"""(heading text, the lines of the FIRST `## ` section) - closing at the
|
||||
next `## `, which is the half that has to be graded rather than assumed."""
|
||||
heads = [i for i, l in enumerate(lines) if l.startswith("## ")]
|
||||
if not heads:
|
||||
return None, []
|
||||
start = heads[0]
|
||||
end = heads[1] if len(heads) > 1 else len(lines)
|
||||
return lines[start][3:].strip(), lines[start:end]
|
||||
|
||||
|
||||
# CONTROL ONE, run on every invocation rather than attested in a commit
|
||||
# message: a synthetic document whose token appears ONLY below the first
|
||||
# section. A slicer that runs to the end of the file finds it; this one must
|
||||
# not. test_defaults_pin.py part 5 plants a literal for the same reason.
|
||||
_SYNTH = ["# Changelog", "", "## 9.9.9", "", "the newest section.", "",
|
||||
"## 9.9.8", "", "PLANTED-ONLY-BELOW", ""]
|
||||
_syn_name, _syn_body = slice_newest(_SYNTH)
|
||||
check("11a. the slicer CLOSES - on a synthetic document it excludes a token "
|
||||
"that exists only in the section BELOW the newest one",
|
||||
_syn_name == "9.9.9" and "PLANTED-ONLY-BELOW" in "\n".join(_SYNTH)
|
||||
and "PLANTED-ONLY-BELOW" not in "\n".join(_syn_body))
|
||||
|
||||
_top_name, _top_body = slice_newest(_cl_lines)
|
||||
_newest = "\n".join(_top_body)
|
||||
_SENTINEL = "## 0.8.0"
|
||||
check(f"...and on the shipped file too: it does not contain {_SENTINEL!r}, "
|
||||
f"which the file does", _SENTINEL in "\n".join(_cl_lines)
|
||||
and _SENTINEL not in _newest)
|
||||
check(f"...and it is not empty: {len(_top_body)} lines under {_top_name!r}",
|
||||
len(_top_body) > 20)
|
||||
|
||||
# CONTROL TWO - and this is the one that catches a LOST HEADING rather than a
|
||||
# loose slicer. Every version section in this file carries exactly one
|
||||
# "in merge order since" contents table. Delete a released section's `## `
|
||||
# heading and its table is swallowed by the section above, which is a released
|
||||
# record disappearing into an unreleased one - the shape `## 0.7.0` already
|
||||
# records for 0.6.0, and the one PKG-08 is left holding.
|
||||
_tables = [l for l in _top_body if "in merge order since" in l]
|
||||
check(f"...and it carries EXACTLY ONE contents table, not {len(_tables)} - a "
|
||||
f"second one means a released section below lost its `## ` heading and "
|
||||
f"was absorbed into this one", len(_tables) == 1)
|
||||
|
||||
# 11b. THE BLOCK IS CLOSED, AND IT CARRIES THE SHIPPED NUMBER.
|
||||
check("11b. the newest `## ` heading is a VERSION and not `Unreleased` - the "
|
||||
"block was closed by whoever bumped the version",
|
||||
_top_name.lower() != "unreleased")
|
||||
_shipped = shipped_version(HERE)
|
||||
check(f"...and it is exactly config.yaml's `version:` ({_shipped!r} in "
|
||||
f"config.yaml, {_top_name!r} in CHANGELOG.md) - two files, so a bump "
|
||||
f"without a section, or a section without a bump, is red",
|
||||
_top_name == _shipped)
|
||||
|
||||
# 11c. A WITHDRAWN price_provider IS NAMED IN THE SECTION THAT WITHDRAWS IT.
|
||||
# ⚠️ READ OFF app/price.py BY AST, NOT TYPED HERE. A list transcribed into this
|
||||
# file would go on passing after a fourth id was retired and documented
|
||||
# nowhere, which is the exact failure TARIFF-07 exists to prevent: an id the
|
||||
# add-on refuses at startup and no release note mentions.
|
||||
import ast # noqa: E402
|
||||
_price_src = open(os.path.join(HERE, "app", "price.py"),
|
||||
encoding="utf-8").read()
|
||||
_retired = []
|
||||
for _node in ast.walk(ast.parse(_price_src)):
|
||||
if isinstance(_node, ast.AnnAssign) and getattr(
|
||||
_node.target, "id", None) == "RETIRED_PROVIDERS":
|
||||
_retired = [k.value for k in _node.value.keys]
|
||||
elif isinstance(_node, ast.Assign) and any(
|
||||
getattr(t, "id", None) == "RETIRED_PROVIDERS" for t in _node.targets):
|
||||
_retired = [k.value for k in _node.value.keys]
|
||||
check(f"11c. precondition: RETIRED_PROVIDERS was read off app/price.py and is "
|
||||
f"not empty - {sorted(_retired)}", len(_retired) > 0)
|
||||
_unnamed = sorted(p for p in _retired if p not in _newest)
|
||||
check(f"...and every one of the {len(_retired)} withdrawn id(s) is named in "
|
||||
f"`## {_top_name}` - an id the validator refuses at startup that no "
|
||||
f"release note mentions is the TARIFF-07 defect with the sign flipped",
|
||||
_unnamed == [])
|
||||
|
||||
# 11d. THE LEDGER OF BLOCKS STILL MISFILED, FROZEN RATHER THAN FIXED.
|
||||
# PKG-08 owns splitting them; this file owns the promise that the number can
|
||||
# only go DOWN. A release that cuts its section and leaves a second
|
||||
# `## Unreleased` standing is red here on the commit that does it.
|
||||
_stale = [n for _, n in _heads[1:] if n.lower() == "unreleased"]
|
||||
check(f"11d. exactly {len(_stale)} `## Unreleased` block(s) remain BELOW the "
|
||||
f"newest section, and the count is pinned EXACTLY so the ledger can "
|
||||
f"only shrink - splitting them is PKG-08, not this file",
|
||||
len(_stale) == 1)
|
||||
|
||||
for d in tmpdirs:
|
||||
shutil.rmtree(d, ignore_errors=True)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user