PKG-14: cut 0.10.0, and say what an upgrade from 0.7.0 crosses #91

Merged
admin merged 1 commits from PKG-14 into release/1.0 2026-09-16 02:58:28 +02:00
Owner

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. Do not merge and do not ship to master - that is a
separate, deliberate act.

Why MINOR, and it is not "a lot of tickets"

0.8.0's test, applied again: is there a stored options blob the previous
version started on and this one will not?
There is, three ways. A refusal is
SystemExit(78) - the add-on does not start, and the ingress panel goes with
it. Every figure below was measured on the reference install's own exported
blob (70 keys), merged over this version's 75 defaults, inside the CI image.

  • TARIFF-07 retires engie_dynamic, totalenergies_dynamic and
    eneco_eddy. A site set to one is refused by name at startup where it
    was accepted before. Measured for all three, with a control on an accepted
    id (generic_dayahead) so the refusals are about the id and not about
    the blob. The three stay in the config.yaml enum on purpose, or the
    Supervisor would reject the options file before validate.py's message
    could ever be read.
  • TARIFF-18 moves price_taxes_eur_kwh 0.06 -> 0.11736. A site that
    stored 0.06 keeps it - a moved default reaches only keys the blob does
    not carry. ⚠️ The non-obvious half is the refusal that deliberately does
    NOT happen
    : TARIFF-17 made a price_taxes_profile table beside a
    non-default scalar a refusal, and moving the default would have turned
    every stored 0.06 into "non-default" with nobody editing anything - so a
    working Walloon or Brussels site would have stopped starting.
    SUPERSEDED_TAXES_DEFAULTS_EUR_KWH = (0.06,) makes it read as untouched.
    The rung is not weakened: ORES's 0.1198 beside the same table is still
    refused, measured.
  • SAFETY-10 adds shadow_mode, shipped auto. ⚠️ An existing install
    that is CONTROLLING does not change by one watt
    - auto resolves to
    not control_started. A fresh install (auto_start: false) comes up
    observing. Measured cross-revision, 8f82dd4 -> e6da478, same stimulus
    through the shipped Controller.cycle() with the start button ON:
    byte-identical write streams, 13 / 13 / 25 writes on a 6000 W import, a
    4000 W export and a 300 W trickle. Positive control, because two identical
    streams also come out of a driver that never reaches shadow mode: the same
    driver at shadow_mode: on gets 13 writes up to 5000 W on the older tree
    and one 0 W write on the newer.

The 0.7.0 -> 0.10.0 path

ha addons update does not walk the versions. It installs 0.10.0 in one step,
so every rule added in 0.8.0 and 0.9.0 lands at the same moment as 0.10.0's
own
, and a config nobody has edited since 0.7.0 meets all of them at once.
Of those, exactly one can refuse such a config: SAFETY-39's rule 10,
0.8.0's, which refuses an explicit slew_w above SLEW_RAIL_FRAC x the rail -
555.6 W on a 5000 W rail - and 0.7.0's own shipped default was
slew_w: 1000
. Measured refused on this tree, by name; the reference
install's actual slew_w: 400 is measured accepted. 0.9.0's own additions
(OPT-09's two) cannot reach a pre-0.9.0 config, so 0.8.0's is the whole of the
crossing. Someone reading only the newest section would otherwise miss the one
rule that can stop the add-on starting.

Where the changelog boundary was drawn

The top block's 18 entries all merged after the 0.9.0 bump 86bdf49, so
the block is genuinely this run's work and is closed as-is; its _this branch_
placeholders are resolved to merge shas.

⚠️ But 0.10.0 ships more than that table lists. Three tickets with runtime
changes landed after 86bdf49 and are not in it:

ticket runtime files written down
SAFETY-43 app/delivery.py, app/packshare.py, config.yaml the ## Unreleased block below ## 0.9.0
TEST-21 app/delivery.py, config.yaml, translations/ the same block
SAFETY-44 app/main.py, app/store.py ⚠️ nowhere in the file

They are named in the preamble, not moved - re-filing this file's
misplaced entries is PKG-08, and an honest boundary beats a tidy one. The
second ## Unreleased block is annotated rather than rewritten or
emptied: UX-02 in it shipped in 0.9.0 (its merge is an ancestor of 86bdf49
and it is already in 0.9.0's contents table), the other three in 0.10.0.
No released section is rewritten.

The check, and the mutations

test_version_gate.py part 11. PKG-09 refuses a deploy whose version: did
not move; nothing graded the mirror failure - the version moves and the
## Unreleased block is never closed, so whoever opens the file before
deploying reads "THIS HAS NO VERSION AND CANNOT REACH AN INSTALL" about the
tree being installed. Two sides from two files; the retired ids read off
app/price.py by AST rather than transcribed; a slicer with a synthetic
positive control run on every invocation
; the count of still-misfiled blocks
frozen so it can only shrink.

All purged, tree bind-mounted :ro, backups at mktemp paths, revert
fingerprinted after every run. No survivors.

mutation delta result
M1 ## 0.10.0 -> ## Unreleased +4 B RED 2
M2 version: 0.10.0 -> 0.10.1 +0 B RED 1
M3 an id stripped from the NEWEST section only, left below +10 B RED 1
M4 a 4th retired provider in price.py, documented nowhere +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 (the instrument itself) -32 B RED 3
M8 SUPERSEDED_TAXES_DEFAULTS_EUR_KWH emptied -5 B RED 2 on the evidence instrument, plus test_validate.py and test_price.py

⚠️ M5 was a SURVIVOR on the first draft, and it is MUTATION.md's fifteenth
instance in a fresh costume.
11a's slicer control was a sentinel (## 0.8.0)
that only a slicer running to EOF could reach - deleting one heading never gets
there, so the control was decoration for the realistic failure. 11a now asserts
the invariant instead: exactly one in merge order since contents table
per section, which is precisely what a lost heading breaks. M2 is a
zero-byte edit and is flagged as such per MUTATION.md.

⚠️ The evidence harness caught two of its own fixture defects before they
reached this page
(habit 4): the generic_dayahead control failed for "two
price sources on one site" because the live blob carries
price_now_entity/price_avg_entity, and the first price_taxes_profile
fixture was in the wrong syntax and was being refused as malformed JSON rather
than for the reason the check named.

Results

29 / 29 test files green

tools/predeploy_check.py --deployed-version 0.7.0 --options <live blob>

BEFORE                             AFTER
  deployed on the box : 0.7.0        deployed on the box : 0.7.0
  shipped in this tree: 0.9.0        shipped in this tree: 0.10.0
  live options pass this             live options pass this
    version's validator                version's validator
  CLEARED for deploy.  exit 0       CLEARED for deploy.  exit 0

Nothing was deployed, /addons/ was not written, and neither 192.168.2.158 nor
the inverter was contacted.

Numbers in the card that did not reproduce

  • the ## Unreleased block is 1541 lines citing 31 ticket ids, not 5487
    and ~70. Those figures fit the whole file (5572) and the
    ## Detail notes block (3082 lines, 61 ids) - which is PKG-08's subject,
    not this one's;
  • 28 runtime commits since 86bdf49, by predeploy_check.py's own count,
    not 30;
  • 18 tickets in the block, not fifteen - 21 with runtime changes once
    SAFETY-43, TEST-21 and SAFETY-44 are counted.

🤖 Generated with Claude Code

https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU

`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. **Do not merge and do not ship to master** - that is a separate, deliberate act. ## Why MINOR, and it is not "a lot of tickets" 0.8.0's test, applied again: **is there a stored options blob the previous version started on and this one will not?** There is, three ways. A refusal is `SystemExit(78)` - the add-on does not start, and the ingress panel goes with it. Every figure below was measured on the reference install's own exported blob (70 keys), merged over this version's 75 defaults, inside the CI image. * **TARIFF-07** retires `engie_dynamic`, `totalenergies_dynamic` and `eneco_eddy`. A site set to one is **refused by name at startup** where it was accepted before. Measured for all three, with a control on an accepted id (`generic_dayahead`) so the refusals are about the **id** and not about the blob. The three stay in the `config.yaml` enum on purpose, or the Supervisor would reject the options file before `validate.py`'s message could ever be read. * **TARIFF-18** moves `price_taxes_eur_kwh` `0.06 -> 0.11736`. A site that **stored** `0.06` keeps it - a moved default reaches only keys the blob does not carry. ⚠️ **The non-obvious half is the refusal that deliberately does NOT happen**: TARIFF-17 made a `price_taxes_profile` table beside a *non-default* scalar a refusal, and moving the default would have turned every stored `0.06` into "non-default" with nobody editing anything - so a working Walloon or Brussels site would have stopped starting. `SUPERSEDED_TAXES_DEFAULTS_EUR_KWH = (0.06,)` makes it read as untouched. **The rung is not weakened**: ORES's `0.1198` beside the same table is still refused, measured. * **SAFETY-10** adds `shadow_mode`, shipped `auto`. ⚠️ **An existing install that is CONTROLLING does not change by one watt** - `auto` resolves to `not control_started`. A **fresh** install (`auto_start: false`) comes up observing. Measured cross-revision, `8f82dd4` -> `e6da478`, same stimulus through the shipped `Controller.cycle()` with the start button ON: **byte-identical write streams**, 13 / 13 / 25 writes on a 6000 W import, a 4000 W export and a 300 W trickle. Positive control, because two identical streams also come out of a driver that never reaches shadow mode: the same driver at `shadow_mode: on` gets 13 writes up to 5000 W on the older tree and **one 0 W write** on the newer. ## The 0.7.0 -> 0.10.0 path `ha addons update` does not walk the versions. It installs 0.10.0 in one step, so **every rule added in 0.8.0 and 0.9.0 lands at the same moment as 0.10.0's own**, and a config nobody has edited since 0.7.0 meets all of them at once. Of those, exactly one can refuse such a config: SAFETY-39's **rule 10**, 0.8.0's, which refuses an explicit `slew_w` above `SLEW_RAIL_FRAC x` the rail - **555.6 W** on a 5000 W rail - and **0.7.0's own shipped default was `slew_w: 1000`**. Measured refused on this tree, by name; the reference install's actual `slew_w: 400` is measured accepted. 0.9.0's own additions (OPT-09's two) cannot reach a pre-0.9.0 config, so 0.8.0's is the whole of the crossing. Someone reading only the newest section would otherwise miss the one rule that can stop the add-on starting. ## Where the changelog boundary was drawn The top block's **18** entries all merged after the 0.9.0 bump `86bdf49`, so the block is genuinely this run's work and is closed as-is; its `_this branch_` placeholders are resolved to merge shas. ⚠️ **But 0.10.0 ships more than that table lists.** Three tickets with runtime changes landed after `86bdf49` and are not in it: | ticket | runtime files | written down | |---|---|---| | SAFETY-43 | `app/delivery.py`, `app/packshare.py`, `config.yaml` | the `## Unreleased` block **below `## 0.9.0`** | | TEST-21 | `app/delivery.py`, `config.yaml`, `translations/` | the same block | | SAFETY-44 | `app/main.py`, `app/store.py` | ⚠️ **nowhere in the file** | They are **named in the preamble, not moved** - re-filing this file's misplaced entries is **PKG-08**, and an honest boundary beats a tidy one. The second `## Unreleased` block is **annotated** rather than rewritten or emptied: UX-02 in it shipped in 0.9.0 (its merge is an ancestor of `86bdf49` and it is already in 0.9.0's contents table), the other three in 0.10.0. **No released section is rewritten.** ## The check, and the mutations `test_version_gate.py` part 11. PKG-09 refuses a deploy whose `version:` did not move; **nothing graded the mirror failure** - the version moves and the `## Unreleased` block is never closed, so whoever opens the file before deploying reads *"THIS HAS NO VERSION AND CANNOT REACH AN INSTALL"* about the tree being installed. Two sides from two files; the retired ids read off `app/price.py` by **AST** rather than transcribed; a slicer with a **synthetic positive control run on every invocation**; the count of still-misfiled blocks frozen so it can only shrink. All purged, tree bind-mounted `:ro`, backups at `mktemp` paths, revert fingerprinted after every run. **No survivors.** | | mutation | delta | result | |---|---|---|---| | M1 | `## 0.10.0` -> `## Unreleased` | +4 B | RED 2 | | M2 | `version:` 0.10.0 -> 0.10.1 | **+0 B** | RED 1 | | M3 | an id stripped from the NEWEST section only, left below | +10 B | RED 1 | | M4 | a 4th retired provider in `price.py`, documented nowhere | +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 (the instrument itself) | -32 B | RED 3 | | M8 | `SUPERSEDED_TAXES_DEFAULTS_EUR_KWH` emptied | -5 B | RED 2 on the evidence instrument, plus `test_validate.py` and `test_price.py` | ⚠️ **M5 was a SURVIVOR on the first draft, and it is MUTATION.md's fifteenth instance in a fresh costume.** 11a's slicer control was a sentinel (`## 0.8.0`) that only a slicer running to EOF could reach - deleting one heading never gets there, so the control was decoration for the realistic failure. 11a now asserts the **invariant** instead: exactly one `in merge order since` contents table per section, which is precisely what a lost heading breaks. M2 is a **zero-byte** edit and is flagged as such per `MUTATION.md`. ⚠️ **The evidence harness caught two of its own fixture defects before they reached this page** (habit 4): the `generic_dayahead` control failed for "two price sources on one site" because the live blob carries `price_now_entity`/`price_avg_entity`, and the first `price_taxes_profile` fixture was in the wrong syntax and was being refused as malformed JSON rather than for the reason the check named. ## Results 29 / 29 test files green tools/predeploy_check.py --deployed-version 0.7.0 --options <live blob> BEFORE AFTER deployed on the box : 0.7.0 deployed on the box : 0.7.0 shipped in this tree: 0.9.0 shipped in this tree: 0.10.0 live options pass this live options pass this version's validator version's validator CLEARED for deploy. exit 0 CLEARED for deploy. exit 0 Nothing was deployed, `/addons/` was not written, and neither 192.168.2.158 nor the inverter was contacted. ## Numbers in the card that did not reproduce * the `## Unreleased` block is **1541 lines citing 31 ticket ids**, not 5487 and ~70. Those figures fit the **whole file** (5572) and the `## Detail notes` block (3082 lines, 61 ids) - which is PKG-08's subject, not this one's; * **28** runtime commits since `86bdf49`, by `predeploy_check.py`'s own count, not 30; * **18** tickets in the block, not fifteen - 21 with runtime changes once SAFETY-43, TEST-21 and SAFETY-44 are counted. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU
admin added 1 commit 2026-09-16 02:49:32 +02:00
PKG-14: cut 0.10.0, and say what an upgrade from 0.7.0 crosses
checks / unit (pull_request) Successful in 7m51s
cb3f686555
`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
admin merged commit 68c5ede7b9 into release/1.0 2026-09-16 02:58:28 +02:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: admin/goodwe-addon#91