config.yaml0.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_kwh0.06 -> 0.11736. A site that stored0.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.
`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
`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.02026-09-16 02:58:28 +02:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
config.yaml0.9.0 -> 0.10.0, and the top## Unreleasedblock is closedinto
## 0.10.0with a preamble that names the backward-incompatible changesand 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 withit. 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.
engie_dynamic,totalenergies_dynamicandeneco_eddy. A site set to one is refused by name at startup where itwas accepted before. Measured for all three, with a control on an accepted
id (
generic_dayahead) so the refusals are about the id and not aboutthe blob. The three stay in the
config.yamlenum on purpose, or theSupervisor would reject the options file before
validate.py's messagecould ever be read.
price_taxes_eur_kwh0.06 -> 0.11736. A site thatstored
0.06keeps it - a moved default reaches only keys the blob doesnot carry. ⚠️ The non-obvious half is the refusal that deliberately does
NOT happen: TARIFF-17 made a
price_taxes_profiletable beside anon-default scalar a refusal, and moving the default would have turned
every stored
0.06into "non-default" with nobody editing anything - so aworking 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.1198beside the same table is stillrefused, measured.
shadow_mode, shippedauto. ⚠️ An existing installthat is CONTROLLING does not change by one watt -
autoresolves tonot control_started. A fresh install (auto_start: false) comes upobserving. Measured cross-revision,
8f82dd4->e6da478, same stimulusthrough 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: ongets 13 writes up to 5000 W on the older treeand one 0 W write on the newer.
The 0.7.0 -> 0.10.0 path
ha addons updatedoes 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_waboveSLEW_RAIL_FRAC xthe 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 referenceinstall's actual
slew_w: 400is 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, sothe 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
86bdf49and are not in it:app/delivery.py,app/packshare.py,config.yaml## Unreleasedblock below## 0.9.0app/delivery.py,config.yaml,translations/app/main.py,app/store.pyThey 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
## Unreleasedblock is annotated rather than rewritten oremptied: UX-02 in it shipped in 0.9.0 (its merge is an ancestor of
86bdf49and 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.pypart 11. PKG-09 refuses a deploy whoseversion:didnot move; nothing graded the mirror failure - the version moves and the
## Unreleasedblock is never closed, so whoever opens the file beforedeploying 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.pyby AST rather than transcribed; a slicer with a syntheticpositive control run on every invocation; the count of still-misfiled blocks
frozen so it can only shrink.
All purged, tree bind-mounted
:ro, backups atmktemppaths, revertfingerprinted after every run. No survivors.
## 0.10.0->## Unreleasedversion:0.10.0 -> 0.10.1price.py, documented nowheretest_price.py## 0.9.0heading lost## Unreleasedleft standingSUPERSEDED_TAXES_DEFAULTS_EUR_KWHemptiedtest_validate.pyandtest_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 sincecontents tableper 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_dayaheadcontrol failed for "twoprice sources on one site" because the live blob carries
price_now_entity/price_avg_entity, and the firstprice_taxes_profilefixture was in the wrong syntax and was being refused as malformed JSON rather
than for the reason the check named.
Results
Nothing was deployed,
/addons/was not written, and neither 192.168.2.158 northe inverter was contacted.
Numbers in the card that did not reproduce
## Unreleasedblock is 1541 lines citing 31 ticket ids, not 5487and ~70. Those figures fit the whole file (5572) and the
## Detail notesblock (3082 lines, 61 ids) - which is PKG-08's subject,not this one's;
86bdf49, bypredeploy_check.py's own count,not 30;
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. 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