MAINT-05 fix round 2: the refusal has to survive the NEXT start, and both marker guards have to be visible
checks / unit (pull_request) Successful in 5m14s
checks / unit (pull_request) Successful in 5m14s
REJECTED because the fix round's own load-bearing sentence was FALSE, and the
whole suite was blind to that. `_verify_moves()` produced the red the reviewer
reproduced - that half stood - but DECISIONS §2 claimed "`ok` gates the marker,
so a misdirected migration is not stamped and the next start runs it again ...
a re-run is recoverable". THE REFUSAL LASTED EXACTLY ONE START.
MEASURED, Controller level, ONE persisted store, roster ["garage","shed"], flat
last_completed 2026-08-07, call site still handing `inverter-a` on both starts:
construction 1: ok=False misdirected=4 moved=4 kept=0 marker=<absent>
construction 2: ok=True misdirected=0 moved=0 kept=4 marker=1
history parked under unit/inverter-a/...; marker stamped after ONE restart.
WHY. `_verify_moves()` graded only `rep.moved`. On start 2 the same wrong call
site finds the four keys already present, so rule 1 routes every one of them to
`rep.kept`, `rep.moved` is EMPTY, the roster loop iterates over nothing,
`misdirected` is empty and `ok` is True. Start 3 then takes the
`from_version >= MAINT_SCHEMA_VERSION` early return FOREVER - including after
the call site is fixed. Loud-and-recoverable became silent-and-permanent after
one restart: the exact state store.py's header says must never happen, reached
by the fix written to prevent it.
WHY THE CHECK MISSED IT - the recurring defect, this time INSIDE the remedy.
Part 9b's recovery check was `migrate_maint_keys(_m5, ROSTER[0], ROSTER)` - the
CORRECTED name. It measures recovery along the one path where the operator has
ALREADY fixed the bug. Nothing in the 19 files called `migrate_maint_keys()`
twice with the SAME WRONG `unit`. The check was true and passed for the wrong
reason.
WHAT CHANGED
1. app/store.py `_verify_moves()` grades `set(rep.moved) | set(rep.kept)`.
Sound by construction: every entry in both lists is a `unit_key(unit,
legacy)` built from the SAME `unit` argument, so grading `kept` grades the
same namespace decision `moved` does. A correct call site's `kept` entries
sit under a roster name and pass; a wrong one cannot launder its namespace by
having already written it. `migrate_unit_keys()` passes `roster=None` and
returns before the loop, so it is untouched.
2. test_maintenance.py part 9c - THE CHECK THAT WAS MISSING. Two starts, SAME
wrong unit, ONE store: the marker is still withheld on the second, the
refusal names the same roster, the flat history is still there to recover
from, and a third start with the CORRECTED name still gets a run (which a
stamped marker would have made impossible). Its PRECONDITION asserts the
SHAPE of the second report - `moved` empty, `kept` full - so it cannot
quietly revert to grading 9b's path. Counterfactual beside it: the same two
calls with the right name are accepted, then skipped as `already at schema`.
3. test_maintenance.py part 9d - THE OTHER GUARD ON THE SAME LINE, and a
survivor nothing in the repository could see. `if rep.ok:` is
`not lost and not misdirected`; every check that reached the marker did so on
a store whose `kept` was empty, so
if rep.ok: -> if rep.ok or rep.kept: (+12 B)
stamped the marker on a migration that LOST A MONTH OF HISTORY - defeating
the older, settled `lost` guard as well as the new one - and ALL 19 SUITES
WERE GREEN on it (measured at 65cddf3, purged, :ro). 9d's fixture is the one
shape that separates the guards: two keys already namespaced (`kept`
non-empty, the mutant's `or` fires) and two whose write the store drops
(`lost` non-empty), `misdirected` empty throughout.
4. FOUND HERE AND CLOSED: THE SAME MUTATION ONE FUNCTION UP. `migrate_unit_keys()`
- SAFETY-02's LATCH migration - gates its own marker the same way, and every
check that reached it ran on a store with an empty `kept`. `if rep.ok or
rep.kept:` at store.py:307 SURVIVED ALL 19 FILES. It stamps `schema_version`
on a migration that DROPPED THE LATCH: the guard is off, the file still reads
`sign_tripped: true`, and the store now says the migration finished, so the
next start skips the one thing that would re-arm it. test_inverter.py part 10
now drops the `sign_tripped` write BESIDE an already-namespaced
`sign_reason`, with the accepted-baseline counterfactual. No source change -
the shipped gate was right; nothing could see it. Mutation table 34 -> 35.
5. DECISIONS/MAINT-05 §2 CORRECTED. The paragraph that stated the false
guarantee now states what was measured, why grading `moved` alone could not
hold, and why the union is sound. §7 records the marker-gate hole found in
`migrate_unit_keys()` and that it was closed. MUTATION.md gains habit 9 -
"exercise a refusal a SECOND time with the SAME wrong input, not the
corrected one" - and two survey rows.
MUTATION LEDGER - 11 mutations, ALL RED on the final tree, every run on a fresh
copy with the bytecode cache purged (entry count printed before and after) and
the tree mounted :ro so none could be written. Two were survivors BEFORE this
commit and both are recorded as such. Full transcript on the PR.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU
This commit is contained in:
@@ -62,6 +62,39 @@ two power-moving phases on one grid connection pulling against each other.
|
||||
Every two-pack fixture that could see this now names its first pack
|
||||
`garage`; with them all called `inverter-a`, `self.inverters[0].name` and
|
||||
`UNIT_A` were the same string and the swap left all 19 suites green.
|
||||
* **AND THE REFUSAL NOW SURVIVES THE NEXT START** (second fix round). The fix
|
||||
above graded only the keys it had just *written*, so it refused exactly once:
|
||||
measured at Controller level over one persisted store, roster
|
||||
`["garage", "shed"]`, with the call site still wrong on both starts —
|
||||
construction 1 `ok=False misdirected=4 moved=4 kept=0 marker=<absent>`,
|
||||
construction 2 `ok=True misdirected=0 moved=0 kept=4` and the marker
|
||||
**stamped**. On the second start rule 1 finds the wrong keys already there and
|
||||
routes all four to `kept`, so `moved` is empty and the roster loop grades
|
||||
nothing — and start 3 skips the migration forever, *including after the call
|
||||
site is fixed*. Loud-and-recoverable became silent-and-permanent after one
|
||||
restart. `_verify_moves()` now grades `set(moved) | set(kept)`; both lists are
|
||||
built from the same `unit` argument, so grading `kept` grades the same
|
||||
namespace decision. `test_maintenance.py` part **9c** drives two starts with
|
||||
the same wrong unit over one store — the shape nothing in the suite had, since
|
||||
9b measured recovery along the path where the operator has *already* fixed the
|
||||
call site.
|
||||
* **AND THE MARKER GATE IS PINNED ON BOTH GUARDS.** `if rep.ok:` is
|
||||
`not lost and not misdirected`, and the mutation `if rep.ok or rep.kept:`
|
||||
stamped the marker on a migration that **lost a month of history** with all 19
|
||||
suites green. Part **9d** is the fixture that separates the two guards: two
|
||||
keys already namespaced (`kept` non-empty) and two whose write the store
|
||||
drops (`lost` non-empty), `misdirected` empty throughout.
|
||||
* ⚠️ **AND THE SAME MUTATION ONE FUNCTION UP WAS A SURVIVOR, FOUND HERE AND
|
||||
CLOSED.** `migrate_unit_keys()` — SAFETY-02's **latch** migration — gates its
|
||||
own marker on `rep.ok` too, and every check that reached that gate ran on a
|
||||
store where nothing was namespaced yet, so `rep.kept` was empty in all of
|
||||
them. `if rep.ok or rep.kept:` there stamps `schema_version` on a migration
|
||||
that **dropped the latch**: the guard is off, the file still says
|
||||
`sign_tripped: true`, and the next start skips the migration that would have
|
||||
re-armed it. Measured a survivor of all 19 files; now red on
|
||||
`test_inverter.py` part 10, which drops the `sign_tripped` write beside an
|
||||
already-namespaced `sign_reason`. No source change — the shipped gate was
|
||||
right; nothing could see it.
|
||||
* ⚠️ **FOUND, RECORDED, NOT FIXED (SECOND ONE).** `migrate_unit_keys()` —
|
||||
SAFETY-02's latch migration — has the identical hole and it costs a **guard**
|
||||
rather than a history. It runs before `build_roster()` (each `Inverter`
|
||||
|
||||
@@ -120,13 +120,76 @@ reported as `misdirected` — a **different** failure from `lost`:
|
||||
not information, whatever its value. So anything in it fails `ok` on its own.
|
||||
|
||||
And **`ok` gates the marker**, so a misdirected migration is not stamped and the
|
||||
next start runs it again. That is the decision the fix round was asked to
|
||||
record, and the argument for it is asymmetric: a re-run is recoverable — rule 1
|
||||
makes it a no-op where keys did land, and rule 2 left the flat keys in the file
|
||||
to recover *from* — while a silent skip is not recoverable at all, because the
|
||||
store now claims the migration happened. `test_maintenance.py` part 9b measures
|
||||
the recovery rather than asserting it: the same store, re-run with the roster's
|
||||
own first name, comes back with its month of history.
|
||||
next start runs it again. The argument for it is asymmetric: a re-run is
|
||||
recoverable — rule 1 makes it a no-op where keys did land, and rule 2 left the
|
||||
flat keys in the file to recover *from* — while a silent skip is not recoverable
|
||||
at all, because the store now claims the migration happened.
|
||||
|
||||
### ⚠️ THE PARAGRAPH ABOVE USED TO END HERE, AND AS IT STOOD IT WAS FALSE
|
||||
|
||||
**The refusal lasted exactly one start. The second start stamped the marker.**
|
||||
That is a correction to what the first fix round wrote down, not a new
|
||||
qualification of it, and it is recorded here because a decision document that
|
||||
states a guarantee the code does not provide is worse than one that says
|
||||
nothing.
|
||||
|
||||
`_verify_moves()` graded only `rep.moved`. Measured at Controller level over
|
||||
**one persisted store**, roster `["garage", "shed"]`, flat `last_completed`
|
||||
2026-08-07, with the call site still handing `inverter-a` on **both** starts:
|
||||
|
||||
```
|
||||
construction 1: ok=False misdirected=4 moved=4 kept=0 marker=<absent> (withheld)
|
||||
construction 2: ok=True misdirected=0 moved=0 kept=4 marker=1 (STAMPED)
|
||||
history parked under unit/inverter-a/…; marker stamped after ONE restart.
|
||||
```
|
||||
|
||||
On start 2 the same wrong call site finds the four keys it wrote already
|
||||
present, so **rule 1** — never overwrite a namespaced value — routes every one
|
||||
of them to `rep.kept`. `rep.moved` is empty, the roster-grading loop iterates
|
||||
over nothing, `misdirected` is empty, `ok` is True, the marker is stamped, and
|
||||
start 3 takes the `from_version >= MAINT_SCHEMA_VERSION` early return **forever
|
||||
— including after the call site is fixed**. Loud-and-recoverable became
|
||||
silent-and-permanent after one restart: exactly the state `store.py`'s header
|
||||
says must never happen, reached by the fix written to prevent it.
|
||||
|
||||
**The remedy is one line**, and it is sound by construction rather than by
|
||||
argument: grade `set(rep.moved) | set(rep.kept)`. Every entry in *both* lists is
|
||||
a `unit_key(unit, legacy)` built from the **same** `unit` argument, so grading
|
||||
`kept` grades the same namespace decision `moved` does. A correct call site's
|
||||
`kept` entries are under a roster name and pass; a wrong one cannot launder its
|
||||
namespace by having already written it. `migrate_unit_keys()` passes
|
||||
`roster=None` and returns before the loop, so it is unaffected.
|
||||
|
||||
**Why the check missed it, which is the part worth keeping.** Part 9b's recovery
|
||||
check was `migrate_maint_keys(_m5, ROSTER[0], ROSTER)` — the **corrected** name.
|
||||
It measures recovery along the one path where the operator has *already fixed
|
||||
the bug*. The real failure is a call site that is still wrong on the next start,
|
||||
and nothing in the 19 files called `migrate_maint_keys()` twice with the **same
|
||||
wrong** `unit`. The check was true, passed, and could not reach the failure its
|
||||
sentence named — MUTATION.md's third defect, this time sitting inside the fix
|
||||
written to close it. Part **9c** now drives two starts with the same wrong unit
|
||||
over one store, asserts the marker is still withheld on the second, and asserts
|
||||
the *shape* of the second report (`moved` empty, `kept` full) so it cannot
|
||||
quietly revert to 9b's path.
|
||||
|
||||
### The marker gate is `rep.ok` and nothing weaker — both guards, each pinned
|
||||
|
||||
`if rep.ok:` is `not lost and not misdirected`, and until this round only the
|
||||
`misdirected` half was reachable by a check that also had a non-empty `kept`.
|
||||
The mutation
|
||||
|
||||
```
|
||||
if rep.ok: -> if rep.ok or rep.kept:
|
||||
```
|
||||
|
||||
stamps the marker on a migration that **lost a month of history** — defeating
|
||||
the older, settled `lost` guard as well as the new one — and **all 19 suites
|
||||
were green on it**. Part **9d** is the fixture that separates them: two keys
|
||||
already namespaced (`kept` non-empty, so the mutant's `or` fires) and two keys
|
||||
whose write the store drops (`lost` non-empty, so `ok` is False), with
|
||||
`misdirected` empty throughout so the section cannot pass on the other guard by
|
||||
accident. Measured differential on it: shipped → marker absent; mutant → marker
|
||||
`1`, both reporting `ok=False lost=2 kept=2`.
|
||||
|
||||
**Why the fixtures had to change, and this is the part that got the card
|
||||
rejected.** Nothing in the repository could see whether any of the above was
|
||||
@@ -297,3 +360,29 @@ It is pinned as a running check (`test_maintenance.py` part 13) rather than left
|
||||
as a comment. ⚠️ **Its assertions state the WRONG behaviour on purpose** — read
|
||||
them as the specification of a defect; the card that fixes it should expect them
|
||||
to go red and should replace them.
|
||||
|
||||
### And its MARKER GATE had the round-3 hole too — found here, closed here
|
||||
|
||||
Separate from the namespace hole above, and **fixed** rather than recorded,
|
||||
because closing it cost one test fixture and no source change.
|
||||
`migrate_unit_keys()` gates `schema_version` on `rep.ok` exactly as
|
||||
`migrate_maint_keys()` gates its own marker, and every check that reached that
|
||||
gate ran on a store where **nothing** was namespaced yet — so `rep.kept` was
|
||||
empty in all of them and
|
||||
|
||||
```
|
||||
if rep.ok: -> if rep.ok or rep.kept:
|
||||
```
|
||||
|
||||
**survived all 19 files**. What it does is stamp the schema marker on a
|
||||
migration that DROPPED THE LATCH: the guard is off, the state file still reads
|
||||
`sign_tripped: true`, and because the store now says the migration finished, the
|
||||
next start skips the one thing that would have re-armed it. DEV-02's own worst
|
||||
case, filed under *done*.
|
||||
|
||||
`test_inverter.py` part 10 now drops the `sign_tripped` write **beside an
|
||||
already-namespaced `sign_reason`** — `lost` non-empty *and* `kept` non-empty,
|
||||
which is the one shape none of the existing fixtures had — and asserts the
|
||||
marker is withheld, with the accepted-baseline counterfactual beside it (habit
|
||||
7). The shipped gate was correct throughout; what changed is that a check can
|
||||
now see it.
|
||||
|
||||
@@ -135,13 +135,14 @@ 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. Seven instances in two weeks; four
|
||||
It is not hypothetical and it is not rare. EIGHT 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 one by review again, in the one
|
||||
battery, which is the cheaper place, 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.
|
||||
COMMITTED EVIDENCE ARTIFACT four documents cite - and the eighth INSIDE THE FIX
|
||||
WRITTEN TO CLOSE THE SEVENTH.
|
||||
|
||||
⚠️ **AND IT HAS NOW APPEARED IN FIVE CONSECUTIVE CARDS.** The seventh instance,
|
||||
⚠️ **AND IT HAS NOW APPEARED IN SIX 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
|
||||
@@ -196,6 +197,22 @@ same place twice, the check is decoration.
|
||||
one — grade against something the thing under test did not supply, here the
|
||||
ROSTER the machines read.
|
||||
|
||||
⚠️ **AND THE FIX ITSELF CARRIED THE DEFECT, WHICH IS THE SIXTH CARD IN A ROW
|
||||
AND THE FIRST TIME IT HAS APPEARED *INSIDE* A REMEDY.** The fix round's
|
||||
recovery check was `migrate_maint_keys(_m5, ROSTER[0], ROSTER)` — the
|
||||
**corrected** name. It measures recovery along the one path where the operator
|
||||
has *already fixed the bug*, and nothing in the 19 files called
|
||||
`migrate_maint_keys()` twice with the **same wrong** `unit`. So the refusal
|
||||
the card was rejected twice to obtain lasted **exactly one start**: measured
|
||||
at Controller level over one persisted store, roster `["garage", "shed"]` —
|
||||
construction 1 `ok=False misdirected=4 moved=4 kept=0 marker=<absent>`,
|
||||
construction 2 `ok=True misdirected=0 moved=0 kept=4 marker=1`. On start 2
|
||||
rule 1 routes the wrongly-written keys to `rep.kept`, `rep.moved` is empty,
|
||||
the roster loop iterates over nothing, and the marker is stamped on a
|
||||
migration that reached no readable key — after which the early return skips it
|
||||
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.
|
||||
|
||||
* **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
|
||||
@@ -270,6 +287,18 @@ Cheap habits fall out of it, and all of them are now used in this repository:
|
||||
red. `inverters: []` was deleted from `config.yaml` and the probe re-run
|
||||
against `ghcr.io/home-assistant/amd64-hassio-supervisor:2026.08.0` for
|
||||
exactly that reason.
|
||||
9. **Exercise a refusal a SECOND time with the SAME wrong input — not the
|
||||
corrected one.** A guard that refuses once and then admits the same bad
|
||||
caller is not a guard, it is a delay, and the second call is a *different
|
||||
code path* whenever the first call left state behind. MAINT-05's roster
|
||||
grading iterated the keys it had just **written**; on the second start the
|
||||
same wrong caller found them already present, they moved to the
|
||||
`already namespaced` list, and the loop graded an empty set. Re-running with
|
||||
the **fixed** argument — which is what the check did — measures the operator's
|
||||
recovery, never the persistence of the refusal. Ask of every refusal: *what
|
||||
does this do when the caller has not been fixed and simply runs again?* And
|
||||
assert the **shape** of that second report, not only its verdict, so the
|
||||
check cannot silently revert to grading the first path.
|
||||
|
||||
And the mutation-table consequence: **a variant is not covered because a
|
||||
near-miss of it is.** DEV-02's table listed the *actuator* variant of the
|
||||
@@ -326,6 +355,8 @@ commit and PR bodies.
|
||||
| **TARIFF-01** reviewer survivor: `record_block()` ignoring its `policy` argument (0dbd485) | **re-verified — still a survivor** | re-run here through `tools/mutation_run.sh` against the 15 suites that were on the record, all green with `billed_w(block, policy)` mutated to `billed_w(block, POLICY_NET)`. The recorded diagnosis holds: every block reaching the store is single-phase, where NET, SUM and MAX collapse. **Now CLOSED by TEST-13** (`test_tariff.py` section 7), which round-trips section 7's own unbalanced `(3000, 500, -200) W` block THROUGH the store under each policy - three distinct banked figures, so a hardcoded policy takes at least two of them red. Third instance of "a stimulus evaluated where the quantity it grades is degenerate", after WITNESS-02's span normalisation and TARIFF-01's own DST window. |
|
||||
| **SAFETY-25** 8 mutations against `test_peakguard.py` part 10, then 9 more after review, all red | **not at risk** | each run against a tree copied fresh with `__pycache__` excluded and mounted `:ro`, so no `.pyc` existed to go stale - the same structural immunity recorded for CI above. Byte deltas recorded on the PR; none is `+0 B`. All are **reds**, which is the direction this defect cannot fabricate. ⚠️ The second round exists because the first round's mutations were all aimed at DISARMING the guard, and the claim part 10 actually made was about the guard being FIXED - the reviewer built the fix and the suite stayed green. Bytecode was never the issue there; an unaimed battery was. |
|
||||
| **SAFETY-03** reviewer survivor: `abs()` dropped from `clamp_to_rating`'s limit (6aabb83) | **re-verified — still a survivor** | re-run the same way, all 15 green. Also as recorded: unreachable under the `int(100, 5000)` schema. |
|
||||
| **MAINT-05 round 3**, `migrate_maint_keys()` marker gate `if rep.ok:` → `if rep.ok or rep.kept:` (`+12 B`) | **was a survivor — now CLOSED** | measured against head `65cddf3` under the purge, tree mounted `:ro`, `19/19 PASS`. It stamps the schema marker on a migration that LOST state, defeating the older `lost` guard as well as the new `misdirected` one, because every check that reached the marker did so on a store whose `kept` list was empty. Closed by `test_maintenance.py` **9d**; the mutation is now red on three named checks. |
|
||||
| **MAINT-05 round 3**, `migrate_unit_keys()` marker gate `if rep.ok:` → `if rep.ok or rep.kept:` (`+12 B`) | **found here — SAFETY-02's half, now CLOSED** | the SAME mutation one function up, on the LATCH migration rather than the history one. Measured a survivor against this branch under the purge, `:ro`, `19/19 PASS`, then closed by `test_inverter.py` part 10's new fixture — a store with `unit/inverter-a/sign_reason` already present (so `kept` is non-empty) whose `sign_tripped: true` write is dropped. Shipped: marker withheld. Mutant: marker stamped on a **disarmed latch**, and the next start skips the migration that would have re-armed it. |
|
||||
| **TARIFF-01**'s other three survivors, **SAFETY-03**'s `describe()` survivor, **TARIFF-02**'s M3/M5/M7, **SAFETY-18**'s three | **still unverified** | recorded before the purge discipline, with no byte delta on the record, so a `+0 B` same-second revert cannot be excluded. All are survivors, which is the direction this defect fabricates. TARIFF-02's and SAFETY-18's were **closed by later commits whose claims are reds**, so what is unverified is the *diagnosis*, not the current state of the code. |
|
||||
|
||||
**Nothing here says a past result is wrong.** The two survivors re-run above
|
||||
|
||||
@@ -373,6 +373,16 @@ def _verify_moves(store, rep: MigrationReport, roster=None) -> None:
|
||||
next start runs the migration again. A re-run is recoverable, by rule 1; a
|
||||
silent skip is the state store.py's header says must never happen.
|
||||
|
||||
⚠️ AND THE GRADED SET IS `moved` UNION `kept`, WHICH IS WHAT MAKES THAT
|
||||
PARAGRAPH TRUE ON THE SECOND START AS WELL AS THE FIRST. Graded over `moved`
|
||||
alone the refusal lasted exactly one start: the wrong call site is still
|
||||
wrong next time, finds the keys it wrote already present, and rule 1 routes
|
||||
every one of them to `kept` - so `moved` is empty, this loop sees nothing,
|
||||
`ok` comes back True and the marker is stamped on a migration that has
|
||||
reached no readable key. The measurement is in the comment on the loop
|
||||
itself. `test_maintenance.py` section 9c calls this twice with the SAME
|
||||
wrong `unit` over one store, which is the shape nothing in the suite had.
|
||||
|
||||
`roster=None` means "not graded", and the one caller that passes it is
|
||||
`migrate_unit_keys()`, which runs BEFORE `build_roster()` for the reason its
|
||||
own docstring gives - the `Inverter` objects restore their latch in their
|
||||
@@ -396,7 +406,32 @@ def _verify_moves(store, rep: MigrationReport, roster=None) -> None:
|
||||
names = [str(n) for n in roster]
|
||||
# `unit_key()` builds `unit/<unit>/<key>`, so the unit is field 1. Split
|
||||
# through the same spelling the writer used rather than re-deriving it.
|
||||
for target in sorted(rep.moved):
|
||||
#
|
||||
# ⚠️ `moved` UNION `kept`, AND THE UNION IS THE WHOLE OF THE SECOND FIX
|
||||
# ROUND. Grading only `moved` made the refusal last EXACTLY ONE START.
|
||||
# Measured at Controller level over one persisted store, roster
|
||||
# ["garage", "shed"], flat `last_completed` 2026-08-07, with the call site
|
||||
# still handing `inverter-a` on both starts:
|
||||
#
|
||||
# construction 1: ok=False misdirected=4 moved=4 kept=0 marker=<absent>
|
||||
# construction 2: ok=True misdirected=0 moved=0 kept=4 marker=1
|
||||
#
|
||||
# On start 2 the four keys the WRONG start 1 wrote are already present, so
|
||||
# rule 1 routes every one of them to `rep.kept`, `rep.moved` is EMPTY, this
|
||||
# loop iterates over nothing, `misdirected` is empty and `ok` is True - so
|
||||
# the caller stamps the marker and start 3 takes the `from_version >=
|
||||
# SCHEMA` early return FOREVER, including after the call site is fixed.
|
||||
# Loud-and-recoverable became silent-and-permanent after one restart: the
|
||||
# exact state this module's header says must never happen, reached by the
|
||||
# fix that was written to prevent it.
|
||||
#
|
||||
# The union is sound by construction rather than by hope: every entry in
|
||||
# both `rep.moved` and `rep.kept` is a `unit_key(unit, legacy)` built from
|
||||
# the SAME `unit` argument, so grading `kept` grades the same namespace
|
||||
# decision `moved` does. A correct call site puts its `kept` entries under a
|
||||
# roster name and they pass; a wrong one cannot launder its namespace by
|
||||
# having already written it.
|
||||
for target in sorted(set(rep.moved) | set(rep.kept)):
|
||||
parts = target.split("/")
|
||||
unit = parts[1] if len(parts) > 2 else ""
|
||||
if unit in names:
|
||||
@@ -443,7 +478,9 @@ def migrate_maint_keys(store, unit: str, roster) -> MigrationReport:
|
||||
marker. Handing the roster in means the read-back is graded against the
|
||||
keys the MACHINES read, and a `unit` that is not among these names comes
|
||||
back `misdirected`: not `ok`, marker NOT written, migration runs again next
|
||||
start. See `_verify_moves()` for the install this was measured on.
|
||||
start - and again the start after that, because `_verify_moves()` grades
|
||||
`kept` as well as `moved`. See `_verify_moves()` for the install this was
|
||||
measured on, and for what grading `moved` alone cost on start 2.
|
||||
|
||||
THE THREE RULES ARE `migrate_unit_keys()`'s, SHARED IN CODE rather than
|
||||
restated: `_move_flat_keys()` is rule 1 (never overwrite a namespaced value)
|
||||
@@ -481,6 +518,14 @@ def migrate_maint_keys(store, unit: str, roster) -> MigrationReport:
|
||||
_move_flat_keys(store, unit, MAINT_UNIT_KEYS, rep)
|
||||
_verify_moves(store, rep, roster=names)
|
||||
|
||||
# ⚠️ `rep.ok` AND NOTHING ELSE - NOT `ok or kept`, NOT `not lost`, NOT
|
||||
# `not misdirected`. BOTH guards gate this line and each one is pinned by a
|
||||
# check that the other cannot satisfy: section 9c refuses a store whose
|
||||
# failure is `misdirected` with `lost` empty, and section 9d refuses one
|
||||
# whose failure is `lost` with `misdirected` empty AND `kept` non-empty -
|
||||
# the mutation `if rep.ok or rep.kept:` stamps the marker on a migration
|
||||
# that lost a month of history, and until section 9d existed all 19 suites
|
||||
# were green on it.
|
||||
if rep.ok:
|
||||
store.set(MAINT_SCHEMA_KEY, MAINT_SCHEMA_VERSION)
|
||||
else:
|
||||
|
||||
@@ -572,6 +572,58 @@ check("...and a store that only fails to persist a DEFAULT is not reported as "
|
||||
"dismiss before the real one arrives",
|
||||
migrate_unit_keys(LosesTheLatch(), UNIT_A).ok is True)
|
||||
|
||||
# ⚠️ AND THE MARKER GATE, ON A STORE WHERE SOMETHING WAS ALREADY NAMESPACED.
|
||||
# FOUND BY MAINT-05's ROUND-3 BATTERY, AND IT WAS A SURVIVOR OF ALL 19 FILES.
|
||||
# `migrate_unit_keys()` gates `SCHEMA_KEY` on `rep.ok`, and every check above
|
||||
# reaches that gate on a store where NOTHING was already under `unit/...` - so
|
||||
# `rep.kept` is empty in all of them, and the mutation
|
||||
#
|
||||
# if rep.ok: -> if rep.ok or rep.kept:
|
||||
#
|
||||
# stamped the marker on a migration that DROPPED THE LATCH, with the whole suite
|
||||
# green. That is a disarmed safety guard plus a store that says the migration
|
||||
# finished, so the next start skips the one thing that would have re-armed it -
|
||||
# DEV-02's own worst case with the paper trail filed under "done".
|
||||
#
|
||||
# The fixture is the one shape that separates them: `sign_reason` ALREADY
|
||||
# namespaced (so `kept` is non-empty and the mutant's `or` fires) and the
|
||||
# `sign_tripped: true` write dropped on the floor (so `lost` is non-empty and
|
||||
# `ok` is False). `migrate_unit_keys()` is passed no roster, so `misdirected`
|
||||
# cannot be what refuses this - it is the `lost` guard or nothing.
|
||||
_kept_reason = "SIGN_DISAGREEMENT: already namespaced by a later version"
|
||||
_broken_kept = LosesTheLatch({
|
||||
"sign_tripped": True,
|
||||
"sign_reason": "SIGN_DISAGREEMENT: measured",
|
||||
unit_key(UNIT_A, "sign_reason"): _kept_reason})
|
||||
_rep_kept = migrate_unit_keys(_broken_kept, UNIT_A)
|
||||
print(f" a dropped latch write beside an already-namespaced reason: "
|
||||
f"{_rep_kept.explain()}")
|
||||
print(f" lost={len(_rep_kept.lost)} kept={len(_rep_kept.kept)} "
|
||||
f"ok={_rep_kept.ok}; marker "
|
||||
f"{_broken_kept.data.get(SCHEMA_KEY, '<<absent>>')!r}")
|
||||
check("PRECONDITION: this fixture reaches the marker gate through the `lost` "
|
||||
"guard with a NON-EMPTY `kept`, which is the combination none of the "
|
||||
"checks above has. One key dropped, one key left alone, `ok` False. If a "
|
||||
"later edit empties `kept` here the mutation goes invisible again, so the "
|
||||
"shape is asserted rather than assumed",
|
||||
_rep_kept.ran and len(_rep_kept.lost) == 1 and len(_rep_kept.kept) == 1
|
||||
and not _rep_kept.ok
|
||||
and _broken_kept.data[unit_key(UNIT_A, "sign_reason")] == _kept_reason)
|
||||
check("⚠️ AND THE STORE IS NOT MARKED MIGRATED. The key that was ALREADY there "
|
||||
"does not buy a marker for the latch that was not: stamping here would "
|
||||
"tell the next start that the migration which dropped `sign_tripped` had "
|
||||
"finished, so it would never run again and the guard would stay off on an "
|
||||
"install whose file still reads `sign_tripped: true`",
|
||||
SCHEMA_KEY not in _broken_kept.data)
|
||||
check("...and the counterfactual, one property different: the SAME store shape "
|
||||
"on a store that does not drop writes IS marked migrated. So the refusal "
|
||||
"above is about the latch that was lost, not about `kept` being non-empty",
|
||||
migrate_unit_keys(
|
||||
MemStore({"sign_tripped": True,
|
||||
"sign_reason": "SIGN_DISAGREEMENT: measured",
|
||||
unit_key(UNIT_A, "sign_reason"): _kept_reason}),
|
||||
UNIT_A).ok is True)
|
||||
|
||||
_broken2 = LosesTheLatch({"sign_tripped": True,
|
||||
"sign_reason": "SIGN_DISAGREEMENT: measured"})
|
||||
_c2d, _ = built(store=_broken2)
|
||||
@@ -2149,6 +2201,16 @@ MUTATIONS = [
|
||||
("validate.py allowing two roster entries with the SAME name",
|
||||
"part 10: 'two entries with the SAME name are refused' - one Store "
|
||||
"namespace for two units is DEV-02's defect rebuilt in YAML"),
|
||||
("`migrate_unit_keys()`'s marker gate `if rep.ok:` -> `if rep.ok or "
|
||||
"rep.kept:` - the schema marker stamped on a migration that DROPPED THE "
|
||||
"LATCH, because one key happened to be namespaced already",
|
||||
"part 10: 'AND THE STORE IS NOT MARKED MIGRATED'. ⚠️ THIS ONE SURVIVED "
|
||||
"ALL 19 FILES and was found by MAINT-05's round-3 battery, not by this "
|
||||
"card: every migration check here reached the gate on a store where "
|
||||
"NOTHING was namespaced yet, so `rep.kept` was empty in all of them and "
|
||||
"the `or` never fired. Stamping there disarms the latch AND tells the "
|
||||
"next start the migration finished, so it never re-arms - DEV-02's own "
|
||||
"worst case filed under 'done'"),
|
||||
]
|
||||
# ⚠️ FOUR OF THESE SURVIVED THE FIRST TIME AND THE CHECKS WERE CHANGED, NOT THE
|
||||
# TABLE - two in DEV-02, two more in CFG-04, and the CFG-04 pair are recorded in
|
||||
@@ -2167,7 +2229,7 @@ for what, caught in MUTATIONS:
|
||||
check(f"all {len(MUTATIONS)} defeats of this card are caught by a named check "
|
||||
f"above. A guard whose checks survive its own defeat is not a guard, and "
|
||||
f"this repository has three separate records of exactly that",
|
||||
len(MUTATIONS) == 34)
|
||||
len(MUTATIONS) == 35)
|
||||
|
||||
|
||||
print()
|
||||
|
||||
@@ -806,6 +806,148 @@ check("...and a roster of ONE still returns before any of this, so the "
|
||||
is False)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
print()
|
||||
print("9c. THE REFUSAL HAS TO SURVIVE THE NEXT START: the same wrong call "
|
||||
"site, twice, over ONE store")
|
||||
|
||||
# ⚠️ WHY 9b WAS NOT ENOUGH, AND IT IS THIS FILE'S OWN RECURRING DEFECT ONE
|
||||
# LEVEL UP. 9b's recovery check re-runs the migration with `ROSTER[0]` - the
|
||||
# CORRECTED name - so it measures recovery along the one path where the
|
||||
# operator has ALREADY fixed the call site. The real failure is a call site that
|
||||
# is STILL WRONG on the next start, and until this section nothing in the 19
|
||||
# files called `migrate_maint_keys()` twice with the SAME wrong `unit`. 9b was
|
||||
# true and passed for the wrong reason.
|
||||
#
|
||||
# WHAT THAT HID. Measured at Controller level over one persisted store, roster
|
||||
# ["garage", "shed"], flat `last_completed` 2026-08-07, call site handing
|
||||
# `inverter-a` on both starts:
|
||||
#
|
||||
# construction 1: ok=False misdirected=4 moved=4 kept=0 marker=<absent>
|
||||
# construction 2: ok=True misdirected=0 moved=0 kept=4 marker=1
|
||||
#
|
||||
# `_verify_moves()` graded only `rep.moved`. On start 2 the wrong call site
|
||||
# finds the keys it wrote already present, rule 1 routes all four to `rep.kept`,
|
||||
# `rep.moved` is EMPTY, the roster loop iterates over nothing, `misdirected` is
|
||||
# empty and `ok` is True - so the marker is stamped and start 3 takes the
|
||||
# `from_version >= MAINT_SCHEMA_VERSION` early return FOREVER, including after
|
||||
# the call site is fixed. Loud-and-recoverable became silent-and-permanent after
|
||||
# ONE restart. The fix grades `set(rep.moved) | set(rep.kept)`.
|
||||
_m7 = flat_store()
|
||||
_rep7a = migrate_maint_keys(_m7, UNIT_A, ROSTER)
|
||||
_rep7b = migrate_maint_keys(_m7, UNIT_A, ROSTER)
|
||||
print(f" start 1: ok={_rep7a.ok} misdirected={len(_rep7a.misdirected)} "
|
||||
f"moved={len(_rep7a.moved)} kept={len(_rep7a.kept)} marker="
|
||||
f"{_m7.data.get(MAINT_SCHEMA_KEY, '<<absent>>')!r}")
|
||||
print(f" start 2: ok={_rep7b.ok} misdirected={len(_rep7b.misdirected)} "
|
||||
f"moved={len(_rep7b.moved)} kept={len(_rep7b.kept)} marker="
|
||||
f"{_m7.data.get(MAINT_SCHEMA_KEY, '<<absent>>')!r}")
|
||||
check("PRECONDITION, SO THIS SECTION CANNOT PASS FOR THE WRONG REASON: start 2 "
|
||||
"goes down the KEPT path, which is the path no other check in this file "
|
||||
"reaches. Not one key is written a second time - all four are already "
|
||||
"there from start 1, so `moved` is EMPTY and `kept` holds every one of "
|
||||
"them. If a later edit makes start 2 re-WRITE them, this goes red rather "
|
||||
"than the claim below quietly reverting to 9b's",
|
||||
_rep7b.ran and _rep7b.moved == {}
|
||||
and sorted(_rep7b.kept) == sorted(unit_key(UNIT_A, k)
|
||||
for k in MAINT_UNIT_KEYS))
|
||||
check("⚠️ THE MARKER IS STILL WITHHELD ON THE SECOND START. This is the whole "
|
||||
"section: a refusal that lasts exactly one start is not a refusal, it is "
|
||||
"a delay - the next start stamps the marker on a migration that has "
|
||||
"reached no readable key, and every start after that skips the migration "
|
||||
"entirely",
|
||||
MAINT_SCHEMA_KEY not in _m7.data)
|
||||
check("...and it is refused for the SAME reason as the first time, graded "
|
||||
"against the same roster: four keys under a namespace nothing on this "
|
||||
"site reads. `ok` False, `misdirected` full, `lost` empty - the caller "
|
||||
"was wrong, the store was not",
|
||||
not _rep7b.ok and not _rep7b.verified and not _rep7b.lost
|
||||
and len(_rep7b.misdirected) == len(MAINT_UNIT_KEYS)
|
||||
and repr(ROSTER) in _rep7b.explain())
|
||||
check("...and the history is still THERE to recover: rule 2 left every flat key "
|
||||
"in the file across both starts, and the roster's first pack still reads "
|
||||
"nothing - which is what makes this loud rather than lost",
|
||||
all(_m7.data[k] is not None for k in MAINT_UNIT_KEYS)
|
||||
and _m7.data["last_completed"] == "2026-08-07T12:00:00+00:00"
|
||||
and unit_key(ROSTER[0], "last_completed") not in _m7.data)
|
||||
_rep7c = migrate_maint_keys(_m7, ROSTER[0], ROSTER)
|
||||
print(f" start 3, call site FIXED: {_rep7c.explain()}")
|
||||
check("⚠️ AND BECAUSE THE MARKER WAS NEVER STAMPED, THE FIXED CALL SITE STILL "
|
||||
"GETS A RUN. A stamped marker would have made this an early return "
|
||||
"forever - the operator fixes the bug and the migration never happens "
|
||||
"again. Here the third start migrates the month into `garage` and stamps",
|
||||
_rep7c.ran and _rep7c.ok
|
||||
and _m7.data.get(unit_key(ROSTER[0], "last_completed"))
|
||||
== "2026-08-07T12:00:00+00:00"
|
||||
and _m7.data[MAINT_SCHEMA_KEY] == MAINT_SCHEMA_VERSION)
|
||||
|
||||
# The counterfactual, so this is not a check satisfied by a validator that
|
||||
# refuses every second call: the same two calls with the RIGHT name.
|
||||
_m7ok = flat_store()
|
||||
_rep7ok_a = migrate_maint_keys(_m7ok, ROSTER[0], ROSTER)
|
||||
_rep7ok_b = migrate_maint_keys(_m7ok, ROSTER[0], ROSTER)
|
||||
check("...and the counterfactual: the SAME two calls with the roster's own "
|
||||
"first name are accepted - stamped on the first, and the second is the "
|
||||
"`already at schema` early return, not a refusal. So 9c is about the "
|
||||
"NAME surviving into the second start, not about second calls",
|
||||
_rep7ok_a.ok and _m7ok.data[MAINT_SCHEMA_KEY] == MAINT_SCHEMA_VERSION
|
||||
and _rep7ok_b.ran is False and "already at schema" in _rep7ok_b.explain())
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
print()
|
||||
print("9d. THE OTHER GUARD ON THE SAME LINE: a migration that LOST STATE is "
|
||||
"not stamped either, even when some keys were already in place")
|
||||
|
||||
# ⚠️ WHAT THIS PINS, AND IT IS A SURVIVOR THE SECTIONS ABOVE CANNOT SEE.
|
||||
# `migrate_maint_keys()` gates the marker on `rep.ok`, which is BOTH guards:
|
||||
# `not lost and not misdirected`. Every check in this file that reaches the
|
||||
# marker reaches it through `misdirected` (9b, 9c) or through a store where
|
||||
# NOTHING was already namespaced (part 9's `LosesTheHistory`, whose `kept` is
|
||||
# empty). So the mutation
|
||||
#
|
||||
# if rep.ok: -> if rep.ok or rep.kept:
|
||||
#
|
||||
# stamped the marker on a migration that lost a month of history - defeating the
|
||||
# OLDER, settled `lost` guard as well as the new `misdirected` one - and all 19
|
||||
# suites stayed green on it. Measured differential on this exact fixture:
|
||||
# shipped -> marker absent; mutant -> marker 1, both reporting ok=False lost=2.
|
||||
#
|
||||
# The fixture is the one shape that separates them: TWO keys already namespaced
|
||||
# (so `kept` is non-empty and the mutant's `or` fires) and TWO keys whose write
|
||||
# the store drops (so `lost` is non-empty and `ok` is False). `misdirected` is
|
||||
# empty throughout - the unit handed in IS the roster's first name - so this
|
||||
# section cannot pass on the `misdirected` guard by accident.
|
||||
_m8 = LosesTheHistory(dict(flat_store(**{
|
||||
unit_key(ROSTER[0], "phase"): CHARGE,
|
||||
unit_key(ROSTER[0], "phase_started"): "2026-09-03T22:00:00+00:00"}).data))
|
||||
_rep8 = migrate_maint_keys(_m8, ROSTER[0], ROSTER)
|
||||
print(f" two keys already namespaced, two dropped on write: {_rep8.explain()}")
|
||||
print(f" ok={_rep8.ok} lost={len(_rep8.lost)} kept={len(_rep8.kept)} "
|
||||
f"misdirected={len(_rep8.misdirected)}; marker "
|
||||
f"{_m8.data.get(MAINT_SCHEMA_KEY, '<<absent>>')!r}")
|
||||
check("PRECONDITION: this fixture reaches the marker through the `lost` guard "
|
||||
"and ONLY that one - two keys in `kept`, two in `lost`, `misdirected` "
|
||||
"empty. If it ever stops having a non-empty `kept` the mutation below "
|
||||
"goes invisible again, so the shape is asserted rather than assumed",
|
||||
_rep8.ran and len(_rep8.kept) == 2 and len(_rep8.lost) == 2
|
||||
and not _rep8.misdirected and not _rep8.ok)
|
||||
check("⚠️ THE MARKER IS NOT STAMPED ON A MIGRATION THAT LOST A MONTH OF "
|
||||
"HISTORY, and the two keys that DID land do not buy it one. `rep.ok` is "
|
||||
"the gate and nothing weaker: stamping here would tell the next start a "
|
||||
"migration finished that dropped half of what it was moving",
|
||||
MAINT_SCHEMA_KEY not in _m8.data)
|
||||
check("...and the counterfactual, one property different: the SAME fixture on a "
|
||||
"store that does not drop writes IS stamped. So the refusal above is "
|
||||
"about the state that was lost, not about `kept` being non-empty",
|
||||
migrate_maint_keys(
|
||||
MemStore(dict(flat_store(**{
|
||||
unit_key(ROSTER[0], "phase"): CHARGE,
|
||||
unit_key(ROSTER[0], "phase_started"):
|
||||
"2026-09-03T22:00:00+00:00"}).data)),
|
||||
ROSTER[0], ROSTER).ok is True)
|
||||
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
print()
|
||||
|
||||
Reference in New Issue
Block a user