MAINT-05 fix round: grade the migration against the ROSTER, not against the name it was handed
checks / unit (pull_request) Successful in 4m47s
checks / unit (pull_request) Successful in 4m47s
REJECTED because a survivor undoes AC 5, and it survived for the recurring
reason: two fixture values that must be distinguishable were EQUAL.
THE SURVIVOR. app/main.py:282 mutated from `self.inverters[0].name` to `UNIT_A`
left the full 19-file suite green. `inverter.UNIT_A == "inverter-a"`, and every
two-pack fixture in test_maintenance.py named its first roster entry
"inverter-a" - so the two expressions were the same string in every check that
existed. main.py's own comment made the distinction load-bearing and nothing in
the repository could see whether that comment was true.
THE FAILURE IS REAL. `build_roster()` names a declared roster's entries from the
config and numbers a nameless one POSITIONALLY (`inverter-1`), so "inverter-a"
is not even the fallback. On ["garage","shed"] with a flat last_completed of
2026-08-07 the mutant migrates into unit/inverter-a/, pack 0 reads back None,
and both packs come up believing they have never been recalibrated.
AND IT FAILED SILENTLY, which was the worse half: `_verify_moves()` read back
the keys it had just WRITTEN, under the namespace it had just been HANDED, was
satisfied, and MAINT_SCHEMA_KEY was stamped - so the next start skipped the
migration too. A postcondition that reads back the namespace it was handed
cannot detect being handed the wrong one.
WHAT CHANGED
1. THE POSTCONDITION IS GRADED AGAINST THE ROSTER. `_verify_moves()` takes the
names the MACHINES read and reports a namespace that is not among them as
`misdirected` - a new, differently-named failure from `lost`:
* `lost` is "the store took the write and dropped it" - a storage fault,
with a harmless-default case (a dropped `last_completed: None` loses
nothing, because absent and never-recalibrated are the same statement);
* `misdirected` is "the store did exactly as it was told, by a caller that
was wrong". There is no harmless-default version of it - a write nobody
reads is not information whatever its value - so it fails `ok` alone.
`ok` gates the marker, so a misdirected migration is NOT stamped and the next
start re-runs it. Asymmetric on purpose: a re-run is recoverable (rule 1
makes it a no-op, rule 2 left the flat keys to recover from), a silent skip
is not. Part 9b MEASURES the recovery rather than asserting it.
2. `migrate_maint_keys()` TAKES THE ROSTER IN PLACE OF AN `n_units` COUNT. The
count was `len(self.inverters)` at the only call site, so the roster carries
it, and a count beside the names is a second opinion about how many packs
there are.
3. THE FIXTURES CAN NOW REACH THE FAILURE THEY NAME. Parts 9 and 11 run on
["garage","shed"], each behind a PRECONDITION check that the first name is
not UNIT_A - so an edit that makes them equal again goes red instead of
making the claims vacuous. Part 11's fixture date is relative to `now`, not
a literal, so "history preserved" and "history lost" cannot converge as the
file ages past `interval_days`.
MUTATION LEDGER - all red, purged tree, none at +0 B, full detail on the PR:
M1 main.py inverters[0].name -> UNIT_A -16 B 4 checks red
M2 store.py drop `roster=names` from _verify_moves -14 B 5 checks red
M3 store.py drop the misdirected.append -37 B 5 checks red
M4 store.py ok: drop `and not self.misdirected` -25 B 3 checks red
M5 fixture garage -> inverter-a (the trap itself) +4 B 5 checks red
M6 store.py parts[1] -> parts[-1] +1 B 1 check red
FOUND AND NOT FIXED - `migrate_unit_keys()` HAS THE SAME HOLE AND IT IS A LATCH.
It runs BEFORE `build_roster()` (each Inverter restores its latch in its
constructor), so it is handed the constant UNIT_A with no roster to grade
against. On a declared roster a pre-DEV-02 `sign_tripped: true` lands under
unit/inverter-a/sign_tripped, which no unit reads - and
`_grade_store_migration()`, whose whole job is to re-arm exactly this, is
DEFEATED BY THE KEY THE DEFECT WROTE: its "is anything namespaced latched" test
is satisfied by the misdirected value itself. Measured, with the flat-config
counterfactual, and pinned as a running check (test_maintenance.py part 13)
whose assertions state the WRONG behaviour on purpose. It wants its own card:
the remedy is in inverter.py and SAFETY-02's semantics, and a silent expansion
into a safety latch is how a rejected PR becomes a broken one.
Suite: 19/19 PASS, purged tree, bind-mounted :ro in goodwe-addon-ci.
No hardware was touched; every fixture here is SIMULATED and pack B's 3000 W
remains a marked hypothesis.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU
This commit is contained in:
co-authored by
Claude Opus 5
parent
1aef19e995
commit
65cddf35f1
@@ -135,12 +135,19 @@ manufactures them from a **fixture**: the check runs, the assertion is
|
||||
evaluated, the sentence above it is true — and the numbers it compares are
|
||||
arranged so that the failure it describes could not have changed the answer.
|
||||
|
||||
It is not hypothetical and it is not rare. Six instances in two weeks; three
|
||||
It is not hypothetical and it is not rare. Seven instances in two weeks; four
|
||||
were found by review, two - CFG-04's fixture pair - by an author's own mutation
|
||||
battery, which is the cheaper place, and the sixth by review again, in the one
|
||||
battery, which is the cheaper place, and one by review again, in the one
|
||||
place nobody had thought to point the rule at: not a check in the suite, but a
|
||||
COMMITTED EVIDENCE ARTIFACT four documents cite.
|
||||
|
||||
⚠️ **AND IT HAS NOW APPEARED IN FIVE CONSECUTIVE CARDS.** The seventh instance,
|
||||
MAINT-05's, is the one that says why: the defect is not carelessness about
|
||||
fixtures, it is that **a check and the thing it checks were built from the same
|
||||
constant**. Ask of every difference-claim not "is this assertion true" but
|
||||
"where did each side of this comparison come from" - and if the answer is the
|
||||
same place twice, the check is decoration.
|
||||
|
||||
* **ARB-03** shipped a call-site scan asserting that every `claims.append` in
|
||||
`cycle()` routes through `actuator_of()`. Its own parse **saw 3 of the 8
|
||||
appends**; the other five were invisible to it and would have passed however
|
||||
@@ -167,6 +174,28 @@ COMMITTED EVIDENCE ARTIFACT four documents cite.
|
||||
than against its behaviour. It now drives the shipped `run_control()` on a
|
||||
two-inverter config, **under a deadline**, because the mutation's real symptom
|
||||
is that the controller enters its loop and never returns.
|
||||
* **MAINT-05** shipped `Controller.__init__` calling
|
||||
`migrate_maint_keys(store, self.inverters[0].name, ...)` under a comment
|
||||
saying *"it targets `self.inverters[0].name` and NOT `UNIT_A` ... writing
|
||||
`UNIT_A` here would deposit an install's whole maintenance history under a
|
||||
name that is not on the roster at all"*. `inverter.UNIT_A` **is** the string
|
||||
`"inverter-a"`, and every two-pack fixture in `test_maintenance.py` named its
|
||||
first roster entry `inverter-a` — so the two expressions were the same value
|
||||
in every check that existed, and swapping them left **all 19 files green**
|
||||
while losing a real install's whole maintenance history. Two-pack fixtures are
|
||||
now `["garage", "shed"]`, each behind a precondition check (habit 1) that the
|
||||
first name is not `UNIT_A`.
|
||||
|
||||
⚠️ **AND THE POSTCONDITION HAD THE SAME SHAPE AS THE FIXTURE.**
|
||||
`store._verify_moves()` read back the keys it had just written, under the
|
||||
namespace it had just been handed — so it verified clean, and the caller
|
||||
stamped the schema marker, on a migration nothing would ever read. *A check
|
||||
that reads back the namespace it was handed cannot detect being handed the
|
||||
wrong one*: it compared a thing against itself, exactly as DEV-02's fixture
|
||||
compared `(-5000, 5000)` against `(-5000, 5000)`. The habit-2 fix is the same
|
||||
one — grade against something the thing under test did not supply, here the
|
||||
ROSTER the machines read.
|
||||
|
||||
* **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