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:
@@ -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