A store that ran pre-DEV-02 flat and declares inverters: [garage, shed] came up with its SAFETY-02 sign-disagreement latch DISARMED, on a state file that still reads sign_tripped: true.
migrate_unit_keys(store, UNIT_A) took a hardcoded constant where its sibling migrate_maint_keys(store, unit, roster) takes the roster and refuses a write no machine reads. The latch moved to unit/inverter-a/sign_tripped, _verify_moves() read it back under the name it had been handed, was satisfied, and stamped the marker - so the next start skipped the migration too. _grade_store_migration() did not catch it: its "is anything namespaced latched" test was satisfied by the misdirected value itself.
The reason it was left - "there is no roster yet" - was a half-truth. The KEYS must be written before the Inverters read them; the NAMES do not come from the Inverters. roster_names(opts) reads them off the options before anything is constructed, and returns [UNIT_A] on the flat shape, so today's upgrade path is byte-identical.
Two edits, and they do different jobs
Measured per leg by reverting each one:
build
SAME-STEP leg
STAGED leg
both edits (shipped)
clean
latch armed, litter reported
guard reverted
latch armed
DISARMED
call site reverted
latch armed, litter
latch armed, litter
The guard carries the safety; the call site carries the hygiene and the withheld marker. Only the guard reaches an install already stamped by 0.9.0, and STAGED is the path "upgrade now, declare the roster later" produces.
What review should push on
The UNIT_A narrowing. The first draft treated every off-roster latch as litter and test_inverter.py caught it re-arming a healthy A off a flat key that B's trip had mirrored. The narrowing is defensible but it is a heuristic on a name. Attack it.
The residual is stated, not fixed: a site whose own first unit is NAMED inverter-a, latched, then taken out of the roster gets re-armed on inverters[0].
The harness's new moved-not-lost rule. It suppresses a VANISHED finding when the same bare key at the same value appears under a key in machine_read. Show me a genuine loss it now hides.
Both replaced test sections were headed as specifications of the defect and asked to be replaced. Check they were replaced with assertions of the fix and not weakened.
28/28 test files pass. Host-side only - no rig, no hardware, nothing flashed.
A store that ran pre-DEV-02 flat and declares `inverters: [garage, shed]` came up with its SAFETY-02 sign-disagreement latch **DISARMED**, on a state file that still reads `sign_tripped: true`.
`migrate_unit_keys(store, UNIT_A)` took a hardcoded constant where its sibling `migrate_maint_keys(store, unit, roster)` takes the roster and refuses a write no machine reads. The latch moved to `unit/inverter-a/sign_tripped`, `_verify_moves()` read it back under the name it had been handed, was satisfied, and stamped the marker - so the next start skipped the migration too. `_grade_store_migration()` did not catch it: its "is anything namespaced latched" test was satisfied by the misdirected value itself.
The reason it was left - "there is no roster yet" - was a half-truth. The KEYS must be written before the `Inverter`s read them; the NAMES do not come from the `Inverter`s. `roster_names(opts)` reads them off the options before anything is constructed, and returns `[UNIT_A]` on the flat shape, so today's upgrade path is byte-identical.
## Two edits, and they do different jobs
Measured per leg by reverting each one:
| build | SAME-STEP leg | STAGED leg |
|---|---|---|
| both edits (shipped) | clean | latch armed, litter reported |
| guard reverted | latch armed | **DISARMED** |
| call site reverted | latch armed, litter | latch armed, litter |
The **guard** carries the safety; the **call site** carries the hygiene and the withheld marker. Only the guard reaches an install already stamped by 0.9.0, and STAGED is the path "upgrade now, declare the roster later" produces.
## What review should push on
1. **The `UNIT_A` narrowing.** The first draft treated every off-roster latch as litter and `test_inverter.py` caught it re-arming a healthy A off a flat key that B's trip had mirrored. The narrowing is defensible but it is a heuristic on a name. Attack it.
2. **The residual is stated, not fixed**: a site whose own first unit is NAMED `inverter-a`, latched, then taken out of the roster gets re-armed on `inverters[0]`.
3. **The harness's new moved-not-lost rule.** It suppresses a VANISHED finding when the same bare key at the same value appears under a key in `machine_read`. Show me a genuine loss it now hides.
4. Both replaced test sections were headed as specifications of the defect and asked to be replaced. Check they were replaced with assertions of the fix and not weakened.
28/28 test files pass. Host-side only - no rig, no hardware, nothing flashed.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU
⚠️ A DISARMED SAFETY-02 GUARD ON AN INSTALL WHOSE STATE FILE SAYS IT IS
PROTECTED. `Controller.__init__` called `migrate_unit_keys(store, UNIT_A)` - a
hardcoded constant - while its sibling `migrate_maint_keys(store, unit, roster)`
takes the roster and refuses a write no machine reads. On a store that ran
pre-DEV-02 flat and declares `inverters: [garage, shed]`, the latch moved to
`unit/inverter-a/sign_tripped`, `_verify_moves()` read it back under the name it
had been HANDED, was satisfied, and stamped the marker. Not one inverter came up
latched, and the next start skipped the migration too.
That is the MAINT-05 defect verbatim, still live in the function whose own
docstring describes it, and `_grade_store_migration()` did not catch it: its
"is anything namespaced latched" test was satisfied BY THE MISDIRECTED VALUE
ITSELF.
⚠️ THE REASON IT WAS LEFT WAS A HALF-TRUTH. "There is no roster yet - the
`Inverter`s restore their latch in their constructors." The KEYS must be written
before the `Inverter`s read them; the NAMES do not come from the `Inverter`s.
`roster_names(opts)` reads them off the options before anything is constructed -
which is exactly what it already existed to do for `MqttPublisher` - and returns
`[UNIT_A]` on the flat shape, so today's upgrade path is byte-identical.
TWO EDITS, AND THEY DO DIFFERENT JOBS. Measured per leg by reverting each one:
SAME-STEP leg STAGED leg
both edits (shipped) clean latch armed, litter reported
guard reverted latch armed ⚠️ DISARMED
call site reverted latch armed, litter latch armed, litter
So the GUARD carries the safety and the CALL SITE carries the hygiene and the
withheld marker. The table is in test_rehearse.py part 5 because a reader who
assumed the roster-aware migration re-arms the latch would delete the guard as
redundant in a tidy-up and take the STAGED column with it - and STAGED is what
"upgrade now, declare the roster later" produces, which is the path an operator
actually takes. Only the guard reaches an install already stamped by 0.9.0.
⚠️ AND "OFF THE ROSTER" ALONE WAS TOO BLUNT, WHICH test_inverter.py CAUGHT. The
first draft treated every off-roster latch as litter. In DEV-02's restart case
the store carries `unit/inverter-b/sign_tripped = True` from B's own trip while
the roster being graded holds only A - and the blunt rule re-armed a HEALTHY A
off the flat key B's trip had mirrored. DEV-02's defect rebuilt inside its own
guard, which is what the paragraph I was overruling had been right to fear.
Narrowed to `UNIT_A`, which is the only name the broken migration ever wrote -
`roster_names()` falls back to a POSITIONAL `inverter-N`, so `inverter-a` is not
a name this code hands to anyone else. A latch stranded under any other name is
a real unit out of today's config and still counts as protection. Residual,
stated in the code rather than left to be found: a site whose own first unit is
NAMED `inverter-a`, latched, then taken out of the roster gets re-armed on
`inverters[0]` - a healthy unit held at 0 W until an operator clears it. The
conservative direction, against a site DRIVING on a file that says it is latched.
⭐ AND FIXING THE CODE EXPOSED A FALSE POSITIVE IN THE HARNESS. With the latch
now landing under `garage`, `rehearse_roster.py` reported VANISHED STORE KEY for
`unit/inverter-a/sign_tripped` three lines above the same value sitting under
`unit/garage/sign_tripped`. A key that moved to a namespace a machine reads has
not vanished - and an alarm that fires on the harmless case is one an operator
learns to dismiss before the real one arrives, which is CONTROL-C's own
argument. The test is not "the name appears somewhere": same bare key, same
value, under a key in `machine_read`, the set built by driving a real start. A
latch that genuinely went missing has no such key and is still a finding, which
M3 (delete the flat rollback view) still measures red.
Test parts replaced deliberately, as both of them asked to be:
- test_rehearse.py part 5 asserted the defect "as REQUIREMENTS so that a fix
turns them red". It now asserts the fix, with 5b and 5c as negative controls
- one per edit - and 5b doubles as the leg-splitter's positive control,
since reverting the guard is the build where the two legs DISAGREE.
- test_maintenance.py section 13 was headed "RECORDED, NOT FIXED" and said the
fixing card "should expect them to go red and should replace them". Replaced,
plus two cases it could not have had: the already-stamped store, and the
`inverter-b` negative control for the narrowing above.
28/28 files pass. Not run: nothing - this is host-side only, no rig, no hardware.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU
⚠️ REVIEW DEFEATED THIS CARD'S OWN FIX WITH ONE ADDED KEY, and the project's own
instrument agreed on the unmodified tree. The guard classified each key and then
wrote ONE site-wide `namespaced_latched` for BOTH "on the roster" and
"off-roster but not UNIT_A", so a single latched key belonging to a unit the
operator had removed from `inverters:` vetoed the repair of the misdirected
`inverter-a` latch beside it. Measured, roster ['garage','shed'], store stamped
so the migration fix cannot reach it:
sign_tripped + unit/inverter-a/sign_tripped -> [('garage', True), ...]
...AND unit/old-shed/sign_tripped -> ⚠️ [('garage', False), ...]
A site DRIVING on a state file that reads `sign_tripped: true` - the sentence in
this card's own first paragraph - and `tools/rehearse_roster.py` printed
`LATCH CAME UP DISARMED` on it, exit 1, against shipped code. It matters more
than a normal hole because this guard is, as main.py says, the ONLY repair path
for an install already stamped by 0.9.0.
⚠️ AND IT IS INSTANCE #17 OF HABIT 4, IN THE EXTENSION MUTATION.md ADDED TWO
DAYS AGO. The two fixtures written for this card each hold ONE arrangement -
`_staged_store` has only `inverter-a`, `_other_store` only `inverter-b` - so the
flag was never written twice with opposite intents in a single store. A per-key
check whose keys have never disagreed WITHIN ONE STORE is a check over whichever
key came last. Three variables now, one per case, and a fixture holding BOTH.
Four cases driven: stranded-only repairs, +1 unrelated repairs, +2 repairs,
off-roster-only does NOT (which is DEV-02's control, still green).
⭐ AND THE MOVED-NOT-LOST RULE HID A GENUINE LATCH LOSS. The match was bare key
plus value over the whole of `machine_read`, with no identity - and
`DEFAULTS["sign_tripped"] is False`, so the ONLY non-default value a latch can
hold is `True`. Any surviving latched roster unit therefore suppressed any other
unit's vanished latch. Review measured a build that drops `unit/shed/sign_*`
while `unit/garage/sign_*` survives, both latched by one site event so the
reasons matched too:
none in LEG SAME-STEP: nothing vanished, nothing stranded ...
NO FINDINGS in the rehearsed transitions exit 0
A roster unit's persisted SAFETY-02 latch deleted, transition reported clean.
The legitimate case is a RENAME - the key left `inverter-a`, which is not on
this roster, and arrived under `garage`, which is - so the vanished key's own
unit must be OFF the roster. A key whose unit is still on the roster has not
been renamed, it has been lost, and no amount of the same value elsewhere makes
that untrue.
⚠️ AND THE BOUND I CITED FOR IT DID NOT EXIST. Both the comment and the last
commit message said "what M3 (delete the flat rollback view) measures". M3 is
not that mutant - it drops `sign_reason` from `LEGACY_UNIT_KEYS` - and the one I
meant, M9, vanishes only FLAT keys, so neither can satisfy the `unit/` gate the
rule sits behind. NEITHER EXERCISED THE BRANCH; it had no negative control at
all, in a card whose subject is checks that cannot reach the failure they name.
A citation is the worst carrier for that, because it reads as having been
checked. M12 is written for it and driven: a roster unit that stops restoring
its latch is exit 1, and the suppression does not swallow it.
`misdirected` IS UNREACHABLE FROM THE CALL SITE, and the claim that it was a
benefit is withdrawn rather than restated. `unit` is `roster_names(opts)[0]` and
`build_roster()` names its first entry from the same list, so `unit` is a member
of `roster` by construction; review measured it - dropping the `roster` argument
inside `migrate_unit_keys()` survives 28/28. The argument stays for the reason
`_verify_moves()` gives for its own `roster=None` branch, it is what a NEW
caller gets - and a guard nothing exercises is a guard nobody knows works, so it
is now exercised at the function's own level: a wrong `unit` comes back
misdirected, the marker is not stamped, and the same call with a roster name
stamps it. Dead `if _roster_names else UNIT_A` removed; `roster_names()` never
returns [].
THE OPERATOR MESSAGE IS ASSERTED. Review deleted it whole (`if stranded:` ->
`if False:`) and 28/28 stayed green - `stranded` had no other consumer, so the
list existed only to print a line nothing checked, and that line is the entire
operator-facing half of the repair. Now: exactly one SAFETY-44 line at ERROR,
naming the stranded key and the roster it was measured against.
A wrong line citation removed: the call-site comment said "line 315's comment"
and "two hundred lines below", and both were wrong (315 is the CFG-03 defaults
note; the comment meant is ~54 lines down). Cite the thing, not its coordinates.
⚠️ AND WHAT 5b/5c ACTUALLY PIN IS RECORDED, because review is owed the
distinction: `mutant_tree()` asserts its target exists, so EDITING the guarded
line makes the file red at the ANCHOR before any behaviour is measured - which
looks identical from the exit code and proves much less. It happened twice while
writing this commit. The behavioural coverage lives in test_maintenance.py
section 13 and test_inverter.py, which build stores rather than strings.
Fixture strings no longer say "measured" - they are synthetic, and the repo's
other fixture carries an explicit SYNTHETIC header.
28/28 files pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU
⚠️ THE CONTROL I WROTE LAST ROUND WAS INERT, AND REVIEW MEASURED IT. M12 ran its
mutant over `state-legacy.json` and the findings diff against the UNMUTATED tree
was EMPTY. It "passed" because that fixture's STAGED leg is already exit 1 on
pre-existing litter, and `"migrated unit/shed/" not in out` was trivially true
because a pre-DEV-02 FLAT store has no shed latch to lose - after migration only
the FIRST roster entry is ever latched.
The decisive measurement is the one it could not make: reverting the
`vanished_unit not in after.units` narrowing - restoring exactly the rule that
hides a real latch loss - left 28/28 GREEN.
⚠️ SO IT WAS HABIT 4 WEARING THE CLOTHES OF ITS OWN FIX, AND WORSE THAN THE
"NO CONTROL" IT REPLACED, because it reads as checked. That is this commit's
own argument about the M3 mis-citation, turned on the thing I wrote to answer it.
⭐ THE FIXTURE CAME FIRST THIS TIME. `state-both-latched.json` is a store at
SCHEMA_VERSION with `unit/garage/*` AND `unit/shed/*` both latched by ONE site
event, so the reasons match too - the arrangement in which same-value
suppression is most dangerous, and the one no existing fixture could produce.
M12's mutant then makes ONE roster unit's latch key VANISH while its sibling's
survives at the same value:
shipped tree, both-latched fixture exit 0, no findings
one unit's latch key deleted exit 1, ADDS
VANISHED STORE KEY unit/shed/sign_tripped
narrowing reverted ⚠️ RED (was 28/28 green)
And it asserts the PRECONDITION - the fixture is clean on the shipped tree -
plus that the mutation ADDS findings rather than coinciding with ones the
fixture already had. An empty diff there is exactly what made the last draft
inert, and nothing said so.
⭐ AND THE MOVE IS NOW INJECTIVE, which is the narrower hole review found still
open. `moved_to` is "any machine-read key with the same bare name and the same
value", and `DEFAULTS["sign_tripped"] is False` means `True` is the ONLY
non-default value a latch can hold - so N vanished keys could all claim the SAME
destination, by construction rather than by coincidence. Measured on a rename
carrying `inverter-a` -> `garage` correctly while DROPPING `inverter-b` -> `shed`:
migrated unit/inverter-a/sign_tripped = True -> unit/garage/sign_tripped
migrated unit/inverter-b/sign_tripped = True -> unit/garage/sign_tripped
Zero store-key findings, on a run where `shed` came up UNLATCHED. Two keys
cannot both have become one key. `diff()` claims each destination once.
⚠️ AND THE CHECK FOR IT IS ANCHORED TO A RUN THAT ACTUALLY PRODUCES MIGRATED
LINES. A dedicated mutant was written first and asserted over an EMPTY list -
vacuous, passing, indistinguishable from working, the same defect twice in one
sitting. These fixtures cannot produce two claimants (`migrate_unit_keys()`
moves FLAT keys only, so one flat latch yields one migration), so rather than
build a fiction to grade, injectivity is asserted over part 5's real migrations,
with a PRECONDITION that there are at least two of them. THE TWO-CLAIMANT
ARRANGEMENT IS A STATED RESIDUAL: bounded in `diff()` by construction, not
measured end-to-end.
Finding 4, and my correction to it was also wrong. The comment cited "line 315"
and "two hundred lines below"; the fix I wrote said "~54 lines down", which was
review's distance to the WRONG line, carried to a new referent without
re-measuring - inside the sentence apologising for a wrong number. Recomputed at
5e382f5: 327-261 = 66, 327-252 = 75, and 54 was neither. There is no distance
now, because a distance is a coordinate and the sentence already names the thing.
And the residual gains its second half, which review noticed and I had not: the
reason re-armed from is the FLAT mirror, carrying whatever tripped LAST, so this
path can print one inverter's evidence under another's name. The latch is right
and the explanation may not be. `test_maintenance.py`'s check that the reason is
not the removed unit's is guaranteed by that fixture's flat string, NOT by the
code, and now says so.
28/28 files pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU
⚠️ REVIEW FOUND THE INJECTIVITY CHECK INERT IN THE SAME WAY M12's FIRST DRAFT
WAS, three paragraphs below the comment diagnosing it. Removing the whole
mechanism left 28/28 GREEN: part 5's two destinations are `sign_reason` and
`sign_tripped`, DIFFERENT bare keys, which can never contend. The
`len(_dests) >= 2` precondition was true and established nothing - "not
asserting over an empty list" was correct, "this run can exercise injectivity"
was not.
Review also measured a second, narrower silent loss: `moved_to` accepts a
destination regardless of whether the TRANSITION put the value there, so a
roster unit already latched in its own right is a free suppression slot. A build
deleting `old-a` while renaming `old-b` -> `shed` reported exit 0, NO FINDINGS,
with `old-a` "migrated" into a key that already held True.
⚠️ I WROTE BOTH ONE-LINE FIXES AND HAVE REVERTED BOTH, because THIS SUITE
CANNOT MEASURE EITHER. Three attempts, three different ways of proving nothing:
1. assert injectivity over part 5's real destinations
-> passes with the entire mechanism deleted (non-contending bare keys)
2. build a dedicated fixture for the two-claimant case
-> discriminates NOTHING: both snapshots read one state file, so nothing
vanishes and there are no migrated lines with the clause or without it
3. rebuild M12's fixture so the destination is transition-made, to reach the
provenance case
-> ⚠️ DISARMED M12 FOR THE NARROWING. All three clause-removals came back
GREEN at once, and the control I had measured RED last round was gone.
⭐ (3) IS WHY THIS IS A SPLIT AND NOT A JUDGEMENT CALL. A later fix to the same
function silently disarmed the control for an earlier one, and I saw it only
because I re-drove the removal rather than trusting the suite's exit code.
Shipping two more unmeasured clauses into the rehearsal instrument, on a card
whose entire subject is checks that cannot reach what they name, would be the
fourth inert artifact in one PR. They go on their own card, with review's
measurements as the starting evidence and the fixture built BEFORE the fix -
which is the ordering that worked for M12 and failed every time I inverted it.
⚠️ AND THE SPLIT IS SAFE, WHICH IS WHY IT IS AVAILABLE. Both remaining holes are
in the REHEARSAL TOOL, not the controller. The round-2 narrowing guarantees a
suppressed key is always OFF the roster, so no roster unit loses protection;
what is left is rollback-view/history mis-reporting on a tool an operator runs
before commissioning. Nothing here can disarm a latch on a live site, and
inverter B goes in on ~2026-09-12. Review offered this split explicitly.
M12's PRECONDITION IS STRENGTHENED, because the exit code could not carry it.
It now requires the shipped tree to be clean AND to not already report the
finding the mutation exists to produce. The first draft passed on exactly that
gap: the fixture's own litter made the run red and nothing separated the two.
Re-driven after the revert, on the round-3 fixture:
shipped exit 0
`vanished_unit not in after.units` gone ⚠️ exit 1
28/28 files pass. The controller half is unchanged since d1b7e3f and review has
no further concerns about it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU
Review APPROVED f109153 and the split. Three asks, all applied, plus a
correction it caught in the round-4 commit message.
C STAYS IN-TREE. Attribution is a third property of the same comprehension -
`sorted()` decides which vanished key wins a destination, so a build renaming
`inverter-b` -> `garage` while DELETING `inverter-a` names the deleted key as
migrated and the one that really moved as VANISHED. A reader of the disclosure
paragraph wants it beside the other two rather than only in Trello. One
sentence, same block.
AND A FOURTH CLAUSE REVIEW MEASURED THIS ROUND: `and after.data.get(k) ==
value` survives the suite and is load-bearing. Without it the rule degenerates
to BARE-NAME matching - a deleted `unit/old-x/sign_reason` was reported as
having migrated into a `sign_reason` holding a COMPLETELY DIFFERENT SENTENCE,
and essentially every off-roster vanished `unit/*` key would be suppressed.
Added to the paragraph and to INSTR-01, so all of `moved_to` is covered by one
fixture design rather than two-thirds of one.
⚠️ AND THE POINT IS THAT THEY ARE FOUR CLAUSES OF **ONE COMPREHENSION**, WHICH
IS STRUCTURAL AND NOT A LIST OF CHORES. Four conditions, four different reasons
to exist, one fixture set that can reach only some at a time - so rebuilding a
fixture to reach clause N moves the others. That is exactly how round 4's
disarming happened, and it is what INSTR-01 has to design against rather than
patch around. Review called this the sharpest thing found in four rounds and it
is a fact about the function, not about the two clauses that exposed it.
⚠️ AND I OVERCLAIMED FOR MY OWN PRECONDITION. The round-4 message said the
strengthened M12 precondition was needed because "the exit code alone cannot
say the second thing". Review checked: on a clean run the finding regex matches
nothing, so the second conjunct is IMPLIED by the first today. It is
belt-and-braces, load-bearing only if the exit-0 requirement is ever relaxed.
WHAT ACTUALLY CLOSED THE FIRST DRAFT'S GAP IS `_m12_new` BEING A SET DIFFERENCE
- the mutation must ADD a finding - and that is the check the narrowing's
removal turns red. The wrong half was credited, and the comment now says which
half does the work.
Nit: the fixture's trailing newline and two backticks, lost last round.
`rehearse_roster.py` is still byte-identical to d1b7e3f - this round touches
only the test file's disclosure paragraph and the fixture's comment. 28/28 pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU
admin
merged commit 806390d8b1 into release/1.02026-09-08 19:16:33 +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.
A store that ran pre-DEV-02 flat and declares
inverters: [garage, shed]came up with its SAFETY-02 sign-disagreement latch DISARMED, on a state file that still readssign_tripped: true.migrate_unit_keys(store, UNIT_A)took a hardcoded constant where its siblingmigrate_maint_keys(store, unit, roster)takes the roster and refuses a write no machine reads. The latch moved tounit/inverter-a/sign_tripped,_verify_moves()read it back under the name it had been handed, was satisfied, and stamped the marker - so the next start skipped the migration too._grade_store_migration()did not catch it: its "is anything namespaced latched" test was satisfied by the misdirected value itself.The reason it was left - "there is no roster yet" - was a half-truth. The KEYS must be written before the
Inverters read them; the NAMES do not come from theInverters.roster_names(opts)reads them off the options before anything is constructed, and returns[UNIT_A]on the flat shape, so today's upgrade path is byte-identical.Two edits, and they do different jobs
Measured per leg by reverting each one:
The guard carries the safety; the call site carries the hygiene and the withheld marker. Only the guard reaches an install already stamped by 0.9.0, and STAGED is the path "upgrade now, declare the roster later" produces.
What review should push on
UNIT_Anarrowing. The first draft treated every off-roster latch as litter andtest_inverter.pycaught it re-arming a healthy A off a flat key that B's trip had mirrored. The narrowing is defensible but it is a heuristic on a name. Attack it.inverter-a, latched, then taken out of the roster gets re-armed oninverters[0].machine_read. Show me a genuine loss it now hides.28/28 test files pass. Host-side only - no rig, no hardware, nothing flashed.
🤖 Generated with Claude Code
https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU