ARCH-02: rehearse the flat-to-roster switch, and it comes up disarmed
Nothing had ever run the real `Controller` through the transition inverter B's commissioning performs on 2026-09-12. `tools/rehearse_roster.py` does, over a COPY of a state file given as an input path, and reports the difference: every store key that moved, appeared, vanished or was STRANDED under a name no machine reads; every entity id before and after with orphans named; and whether the maintenance history MAINT-05 moved once survived being moved again. ⭐ WHAT IT FOUND, AND IT IS SAFETY-44's ANSWER. A store that ran pre-DEV-02 flat and declares a multi-inverter roster comes up with its SAFETY-02 latch DISARMED unless the first roster entry happens to be called `inverter-a`. `Controller.__init__` hands `migrate_unit_keys()` the hardcoded `UNIT_A`, so the latch lands in `unit/inverter-a/sign_tripped`; a roster of ["garage","shed"] reads `unit/garage/sign_tripped`, which is absent. And `_grade_store_migration()` - the guard written for exactly this - sees the migration's OWN misdirected key satisfy its `namespaced_latched` test and returns without re-arming. Measured, not argued: the file says `sign_tripped: true`, no inverter is latched, nothing is held at 0 W. The same state file onto a roster whose first entry IS `inverter-a` comes up latched, so the finding is attributable to the name. The staged upgrade reaches it too: the flat start stamps `schema_version`, and the roster start's migration then takes its early return. ⚠️ NO RUNTIME CODE IS CHANGED HERE. The fix to `migrate_unit_keys()` belongs to SAFETY-44 reopened as a defect, shown red before it; this commit is the instrument that shows it red. THE CHECK CAN FAIL, AND WAS SEEN TO. `test_rehearse.py` establishes two CLEAN CONTROLS first - two transitions the shipped code gets right, on which the harness must say nothing - and then breaks the shipped migration and the entity-id composition six ways in a private copy of the tree, requiring each to turn its control from green to red WITH THE RIGHT FINDING NAMED. One mutation is required to SURVIVE, with the reason written down. The synthetic state carries maintenance history and a legacy-shaped latched `sign_tripped` with no `schema_version` beside it, because state built by the happy path cannot detect a migration that only mishandles legacy shapes. ⚠️ THE FIRST DRAFT OF THE HARNESS HAD HABIT 4 IN IT and its own battery caught it - MUTATION.md's thirteenth instance, third in an instrument. The entity diff composed both sides with the same build, so renaming the MQTT device moved every id equally and it reported "no orphans" about a change that orphans everything. `--live-entities` is the fix: a reference that does not move with the code. THE RETREAT is supported and does not corrupt state - the flat rollback view keeps the latch and the first pack's history live - but it orphans eight HA entities, and the ROUND TRIP is not supported: a maintenance cycle completed while rolled back is discarded on re-advance. Stated in DOCS.md as a decision. Fetching /data/state.json, the options blob and the entity list are OWNER STEPS and are written down. ⚠️ Nothing here was run against real state and nothing here claims to have been. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU
This commit is contained in:
@@ -225,6 +225,32 @@ same place twice, the check is decoration.
|
||||
forever, *including after the call site is fixed*. The check was true and
|
||||
passed for the wrong reason. Habit **9** below is what falls out of it.
|
||||
|
||||
* ⚠️ **ARCH-02's OWN REHEARSAL HARNESS, CAUGHT BY ITS OWN MUTATION BATTERY —
|
||||
the THIRTEENTH instance, and the third in an INSTRUMENT rather than in a
|
||||
check.** `tools/rehearse_roster.py` reports orphaned Home Assistant entities
|
||||
by composing the ids the add-on announces **before** the roster switch and
|
||||
**after** it, and diffing. Both sides are composed by the same build. So the
|
||||
two mutations that change the id composition itself — renaming the MQTT device
|
||||
(`"GoodWe"` -> `"GoodWe Controller"`), renaming one site sensor (`"Grid power"`
|
||||
-> `"Grid power W"`) — moved **every id on both sides equally**, and the
|
||||
harness reported *"no orphans"* about a change that orphans the entire live
|
||||
entity list. It read exactly like a clean transition. The first draft's
|
||||
M5/M6 came back `exit 0` and `exit 2`, and neither was the failure they name.
|
||||
|
||||
The fix is habit 2 with a filename: **the reference has to be data that does
|
||||
not move with the code.** `--live-entities` takes a list of the ids the
|
||||
install actually has, `tools/rehearsal/live-entities-recorded.txt` is the
|
||||
recorded stand-in that keeps it live in CI, and a build that announces an id
|
||||
the list does not carry is now a `COMPOSITION DRIFT` finding rather than a
|
||||
silence. ⚠️ **The anchor moved for the same reason**: it had been
|
||||
`compose(mqtt.DEVICE["name"], p1["name"]) == "sensor.goodwe_p1_sample_age"`,
|
||||
which any renaming of the device satisfies by moving both operands, and it is
|
||||
now composed from two recorded **literals**.
|
||||
|
||||
Ask of an instrument what habit 4 asks of a check: *where did each side of
|
||||
this comparison come from* — and if a mutation of the subject moves both
|
||||
sides, the instrument is decoration.
|
||||
|
||||
* **CFG-04's `tools/supervisor_shape_probe.py` §B** — and this one is the rule
|
||||
reaching outside the suite. The probe is the committed evidence that
|
||||
`config.yaml`'s `inverters: []` default is load-bearing, and four documents
|
||||
|
||||
Reference in New Issue
Block a user