SAFETY-31 AC 4: the alarm's attribution was graded by nothing, and a mutation proved it #68

Merged
admin merged 2 commits from SAFETY-31-attribution into release/1.0 2026-09-08 04:27:17 +02:00
Owner

Closes the first of the two gaps the 2026-09-07 verification left open on SAFETY-31. The rig half is a separate PR against admin/goodwe (SAFETY-31-rig) — the scenario cannot live in this repository.

The gap

A mutation survived the whole 26-file suite:

app/main.py:3119   pos, neg = opposing(...)  ->  ([], [])   (-102 B)   SURVIVED 26/26

It silently deletes the "Right now garage is discharging while shed charges" clause from the operator notification. circulation.py's own docstring calls that sentence the first thing somebody woken by this alarm needs, and "impossible to reconstruct from the number alone". The commit that shipped this card claimed "18 mutations, all RED"; the DECISIONS ledger M1–M19 has no entry for it.

It is not dead code. Ledger M18 — the whole await check_circulating() call removed — is red on four checks, so the function runs. The three checks on that notification asserted METER CANNOT SEE, CONTROL HAS BEEN STOPPED and isolator, and never an inverter name.

The fix

test_watchdog.py §6a, on a fixture built to tell three failures apart:

  • the roster is ["garage", "shed", "cellar"] — operator names, none of them UNIT_A. This is test_maintenance.py §11's lesson: on a fixture whose names are the module's own constants, a check cannot tell the roster from the constant.
  • the site runs twice, mirrored, so the sentence is not symmetric and a pos/neg swap is a different red from a deletion.
  • a third pack rests at 5 W, so the floor that stops a standby pack being named as a participant in a 6 kW loop is graded too.

Mutations — every one run on a tree with no __pycache__, mounted :ro into the checks image

# mutation Δ result
M20 pos, neg = opposing(...) → ([], []) −102 B RED (2 checks) — the survivor
M21 pos and neg exchanged +0 B RED (3 checks)
M22 opposing(..., min_w=cfg.min_w / 2.0) → min_w=0.0 −25 B RED (3 checks)
=== MUTATION M20  (-102 bytes)
test_watchdog.py exit=1
  FAIL THE ALARM NAMES THE TWO PACKS AND SAYS WHICH WAY EACH ONE IS PULLING...
  FAIL AND WITH THE TWO SWAPPED, THE SENTENCE SWAPS...
2 FAILED

=== MUTATION M21  (0 bytes)
test_watchdog.py exit=1
  FAIL THE ALARM NAMES THE TWO PACKS...
  FAIL AND WITH THE TWO SWAPPED, THE SENTENCE SWAPS...
  FAIL ...and each run carries ONLY its own sentence...
3 FAILED

=== MUTATION M22  (-25 bytes)
test_watchdog.py exit=1
  FAIL THE ALARM NAMES THE TWO PACKS...
  FAIL AND WITH THE TWO SWAPPED, THE SENTENCE SWAPS...
  FAIL ...and the RESTING pack is not named. `cellar` is on the roster...
3 FAILED

Suite with the block in place: 26/26 green.

⚠️ And a disclosure, which is the uncomfortable half

DECISIONS/SAFETY-31-circulating-power.md §3 records that the 924-run campaign behind circulation_w: 700 and circulation_hold_s: 75 used a harness committed in neither repository. Two further sweeps have now been run and no two of the three agree:

sweep worst peak worst continuous trips
924-run campaign — harness uncommitted 1700 W 505.0 s at 100 W; 5.0 s above 700 W 0 / 924
independent reviewer sweep, 2026-09-07 2750 W 15.0 s above 700 W 0 / 48
two_master_circulation, 840 runs — committed 1048 W 10.0 s at 100 W; 5.0 s above 700 W 0 / 840

This is not evidence of fabrication and is not reported as any. The committed sweep measures why the spread is that wide — on one grid, changing only the plant model and the write-stagger set:

plant / stagger worst legitimate peak
modelled plant (lag 10 s, ramp 800 W/s), staggers up to a whole cycle 1048 W
INSTANT (lag 0, unbounded ramp), staggers up to a whole cycle 2250 W
INSTANT, staggers strictly under a whole cycle 0 W

A factor of 2.1×, and a factor of infinity, between three defensible descriptions of the same rig. The plant and the stagger are terms of the number. Three sweeps can disagree that much without anybody having invented anything — and nobody can settle it while the instrument is missing.

What survives: the window argument. All three sweeps trip zero times, across 924 + 48 + 840 non-faulty runs, and all three put the worst continuous episode above 700 W at one to three control cycles against a fifteen-cycle window.

What does not survive as stated: 505.0 s at 100 W, which is what the "knee at 400 W" argument for circulation_w: 700 rests on. The committed grid measures 10.0 s there. The curve still falls with the threshold (126/840 runs cross 100 W against 1/840 at 700 W), so the knee is real in direction; its depth is established by nothing anybody can run. §3 now says so, and the overclaiming comment at test_watchdog.py's 1700 W fixture ("the worst single transient the rig can produce") is replaced with what the fixture can support. The number in the check is unchanged.

Nothing about the guard is compromised. 0 trips in 3 independent grids.

What would settle it: the original harness, committed.

Not done here

sim/inject.py was never executed in this session (prohibited), and no hardware, container or rig lock was touched. The add-on suite ran in goodwe-addon-checks:1 against a read-only mount.

🤖 Generated with Claude Code

https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU

Closes the first of the two gaps the 2026-09-07 verification left open on SAFETY-31. **The rig half is a separate PR against `admin/goodwe` (`SAFETY-31-rig`)** — the scenario cannot live in this repository. ## The gap A mutation **survived the whole 26-file suite**: ``` app/main.py:3119 pos, neg = opposing(...) -> ([], []) (-102 B) SURVIVED 26/26 ``` It silently deletes the *"Right now garage is discharging while shed charges"* clause from the operator notification. `circulation.py`'s own docstring calls that sentence the first thing somebody woken by this alarm needs, and *"impossible to reconstruct from the number alone"*. The commit that shipped this card claimed *"18 mutations, all RED"*; the DECISIONS ledger M1–M19 has no entry for it. **It is not dead code.** Ledger M18 — the whole `await check_circulating()` call removed — is red on four checks, so the function runs. The three checks on that notification asserted `METER CANNOT SEE`, `CONTROL HAS BEEN STOPPED` and `isolator`, and **never an inverter name**. ## The fix `test_watchdog.py` §6a, on a fixture built to tell three failures apart: * the roster is **`["garage", "shed", "cellar"]`** — operator names, none of them `UNIT_A`. This is `test_maintenance.py` §11's lesson: on a fixture whose names are the module's own constants, a check cannot tell the roster from the constant. * the site runs **twice, mirrored**, so the sentence is not symmetric and a `pos`/`neg` swap is a *different* red from a deletion. * a **third pack rests at 5 W**, so the floor that stops a standby pack being named as a participant in a 6 kW loop is graded too. ## Mutations — every one run on a tree with no `__pycache__`, mounted `:ro` into the checks image | # | mutation | Δ | result | |---|---|---:|---| | **M20** | `pos, neg = opposing(...)` → `([], [])` | −102 B | **RED** (2 checks) — *the survivor* | | M21 | `pos` and `neg` exchanged | **+0 B** | **RED** (3 checks) | | M22 | `opposing(..., min_w=cfg.min_w / 2.0)` → `min_w=0.0` | −25 B | **RED** (3 checks) | ``` === MUTATION M20 (-102 bytes) test_watchdog.py exit=1 FAIL THE ALARM NAMES THE TWO PACKS AND SAYS WHICH WAY EACH ONE IS PULLING... FAIL AND WITH THE TWO SWAPPED, THE SENTENCE SWAPS... 2 FAILED === MUTATION M21 (0 bytes) test_watchdog.py exit=1 FAIL THE ALARM NAMES THE TWO PACKS... FAIL AND WITH THE TWO SWAPPED, THE SENTENCE SWAPS... FAIL ...and each run carries ONLY its own sentence... 3 FAILED === MUTATION M22 (-25 bytes) test_watchdog.py exit=1 FAIL THE ALARM NAMES THE TWO PACKS... FAIL AND WITH THE TWO SWAPPED, THE SENTENCE SWAPS... FAIL ...and the RESTING pack is not named. `cellar` is on the roster... 3 FAILED ``` Suite with the block in place: **26/26 green.** ## ⚠️ And a disclosure, which is the uncomfortable half `DECISIONS/SAFETY-31-circulating-power.md` §3 records that the 924-run campaign behind `circulation_w: 700` and `circulation_hold_s: 75` used a harness **committed in neither repository**. Two further sweeps have now been run and no two of the three agree: | sweep | worst peak | worst continuous | trips | |---|---:|---:|---:| | 924-run campaign — harness uncommitted | 1700 W | 505.0 s at 100 W; 5.0 s above 700 W | 0 / 924 | | independent reviewer sweep, 2026-09-07 | **2750 W** | **15.0 s above 700 W** | 0 / 48 | | `two_master_circulation`, 840 runs — **committed** | 1048 W | 10.0 s at 100 W; 5.0 s above 700 W | **0 / 840** | **This is not evidence of fabrication and is not reported as any.** The committed sweep measures *why* the spread is that wide — on one grid, changing only the plant model and the write-stagger set: | plant / stagger | worst legitimate peak | |---|---:| | modelled plant (lag 10 s, ramp 800 W/s), staggers up to a whole cycle | 1048 W | | `INSTANT` (lag 0, unbounded ramp), staggers up to a whole cycle | 2250 W | | `INSTANT`, staggers strictly under a whole cycle | **0 W** | A factor of 2.1×, and a factor of infinity, between three defensible descriptions of the same rig. **The plant and the stagger are terms of the number.** Three sweeps can disagree that much without anybody having invented anything — and nobody can settle it while the instrument is missing. **What survives:** the window argument. All three sweeps trip **zero** times, across 924 + 48 + 840 non-faulty runs, and all three put the worst continuous episode above 700 W at one to three control cycles against a fifteen-cycle window. **What does not survive as stated:** `505.0 s at 100 W`, which is what the *"knee at 400 W"* argument for `circulation_w: 700` rests on. The committed grid measures **10.0 s** there. The curve still falls with the threshold (126/840 runs cross 100 W against 1/840 at 700 W), so the knee is real in direction; its **depth is established by nothing anybody can run**. §3 now says so, and the overclaiming comment at `test_watchdog.py`'s 1700 W fixture (*"the worst single transient the rig can produce"*) is replaced with what the fixture can support. The number in the check is unchanged. **Nothing about the guard is compromised.** 0 trips in 3 independent grids. **What would settle it:** the original harness, committed. ## Not done here `sim/inject.py` was never executed in this session (prohibited), and no hardware, container or rig lock was touched. The add-on suite ran in `goodwe-addon-checks:1` against a read-only mount. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU
admin added 1 commit 2026-09-07 19:10:02 +02:00
`pos, neg = opposing(...)` -> `([], [])` in main.py's `check_circulating()`
(-102 B) deletes the "Right now X is discharging while Y charges" clause from
the operator notification, and SURVIVED ALL 26 FILES. The commit that shipped
this card claimed "18 mutations, all RED"; the ledger has no entry for the
attribution, and this is that entry.

It is not dead code. Ledger M18 - the whole `await check_circulating()` call
removed - is red on four checks, so the function runs. The three checks on the
notification asserted METER CANNOT SEE, CONTROL HAS BEEN STOPPED and `isolator`,
and never an inverter name. `circulation.py`'s own docstring calls that sentence
the first thing somebody woken by this alarm needs and "impossible to
reconstruct from the number alone".

test_watchdog.py section 6a closes it on a fixture that can tell three failures
apart:

  * the roster is ["garage", "shed", "cellar"] - operator names, none of them
    `UNIT_A`, for test_maintenance.py section 11's reason: on a fixture whose
    names are the module's own constants a check cannot tell the roster from
    the constant;
  * the site is run TWICE, MIRRORED, so the sentence is not symmetric and a
    pos/neg swap is a different red from a deletion;
  * a third pack rests at 5 W, so the floor that stops a pack in standby being
    named as a participant in a 6 kW loop is graded too.

Mutations, on a tree with no __pycache__ mounted :ro into the checks image:

  M20  pos, neg = opposing(...) -> ([], [])          -102 B  RED (2 checks)
  M21  pos and neg exchanged                          +0 B   RED (3 checks)
  M22  opposing(..., min_w=cfg.min_w/2.0) -> 0.0      -25 B  RED (3 checks)

26/26 files green with the block in place.

AND A DISCLOSURE, WHICH IS THE UNCOMFORTABLE HALF. DECISIONS section 3 records
that the 924-run campaign behind `circulation_w: 700` and `circulation_hold_s:
75` used a harness that is not committed in either repository. Two further
sweeps have now been run and no two of the three agree: peaks of 1700 W, 2750 W
and 1048 W, and worst-continuous figures above 700 W of 5.0 s, 15.0 s and 5.0 s.

That is NOT evidence of fabrication and is not reported as any. The committed
sweep measures why the spread is that wide: on one grid, changing only the
plant model and the write-stagger set, the worst legitimate peak is 1048 W, or
2250 W, or 0 W. The plant and the stagger are TERMS of the number. What the
spread does establish is that the claim cannot be adjudicated while the
instrument is missing.

The window argument survives and is now committed - all three sweeps trip zero
times, on 924 + 48 + 840 non-faulty runs. The threshold CURVE does not survive
as stated: `505.0 s at 100 W`, which is what the "knee at 400 W" argument rests
on, is 10.0 s on the committed grid. The curve still falls with the threshold,
so the knee is real in direction; its depth is established by nothing anybody
can run, and this commit says so rather than repeating it. The overclaiming
comment at test_watchdog.py's 1700 W fixture ("the worst single transient the
rig can produce") is replaced with what the fixture can support.

The committed, re-runnable half is admin/goodwe's `two_master_circulation`,
opened as a separate PR against that repository.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU
admin added 1 commit 2026-09-07 20:13:03 +02:00
Review reproduced every figure in both PRs except one, and rejected on it.

⚠️ "0 of 90 GAINS x SPLITS combinations produce opposing shares" - the SUBSTANCE
is right and review re-measured it, but the DENOMINATOR is invented. sitesim.py
declares GAINS with 7 entries and SPLITS with 6: the grid is 42. 90 is not even
divisible by 7, so no reading of "GAINS x SPLITS" reaches it. It is almost
certainly TEST-20`s RUN count (84 + 6), which is a different quantity that
happened to be nearby.

⚠️ AND THE WORST COPY OF IT WAS INSIDE THE `PRECONDITION FAILED` TEXT, so the
next engineer to see this scenario go red would have been handed an
unreproducible measurement as their diagnosis - the exact defect class this
campaign exists to correct, in the failure path where it does the most harm.

Corrected in all seven places. At the failure-text site the denominator is now
`len(GAINS) * len(SPLITS)`, computed rather than typed, so it cannot drift from
the grid again - the same fix test_translations.py already took.

Also N3: a missing blank line was going to render the S3 and M19 paragraphs as
one in DECISIONS.

Re-run after the change: `inject.py two_master_circulation` PASS in 35 s, norig,
no rig lock taken. Add-on suite 26 files, all pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU
admin merged commit a0d981731c into release/1.0 2026-09-08 04:27:17 +02:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: admin/goodwe-addon#68